55namespace OpenFeature \implementation \multiprovider ;
66
77use InvalidArgumentException ;
8+ use OpenFeature \implementation \multiprovider \strategy \BaseEvaluationStrategy ;
9+ use OpenFeature \implementation \multiprovider \strategy \FirstMatchStrategy ;
10+ use OpenFeature \implementation \multiprovider \strategy \StrategyEvaluationContext ;
11+ use OpenFeature \implementation \multiprovider \strategy \StrategyPerProviderContext ;
12+ use OpenFeature \implementation \provider \AbstractProvider ;
13+ use OpenFeature \implementation \provider \Reason ;
14+ use OpenFeature \implementation \provider \ResolutionDetailsBuilder ;
15+ use OpenFeature \implementation \provider \ResolutionError ;
16+ use OpenFeature \interfaces \flags \EvaluationContext ;
17+ use OpenFeature \interfaces \provider \ErrorCode ;
818use OpenFeature \interfaces \provider \Provider ;
9- use OpenFeature \interfaces \strategy \ Strategy as StrategyInterface ;
10- use Psr \ Log \ LoggerAwareTrait ;
19+ use OpenFeature \interfaces \provider \ ResolutionDetails ;
20+ use Throwable ;
1121
1222use function array_count_values ;
1323use function array_diff ;
1626use function array_map ;
1727use function count ;
1828use function implode ;
29+ use function is_array ;
30+ use function is_bool ;
31+ use function is_float ;
32+ use function is_int ;
33+ use function is_string ;
1934use function strtolower ;
2035use function trim ;
2136
22- class Multiprovider
37+ class Multiprovider extends AbstractProvider
2338{
24- use LoggerAwareTrait ;
39+ protected static string $ NAME = ' Multiprovider ' ;
2540
2641 /**
2742 * List of supported keys in each provider data entry.
@@ -39,16 +54,237 @@ class Multiprovider
3954 */
4055 protected array $ providersByName = [];
4156
57+ /**
58+ * The evaluation strategy to use for flag resolution.
59+ */
60+ protected BaseEvaluationStrategy $ strategy ;
61+
4262 /**
4363 * Multiprovider constructor.
4464 *
4565 * @param array<int, array{name?: string, provider: Provider}> $providerData Array of provider data entries.
46- * @param StrategyInterface |null $strategy Optional strategy instance.
66+ * @param BaseEvaluationStrategy |null $strategy Optional strategy instance.
4767 */
48- public function __construct (array $ providerData = [], protected ? StrategyInterface $ strategy = null )
68+ public function __construct (array $ providerData = [], ? BaseEvaluationStrategy $ strategy = null )
4969 {
5070 $ this ->validateProviderData ($ providerData );
5171 $ this ->registerProviders ($ providerData );
72+
73+ $ this ->strategy = $ strategy ?? new FirstMatchStrategy ();
74+ }
75+
76+ /**
77+ * Resolves the flag value for the provided flag key as a boolean
78+ *
79+ * @param string $flagKey The flag key to resolve
80+ * @param bool $defaultValue The default value to return if no provider resolves the flag
81+ * @param EvaluationContext|null $context The evaluation context
82+ *
83+ * @return ResolutionDetails The resolution details
84+ */
85+ public function resolveBooleanValue (string $ flagKey , bool $ defaultValue , ?EvaluationContext $ context = null ): ResolutionDetails
86+ {
87+ return $ this ->evaluateFlag ('boolean ' , $ flagKey , $ defaultValue , $ context );
88+ }
89+
90+ /**
91+ * Resolves the flag value for the provided flag key as a string
92+ * * @param string $flagKey The flag key to resolve
93+ *
94+ * @param string $defaultValue The default value to return if no provider resolves the flag
95+ * @param EvaluationContext|null $context The evaluation context
96+ *
97+ * @return ResolutionDetails The resolution details
98+ */
99+ public function resolveStringValue (string $ flagKey , string $ defaultValue , ?EvaluationContext $ context = null ): ResolutionDetails
100+ {
101+ return $ this ->evaluateFlag ('string ' , $ flagKey , $ defaultValue , $ context );
102+ }
103+
104+ /**
105+ * Resolves the flag value for the provided flag key as an integer
106+ * * @param string $flagKey The flag key to resolve
107+ *
108+ * @param int $defaultValue The default value to return if no provider resolves the flag
109+ * @param EvaluationContext|null $context The evaluation context
110+ *
111+ * @return ResolutionDetails The resolution details
112+ */
113+ public function resolveIntegerValue (string $ flagKey , int $ defaultValue , ?EvaluationContext $ context = null ): ResolutionDetails
114+ {
115+ return $ this ->evaluateFlag ('integer ' , $ flagKey , $ defaultValue , $ context );
116+ }
117+
118+ /**
119+ * Resolves the flag value for the provided flag key as a float
120+ * * @param string $flagKey The flag key to resolve
121+ *
122+ * @param float $defaultValue The default value to return if no provider resolves the flag
123+ * @param EvaluationContext|null $context The evaluation context
124+ *
125+ * @return ResolutionDetails The resolution details
126+ */
127+ public function resolveFloatValue (string $ flagKey , float $ defaultValue , ?EvaluationContext $ context = null ): ResolutionDetails
128+ {
129+ return $ this ->evaluateFlag ('float ' , $ flagKey , $ defaultValue , $ context );
130+ }
131+
132+ /**
133+ * Resolves the flag value for the provided flag key as an object
134+ *
135+ * @param string $flagKey The flag key to resolve
136+ * @param EvaluationContext|null $context The evaluation context
137+ * @param mixed[] $defaultValue
138+ *
139+ * @return ResolutionDetails The resolution details
140+ */
141+ public function resolveObjectValue (string $ flagKey , array $ defaultValue , ?EvaluationContext $ context = null ): ResolutionDetails
142+ {
143+ return $ this ->evaluateFlag ('object ' , $ flagKey , $ defaultValue , $ context );
144+ }
145+
146+ /**
147+ * Core evaluation logic that works with the strategy to resolve flags across multiple providers.
148+ */
149+ private function evaluateFlag (string $ flagType , string $ flagKey , mixed $ defaultValue , ?EvaluationContext $ context ): ResolutionDetails
150+ {
151+ $ context = $ context ?? new \OpenFeature \implementation \flags \EvaluationContext ();
152+
153+ // Create base evaluation context
154+ $ baseContext = new StrategyEvaluationContext ($ flagKey , $ flagType , $ defaultValue , $ context );
155+
156+ // Collect results from providers based on strategy
157+ if ($ this ->strategy ->runMode === 'parallel ' ) {
158+ $ resolutions = $ this ->evaluateParallel ($ baseContext );
159+ } else {
160+ $ resolutions = $ this ->evaluateSequential ($ baseContext );
161+ }
162+
163+ // Let strategy determine final result
164+ $ finalResult = $ this ->strategy ->determineFinalResult ($ baseContext , $ resolutions );
165+
166+ if ($ finalResult ->isSuccessful ()) {
167+ $ details = $ finalResult ->getDetails ();
168+ if ($ details instanceof ResolutionDetails) {
169+ return $ details ;
170+ }
171+ }
172+
173+ // Handle error case
174+ return $ this ->createErrorResolution ($ flagKey , $ defaultValue , $ finalResult ->getErrors ());
175+ }
176+
177+ /**
178+ * Evaluate providers sequentially based on strategy decisions.
179+ *
180+ * @return array<int, ProviderResolutionResult> Array of resolution results from evaluated providers.
181+ */
182+ private function evaluateSequential (StrategyEvaluationContext $ baseContext ): array
183+ {
184+ $ resolutions = [];
185+
186+ foreach ($ this ->providersByName as $ providerName => $ provider ) {
187+ $ perProviderContext = new StrategyPerProviderContext ($ baseContext , $ providerName , $ provider );
188+
189+ // Check if we should evaluate this provider
190+ if (!$ this ->strategy ->shouldEvaluateThisProvider ($ perProviderContext )) {
191+ continue ;
192+ }
193+
194+ // Evaluate provider
195+ $ result = $ this ->evaluateProvider ($ provider , $ providerName , $ baseContext );
196+ $ resolutions [] = $ result ;
197+
198+ // Check if we should continue to next provider
199+ if (!$ this ->strategy ->shouldEvaluateNextProvider ($ perProviderContext , $ result )) {
200+ break ;
201+ }
202+ }
203+
204+ return $ resolutions ;
205+ }
206+
207+ /**
208+ * Evaluate all providers in parallel (all that pass shouldEvaluateThisProvider).
209+ *
210+ * @return array<int, ProviderResolutionResult> Array of resolution results from evaluated providers.
211+ */
212+ private function evaluateParallel (StrategyEvaluationContext $ baseContext ): array
213+ {
214+ $ resolutions = [];
215+
216+ foreach ($ this ->providersByName as $ providerName => $ provider ) {
217+ $ perProviderContext = new StrategyPerProviderContext ($ baseContext , $ providerName , $ provider );
218+
219+ // Check if we should evaluate this provider
220+ if (!$ this ->strategy ->shouldEvaluateThisProvider ($ perProviderContext )) {
221+ continue ;
222+ }
223+
224+ // Evaluate provider
225+ $ result = $ this ->evaluateProvider ($ provider , $ providerName , $ baseContext );
226+ $ resolutions [] = $ result ;
227+ }
228+
229+ return $ resolutions ;
230+ }
231+
232+ /**
233+ * Evaluate a single provider and return result with error handling.
234+ */
235+ private function evaluateProvider (Provider $ provider , string $ providerName , StrategyEvaluationContext $ context ): ProviderResolutionResult
236+ {
237+ try {
238+ $ flagType = $ context ->getFlagType ();
239+ /** @var bool|string|int|float|array<mixed>|null $defaultValue */
240+ $ defaultValue = $ context ->getDefaultValue ();
241+ $ evalContext = $ context ->getEvaluationContext ();
242+
243+ $ details = match ($ flagType ) {
244+ 'boolean ' => is_bool ($ defaultValue )
245+ ? $ provider ->resolveBooleanValue ($ context ->getFlagKey (), $ defaultValue , $ evalContext )
246+ : throw new InvalidArgumentException ('Default value for boolean flag must be bool ' ),
247+ 'string ' => is_string ($ defaultValue )
248+ ? $ provider ->resolveStringValue ($ context ->getFlagKey (), $ defaultValue , $ evalContext )
249+ : throw new InvalidArgumentException ('Default value for string flag must be string ' ),
250+ 'integer ' => is_int ($ defaultValue )
251+ ? $ provider ->resolveIntegerValue ($ context ->getFlagKey (), $ defaultValue , $ evalContext )
252+ : throw new InvalidArgumentException ('Default value for integer flag must be int ' ),
253+ 'float ' => is_float ($ defaultValue )
254+ ? $ provider ->resolveFloatValue ($ context ->getFlagKey (), $ defaultValue , $ evalContext )
255+ : throw new InvalidArgumentException ('Default value for float flag must be float ' ),
256+ 'object ' => is_array ($ defaultValue )
257+ ? $ provider ->resolveObjectValue ($ context ->getFlagKey (), $ defaultValue , $ evalContext )
258+ : throw new InvalidArgumentException ('Default value for object flag must be array ' ),
259+ default => throw new InvalidArgumentException ('Unknown flag type: ' . $ flagType ),
260+ };
261+
262+ return new ProviderResolutionResult ($ providerName , $ provider , $ details , null );
263+ } catch (Throwable $ error ) {
264+ return new ProviderResolutionResult ($ providerName , $ provider , null , $ error );
265+ }
266+ }
267+
268+ /**
269+ * Create an error resolution with aggregated errors from multiple providers.
270+ *
271+ * @param string $flagKey The flag key being evaluated.
272+ * @param mixed $defaultValue The default value to return.
273+ * @param array<int, array{providerName: string, error: Throwable}>|null $errors Array of errors encountered during evaluation.
274+ */
275+ private function createErrorResolution (string $ flagKey , mixed $ defaultValue , ?array $ errors ): ResolutionDetails
276+ {
277+ $ errorMessage = 'Multi-provider evaluation failed ' ;
278+ $ errorCode = ErrorCode::GENERAL ();
279+
280+ if ($ errors !== null && count ($ errors ) > 0 ) {
281+ $ errorMessage .= ' with ' . count ($ errors ) . ' provider error(s) ' ;
282+ }
283+
284+ return (new ResolutionDetailsBuilder ())
285+ ->withReason (Reason::ERROR )
286+ ->withError (new ResolutionError ($ errorCode , $ errorMessage ))
287+ ->build ();
52288 }
53289
54290 /**
0 commit comments