├── .gitmodules
├── .pre-commit-config.yaml
├── .release-please-manifest.json
├── .tool-versions
├── CODEOWNERS
├── LICENSE
├── README.md
├── composer.json
├── devenv.lock
├── devenv.nix
├── devenv.yaml
├── integration
├── .gitignore
├── composer.json
└── features
│ └── bootstrap
│ └── FeatureContext.php
├── release-please-config.json
├── renovate.json
└── src
├── OpenFeatureAPI.php
├── OpenFeatureClient.php
├── implementation
├── .gitkeep
├── common
│ ├── ArrayHelper.php
│ ├── Metadata.php
│ ├── StringHelper.php
│ └── ValueTypeValidator.php
├── errors
│ ├── FlagValueTypeError.php
│ └── InvalidResolutionValueError.php
├── flags
│ ├── Attributes.php
│ ├── AttributesMerger.php
│ ├── EvaluationContext.php
│ ├── EvaluationContextMerger.php
│ ├── EvaluationDetails.php
│ ├── EvaluationDetailsBuilder.php
│ ├── EvaluationDetailsFactory.php
│ ├── EvaluationOptions.php
│ ├── MutableAttributes.php
│ ├── MutableEvaluationContext.php
│ └── NoOpClient.php
├── hooks
│ ├── AbstractHook.php
│ ├── AbstractHookContext.php
│ ├── BooleanHook.php
│ ├── FloatHook.php
│ ├── HookContextBuilder.php
│ ├── HookContextFactory.php
│ ├── HookContextTransformer.php
│ ├── HookExecutor.php
│ ├── HookHints.php
│ ├── ImmutableHookContext.php
│ ├── IntegerHook.php
│ ├── MutableHookContext.php
│ ├── ObjectHook.php
│ └── StringHook.php
└── provider
│ ├── AbstractProvider.php
│ ├── NoOpProvider.php
│ ├── Reason.php
│ ├── ResolutionDetails.php
│ ├── ResolutionDetailsBuilder.php
│ ├── ResolutionDetailsFactory.php
│ └── ResolutionError.php
└── interfaces
├── common
├── LoggerAwareTrait.php
├── Metadata.php
├── MetadataGetter.php
├── StringIndexed.php
└── TypeValuePair.php
├── flags
├── API.php
├── AttributeByTypeExporter.php
├── AttributeType.php
├── Attributes.php
├── Client.php
├── EvaluationContext.php
├── EvaluationContextAware.php
├── EvaluationDetails.php
├── EvaluationOptions.php
├── FeatureDetails.php
├── FeatureValues.php
├── FlagValueType.php
├── MutableAttributes.php
└── MutableEvaluationContext.php
├── hooks
├── Hook.php
├── HookContext.php
├── HookHints.php
├── HooksAdder.php
├── HooksAware.php
├── HooksAwareTrait.php
├── HooksGetter.php
├── HooksSetter.php
└── MutableHookContext.php
└── provider
├── ErrorCode.php
├── Provider.php
├── ProviderAware.php
├── Reason.php
├── ResolutionDetails.php
├── ResolutionError.php
└── ThrowableWithResolutionError.php
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "test-harness"]
2 | path = integration/test-harness
3 | url = https://github.com/open-feature/test-harness.git
4 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | /nix/store/6v3blbcxh5aqc5k2qzf5cqyxnp8yinlp-pre-commit-config.json
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "2.0.10"
3 | }
4 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | php 8.2.12
2 | # php 8.1.11
3 | # php 8.0.24
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Welcome to the CODEOWNERS file
2 | # This file dictates code owners who will be required for
3 | # reviews in the project
4 |
5 | # These owners will be the default owners for everything in
6 | # the repo. Unless a later match takes precedence,
7 | # @global-owner1 and @global-owner2 will be requested for
8 | # review when someone opens a pull request.
9 | * @tcarrio
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | OpenFeature PHP SDK
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | [OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool.
48 |
49 |
50 | ## 🚀 Quick start
51 |
52 | ### Requirements
53 |
54 | This library targets PHP version 8.0 and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK.
55 |
56 | This package also has a `.tool-versions` file for use with PHP version managers like `asdf`.
57 |
58 | ### Install
59 |
60 | ```shell
61 | composer require open-feature/sdk
62 | ```
63 |
64 | ### Usage
65 |
66 | ```php
67 | use OpenFeature\OpenFeatureAPI;
68 | use OpenFeature\Providers\Flagd\FlagdProvider;
69 |
70 | function example()
71 | {
72 | $api = OpenFeatureAPI::getInstance();
73 |
74 | // configure a provider
75 | $api->setProvider(new FlagdProvider());
76 |
77 | // create a client
78 | $client = $api->getClient();
79 |
80 | // get a bool flag value
81 | $client->getBooleanValue('v2_enabled', false);
82 | }
83 | ```
84 |
85 | #### Extended Example
86 |
87 | ```php
88 | use OpenFeature\OpenFeatureClient;
89 |
90 | class MyClass
91 | {
92 | private OpenFeatureClient $client;
93 |
94 | public function __construct()
95 | {
96 | $this->client = OpenFeatureAPI::getInstance()->getClient('MyClass');
97 | }
98 |
99 | public function booleanExample(): UI
100 | {
101 | // Should we render the redesign? Or the default webpage?
102 | if ($this->client->getBooleanValue('redesign_enabled', false)) {
103 | return render_redesign();
104 | }
105 | return render_normal();
106 | }
107 |
108 | public function stringExample(): Template
109 | {
110 | // Get the template to load for the custom new homepage
111 | $template = $this->client->getStringValue('homepage_template', 'default-homepage.html');
112 |
113 | return render_template($template);
114 | }
115 |
116 | public function numberExample(): array
117 | {
118 | // How many modules should we be fetching?
119 | $count = $this->client->getIntegerValue('module-fetch-count', 4);
120 |
121 | return fetch_modules($count);
122 | }
123 |
124 | public function structureExample(): HomepageModule
125 | {
126 | $obj = $this->client->getObjectValue('hero-module', $previouslyDefinedDefaultStructure);
127 |
128 | return HomepageModuleBuilder::new()
129 | ->title($obj->getValue('title'))
130 | ->body($obj->getValue('description'))
131 | ->build();
132 | }
133 | }
134 | ```
135 |
136 | ## 🌟 Features
137 |
138 | | Status | Features | Description |
139 | | ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
140 | | ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
141 | | ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
142 | | ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
143 | | ✅ | [Logging](#logging) | Integrate with popular logging packages. |
144 | | ❌ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
145 | | ⚠️ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
146 | | ❌ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
147 | | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
148 |
149 | Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌
150 |
151 | ### Providers
152 |
153 | [Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK.
154 | Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=PHP) for a complete list of available providers.
155 | If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself.
156 |
157 | Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
158 |
159 | ```php
160 | $api = OpenFeatureAPI::getInstance();
161 | $api->setProvider(new MyProvider());
162 | ```
163 |
164 |
166 |
167 | ### Targeting
168 |
169 | Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location.
170 | In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
171 | If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
172 |
173 | ```php
174 | // add a value to the global context
175 | $api = OpenFeatureAPI::getInstance();
176 | $api->setEvaluationContext(new EvaluationContext('targetingKey', ['myGlobalKey' => 'myGlobalValue']));
177 |
178 | // add a value to the client context
179 | $client = $api->getClient();
180 | $client->setEvaluationContext(new EvaluationContext('targetingKey', ['myClientKey' => 'myClientValue']));
181 |
182 | // add a value to the invocation context
183 | $context = new EvaluationContext('targetingKey', ['myInvocationKey' => 'myInvocationValue']);
184 |
185 | $boolValue = $client->getBooleanValue('boolFlag', false, $context);
186 | ```
187 |
188 | ### Hooks
189 |
190 | [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
191 | Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=php) for a complete list of available hooks.
192 | If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
193 |
194 | Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
195 |
196 | ```php
197 | // add a hook globally, to run on all evaluations
198 | $api = OpenFeatureAPI::getInstance();
199 | $api->addHook(new ExampleGlobalHook());
200 |
201 | // add a hook on this client, to run on all evaluations made by this client
202 | $client = $api->getClient();
203 | $client->addHook(new ExampleClientHook());
204 |
205 | // add a hook for this evaluation only
206 | $value = $client->getBooleanValue("boolFlag", false, $context, new EvaluationOptions([new ExampleInvocationHook()]));
207 | ```
208 |
209 | ### Logging
210 |
211 | The PHP SDK utilizes several of the PHP Standards Recommendation, one of those being [PSR-3](https://www.php-fig.org/psr/psr-3/) which provides a standard `LoggerInterface`.
212 | The SDK makes use of a `LoggerAwareTrait` on several components, including the client for flag evaluation, the hook executor, and the global `OpenFeatureAPI` instance.
213 | When an OpenFeature client is created by the API, it will automatically utilize the configured logger in the API for it. The logger set in the client is also automatically used for the hook execution.
214 |
215 | > ⚠️ Once the client is instantiated, updates to the API's logger will not synchronize. This is done to support the separation of named clients. If you must update an existing client's logger, do so directly!
216 |
217 | ```php
218 | $api = OpenFeatureAPI::getInstance();
219 |
220 | $logger = new FancyLogger();
221 |
222 | $defaultLoggerClient = $api->getClient('default-logger');
223 |
224 | $api->setLogger(new CustomLogger());
225 |
226 | $customLoggerClient = $api->getClient('custom-logger');
227 |
228 | $overrideLoggerClient = $api->getClient('override-logger');
229 | $overrideLoggerClient->setLogger($logger);
230 |
231 | // now let's do some evaluations with these!
232 |
233 | $defaultLoggerClient->getBooleanValue('A', false);
234 | // uses default logger in the SDK
235 |
236 | $customLoggerClient->getBooleanValue('B', false);
237 | // uses the CustomLogger set in the API before the client was made
238 |
239 | $overrideLoggerClient->getBooleanValue('C', false);
240 | // uses the FancyLogger set directly on the client
241 | ```
242 |
243 | ### Named clients
244 |
245 | Named clients are not yet available in the PHP SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/php-sdk/issues/93).
246 |
247 | ### Eventing
248 |
249 | Events are not yet available in the PHP SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/php-sdk/issues/93).
250 |
251 | ### Shutdown
252 |
253 | A shutdown method is not yet available in the PHP SDK. Progress on this feature can be tracked [here](https://github.com/open-feature/php-sdk/issues/93).
254 |
255 | ## Extending
256 |
257 | ### Develop a provider
258 |
259 | To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
260 | This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/php-sdk-contrib) available under the OpenFeature organization.
261 | You’ll then need to write the provider by implementing the `Provider` interface exported by the OpenFeature SDK.
262 |
263 | ```php
264 | declare(strict_types=1);
265 |
266 | namespace OpenFeature\Example\Providers;
267 |
268 | use OpenFeature\implementation\common\Metadata;
269 | use OpenFeature\interfaces\common\Metadata as IMetadata;
270 | use OpenFeature\interfaces\flags\EvaluationContext;
271 | use OpenFeature\interfaces\hooks\Hook;
272 | use OpenFeature\interfaces\provider\Provider;
273 | use OpenFeature\interfaces\provider\ResolutionDetails;
274 |
275 | class ExampleProviderImplementation implements Provider
276 | {
277 | public function setLogger(LoggerInterface $logger): void
278 | {
279 | $this->logger = $logger;
280 |
281 | // or, utilize the OpenFeature\interfaces\common\LoggerAwareTrait
282 | }
283 |
284 | /**
285 | * @return Hook[]
286 | */
287 | public function getHooks(): array
288 | {
289 | return $this->hooks; // implement according to the OpenFeature specification
290 | }
291 |
292 | /**
293 | * Returns the metadata for the current resource
294 | */
295 | public function getMetadata(): IMetadata
296 | {
297 | return new Metadata(self::class);
298 | }
299 |
300 | public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
301 | {
302 | // resolve some ResolutionDetails
303 | }
304 |
305 | public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
306 | {
307 | // resolve some ResolutionDetails
308 | }
309 |
310 | public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
311 | {
312 | // resolve some ResolutionDetails
313 | }
314 |
315 | public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
316 | {
317 | // resolve some ResolutionDetails
318 | }
319 |
320 | /**
321 | * @inheritdoc
322 | */
323 | public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails
324 | {
325 | // resolve some ResolutionDetails
326 | }
327 | }
328 | ```
329 |
330 | As you can see, this ends up requiring some boilerplate to fulfill all of the functionality that a Provider expects.
331 | Another option for implementing a provider is to utilize the AbstractProvider base class.
332 | This provides some internally wiring and simple scaffolding so you can skip some of it and focus on what's most important: resolving feature flags!
333 |
334 | ```php
335 | declare(strict_types=1);
336 |
337 | namespace OpenFeature\Example\Providers;
338 |
339 | use OpenFeature\interfaces\flags\EvaluationContext;
340 | use OpenFeature\interfaces\provider\ResolutionDetails;
341 |
342 | class ExampleProviderExtension extends AbstractProvider
343 | {
344 | protected static string $NAME = self::class;
345 |
346 | public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetailsInterface
347 | {
348 | // resolve some ResolutionDetails
349 | }
350 |
351 | public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetailsInterface
352 | {
353 | // resolve some ResolutionDetails
354 | }
355 |
356 | public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetailsInterface
357 | {
358 | // resolve some ResolutionDetails
359 | }
360 |
361 | public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetailsInterface
362 | {
363 | // resolve some ResolutionDetails
364 | }
365 |
366 | /**
367 | * @inheritdoc
368 | */
369 | public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetailsInterface
370 | {
371 | // resolve some ResolutionDetails
372 | }
373 | }
374 | ```
375 |
376 | > Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs!
377 |
378 | ### Develop a hook
379 |
380 | To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
381 | This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/php-sdk-contrib) available under the OpenFeature organization.
382 | Implement your own hook by conforming to the `Hook interface`.
383 | To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined.
384 | To avoid defining empty functions, make use of the `UnimplementedHook` struct (which already implements all the empty functions).
385 |
386 | ```php
387 | declare(strict_types=1);
388 |
389 | namespace OpenFeature\Example\Hooks;
390 |
391 | use OpenFeature\interfaces\flags\EvaluationContext;
392 | use OpenFeature\interfaces\flags\FlagValueType;
393 | use OpenFeature\interfaces\hooks\Hook;
394 | use OpenFeature\interfaces\hooks\HookContext;
395 | use OpenFeature\interfaces\hooks\HookHints;
396 | use OpenFeature\interfaces\provider\ResolutionDetails;
397 |
398 |
399 | class ExampleStringHookImplementation implements Hook
400 | {
401 | public function before(HookContext $context, HookHints $hints): ?EvaluationContext
402 | {
403 | }
404 |
405 | public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void
406 | {
407 | }
408 |
409 | public function error(HookContext $context, Throwable $error, HookHints $hints): void
410 | {
411 | }
412 |
413 | public function finally(HookContext $context, HookHints $hints): void
414 | {
415 | }
416 |
417 |
418 | public function supportsFlagValueType(string $flagValueType): bool
419 | {
420 | return $flagValueType === FlagValueType::STRING;
421 | }
422 | }
423 | ```
424 |
425 | You can also make use of existing base classes for various types and behaviors.
426 | Suppose you want to make this same hook, and have no limitation around extending a base class, you could do the following:
427 |
428 | ```php
429 | declare(strict_types=1);
430 |
431 | namespace OpenFeature\Example\Hooks;
432 |
433 | use OpenFeature\implementation\hooks\StringHook;
434 | use OpenFeature\interfaces\flags\EvaluationContext;
435 | use OpenFeature\interfaces\flags\FlagValueType;
436 | use OpenFeature\interfaces\hooks\HookContext;
437 | use OpenFeature\interfaces\hooks\HookHints;
438 | use OpenFeature\interfaces\provider\ResolutionDetails;
439 |
440 |
441 | class ExampleStringHookExtension extends StringHook
442 | {
443 | public function before(HookContext $context, HookHints $hints): ?EvaluationContext
444 | {
445 | }
446 |
447 | public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void
448 | {
449 | }
450 |
451 | public function error(HookContext $context, Throwable $error, HookHints $hints): void
452 | {
453 | }
454 |
455 | public function finally(HookContext $context, HookHints $hints): void
456 | {
457 | }
458 | }
459 | ```
460 |
461 | > Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
462 |
463 |
464 | ## ⭐️ Support the project
465 |
466 | - Give this repo a ⭐️!
467 | - Follow us on social media:
468 | - Twitter: [@openfeature](https://twitter.com/openfeature)
469 | - LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/)
470 | - Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1)
471 | - For more, check out our [community page](https://openfeature.dev/community/)
472 |
473 | ## 🤝 Contributing
474 |
475 | Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide.
476 |
477 | ### Thanks to everyone who has already contributed
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 | Made with [contrib.rocks](https://contrib.rocks).
486 |
487 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "open-feature/sdk",
3 | "description": "PHP implementation of the OpenFeature SDK",
4 | "license": "Apache-2.0",
5 | "type": "library",
6 | "keywords": [
7 | "featureflags",
8 | "featureflagging",
9 | "openfeature"
10 | ],
11 | "authors": [
12 | {
13 | "name": "Tom Carrio",
14 | "email": "tom@carrio.dev"
15 | }
16 | ],
17 | "require": {
18 | "php": "^8",
19 | "myclabs/php-enum": "^1.8",
20 | "psr/log": "^2.0 || ^3.0"
21 | },
22 | "require-dev": {
23 | "behat/behat": "^3.11",
24 | "captainhook/captainhook": "^5.10",
25 | "captainhook/plugin-composer": "^5.3",
26 | "ergebnis/composer-normalize": "^2.25",
27 | "hamcrest/hamcrest-php": "^2.0",
28 | "mdwheele/zalgo": "^0.3.1",
29 | "mockery/mockery": "^1.5",
30 | "php-parallel-lint/php-console-highlighter": "^1.0",
31 | "php-parallel-lint/php-parallel-lint": "^1.3",
32 | "phpstan/extension-installer": "^1.1",
33 | "phpstan/phpstan": "~1.12.0",
34 | "phpstan/phpstan-mockery": "^1.0",
35 | "phpstan/phpstan-phpunit": "^1.1",
36 | "psalm/plugin-mockery": "^1.0.0",
37 | "psalm/plugin-phpunit": "^0.19.0",
38 | "ramsey/coding-standard": "^2.0.3",
39 | "ramsey/composer-repl": "^1.4",
40 | "ramsey/conventional-commits": "^1.3",
41 | "roave/security-advisories": "dev-latest",
42 | "spatie/phpunit-snapshot-assertions": "^4.2",
43 | "vimeo/psalm": "~5.26.0"
44 | },
45 | "minimum-stability": "dev",
46 | "prefer-stable": true,
47 | "autoload": {
48 | "psr-4": {
49 | "OpenFeature\\": "src/"
50 | }
51 | },
52 | "autoload-dev": {
53 | "psr-4": {
54 | "OpenFeature\\Test\\": "tests/"
55 | }
56 | },
57 | "config": {
58 | "allow-plugins": {
59 | "phpstan/extension-installer": true,
60 | "dealerdirect/phpcodesniffer-composer-installer": true,
61 | "ergebnis/composer-normalize": true,
62 | "captainhook/plugin-composer": true,
63 | "ramsey/composer-repl": true
64 | },
65 | "sort-packages": true
66 | },
67 | "extra": {
68 | "captainhook": {
69 | "force-install": true
70 | },
71 | "ramsey/conventional-commits": {
72 | "configFile": "conventional-commits.json"
73 | }
74 | },
75 | "scripts": {
76 | "dev:analyze": [
77 | "@dev:analyze:phpstan",
78 | "@dev:analyze:psalm"
79 | ],
80 | "dev:analyze:phpstan": "phpstan analyse --ansi --debug --memory-limit=512M",
81 | "dev:analyze:psalm": "psalm",
82 | "dev:build:clean": "git clean -fX build/",
83 | "dev:lint": [
84 | "@dev:lint:syntax",
85 | "@dev:lint:style"
86 | ],
87 | "dev:lint:fix": "phpcbf",
88 | "dev:lint:style": "phpcs --colors",
89 | "dev:lint:syntax": "parallel-lint --colors src/ tests/",
90 | "dev:test": [
91 | "@dev:lint",
92 | "@dev:analyze",
93 | "@dev:test:unit"
94 | ],
95 | "dev:test:coverage:ci": "phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml",
96 | "dev:test:coverage:html": "phpunit --colors=always --coverage-html build/coverage/coverage-html/",
97 | "dev:test:unit": "phpunit --colors=always --testdox",
98 | "dev:test:unit:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on",
99 | "test": "@dev:test"
100 | },
101 | "scripts-descriptions": {
102 | "dev:analyze": "Runs all static analysis checks.",
103 | "dev:analyze:phpstan": "Runs the PHPStan static analyzer.",
104 | "dev:analyze:psalm": "Runs the Psalm static analyzer.",
105 | "dev:build:clean": "Cleans the build/ directory.",
106 | "dev:lint": "Runs all linting checks.",
107 | "dev:lint:fix": "Auto-fixes coding standards issues, if possible.",
108 | "dev:lint:style": "Checks for coding standards issues.",
109 | "dev:lint:syntax": "Checks for syntax errors.",
110 | "dev:test": "Runs linting, static analysis, and unit tests.",
111 | "dev:test:coverage:ci": "Runs unit tests and generates CI coverage reports.",
112 | "dev:test:coverage:html": "Runs unit tests and generates HTML coverage report.",
113 | "dev:test:unit": "Runs unit tests.",
114 | "test": "Runs linting, static analysis, and unit tests."
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/devenv.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "devenv": {
4 | "locked": {
5 | "dir": "src/modules",
6 | "lastModified": 1675875772,
7 | "narHash": "sha256-sYXHPZ4tsjdG+UXK0mYnABhiS/RuzHiV9uGOU9YakwE=",
8 | "owner": "cachix",
9 | "repo": "devenv",
10 | "rev": "eac5eb12eb42765f5f252972dc876d1f96b03dfe",
11 | "type": "github"
12 | },
13 | "original": {
14 | "dir": "src/modules",
15 | "owner": "cachix",
16 | "repo": "devenv",
17 | "type": "github"
18 | }
19 | },
20 | "flake-compat": {
21 | "flake": false,
22 | "locked": {
23 | "lastModified": 1673956053,
24 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
25 | "owner": "edolstra",
26 | "repo": "flake-compat",
27 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "edolstra",
32 | "repo": "flake-compat",
33 | "type": "github"
34 | }
35 | },
36 | "flake-utils": {
37 | "locked": {
38 | "lastModified": 1667395993,
39 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
40 | "owner": "numtide",
41 | "repo": "flake-utils",
42 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
43 | "type": "github"
44 | },
45 | "original": {
46 | "owner": "numtide",
47 | "repo": "flake-utils",
48 | "type": "github"
49 | }
50 | },
51 | "gitignore": {
52 | "inputs": {
53 | "nixpkgs": [
54 | "pre-commit-hooks",
55 | "nixpkgs"
56 | ]
57 | },
58 | "locked": {
59 | "lastModified": 1660459072,
60 | "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
61 | "owner": "hercules-ci",
62 | "repo": "gitignore.nix",
63 | "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
64 | "type": "github"
65 | },
66 | "original": {
67 | "owner": "hercules-ci",
68 | "repo": "gitignore.nix",
69 | "type": "github"
70 | }
71 | },
72 | "nixpkgs": {
73 | "locked": {
74 | "lastModified": 1675758091,
75 | "narHash": "sha256-7gFSQbSVAFUHtGCNHPF7mPc5CcqDk9M2+inlVPZSneg=",
76 | "owner": "NixOS",
77 | "repo": "nixpkgs",
78 | "rev": "747927516efcb5e31ba03b7ff32f61f6d47e7d87",
79 | "type": "github"
80 | },
81 | "original": {
82 | "owner": "NixOS",
83 | "ref": "nixpkgs-unstable",
84 | "repo": "nixpkgs",
85 | "type": "github"
86 | }
87 | },
88 | "nixpkgs-stable": {
89 | "locked": {
90 | "lastModified": 1673800717,
91 | "narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=",
92 | "owner": "NixOS",
93 | "repo": "nixpkgs",
94 | "rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f",
95 | "type": "github"
96 | },
97 | "original": {
98 | "owner": "NixOS",
99 | "ref": "nixos-22.11",
100 | "repo": "nixpkgs",
101 | "type": "github"
102 | }
103 | },
104 | "nixpkgs_2": {
105 | "locked": {
106 | "lastModified": 1671271357,
107 | "narHash": "sha256-xRJdLbWK4v2SewmSStYrcLa0YGJpleufl44A19XSW8k=",
108 | "owner": "NixOS",
109 | "repo": "nixpkgs",
110 | "rev": "40f79f003b6377bd2f4ed4027dde1f8f922995dd",
111 | "type": "github"
112 | },
113 | "original": {
114 | "owner": "NixOS",
115 | "ref": "nixos-unstable",
116 | "repo": "nixpkgs",
117 | "type": "github"
118 | }
119 | },
120 | "pre-commit-hooks": {
121 | "inputs": {
122 | "flake-compat": "flake-compat",
123 | "flake-utils": "flake-utils",
124 | "gitignore": "gitignore",
125 | "nixpkgs": "nixpkgs_2",
126 | "nixpkgs-stable": "nixpkgs-stable"
127 | },
128 | "locked": {
129 | "lastModified": 1675688762,
130 | "narHash": "sha256-oit/SxMk0B380ASuztBGQLe8TttO1GJiXF8aZY9AYEc=",
131 | "owner": "cachix",
132 | "repo": "pre-commit-hooks.nix",
133 | "rev": "ab608394886fb04b8a5df3cb0bab2598400e3634",
134 | "type": "github"
135 | },
136 | "original": {
137 | "owner": "cachix",
138 | "repo": "pre-commit-hooks.nix",
139 | "type": "github"
140 | }
141 | },
142 | "root": {
143 | "inputs": {
144 | "devenv": "devenv",
145 | "nixpkgs": "nixpkgs",
146 | "pre-commit-hooks": "pre-commit-hooks"
147 | }
148 | }
149 | },
150 | "root": "root",
151 | "version": 7
152 | }
153 |
--------------------------------------------------------------------------------
/devenv.nix:
--------------------------------------------------------------------------------
1 | { pkgs, ... }:
2 |
3 | {
4 |
5 | # https://devenv.sh/packages/
6 | packages = [ pkgs.git ];
7 |
8 | # https://devenv.sh/languages/
9 | languages.nix.enable = true;
10 | languages.php.enable = true;
11 | languages.php.package = pkgs.php80;
12 |
13 | # https://devenv.sh/basics/
14 | env.PROJECT_NAME = "openfeature-php-sdk";
15 |
16 | # https://devenv.sh/scripts/
17 | scripts.hello.exec = "echo $ Started devenv shell in $PROJECT_NAME";
18 |
19 | enterShell = ''
20 | hello
21 | echo
22 | git --version
23 | php --version
24 | echo
25 |
26 | # optimization step -- files and directories that match entries
27 | # in the .gitignore will still be traversed, and the .devenv
28 | # directory contains over 5000 files and 121MB.
29 | if ! grep -E "excludesfile.+\.gitignore" .git/config &>/dev/null
30 | then
31 | git config --local core.excludesfile .gitignore
32 | fi
33 | '';
34 |
35 | ## https://devenv.sh/pre-commit-hooks/
36 | pre-commit.hooks = {
37 | # # general formatting
38 | # prettier.enable = true;
39 | # github actions
40 | actionlint.enable = true;
41 | # nix
42 | deadnix.enable = true;
43 | nixfmt.enable = true;
44 | # php
45 | phpcbf.enable = true;
46 | # # ensure Markdown code is executable
47 | # mdsh.enable = true;
48 | };
49 |
50 | # https://devenv.sh/processes/
51 | # processes.ping.exec = "ping example.com";
52 | }
53 |
--------------------------------------------------------------------------------
/devenv.yaml:
--------------------------------------------------------------------------------
1 | inputs:
2 | nixpkgs:
3 | url: github:NixOS/nixpkgs/nixpkgs-unstable
4 | pre-commit-hooks:
5 | url: github:cachix/pre-commit-hooks.nix
--------------------------------------------------------------------------------
/integration/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /composer.lock
3 | /features/*.feature
--------------------------------------------------------------------------------
/integration/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "open-feature/integration-tests",
3 | "description": "Integration tests for the OpenFeature SDK",
4 | "license": "Apache-2.0",
5 | "type": "library",
6 | "keywords": [
7 | "featureflags",
8 | "featureflagging",
9 | "openfeature"
10 | ],
11 | "authors": [
12 | {
13 | "name": "Tom Carrio",
14 | "email": "tom@carrio.dev"
15 | }
16 | ],
17 | "require": {
18 | "php": "^8",
19 | "open-feature/sdk": "^2.0.0",
20 | "open-feature/flagd-provider": "^0.7.0",
21 | "guzzlehttp/guzzle": "^7.5",
22 | "guzzlehttp/psr7": "^2.4"
23 | },
24 | "require-dev": {
25 | "behat/behat": "^3.11",
26 | "phpunit/phpunit": "^9.5"
27 | },
28 | "repositories": [
29 | {
30 | "type": "path",
31 | "url": "../",
32 | "options": {
33 | "versions": {
34 | "open-feature/sdk": "2.0.0"
35 | }
36 | }
37 | }
38 | ],
39 | "minimum-stability": "dev",
40 | "prefer-stable": true,
41 | "scripts": {
42 | "dev:test": [
43 | "@dev:test:features"
44 | ],
45 | "dev:test:features": [
46 | "@dev:test:features:init",
47 | "@dev:test:features:setup",
48 | "@dev:test:features:run"
49 | ],
50 | "dev:test:features:init": "git submodule update --recursive",
51 | "dev:test:features:run": "vendor/bin/behat",
52 | "dev:test:features:setup": "cp ./test-harness/features/evaluation.feature ./features/",
53 | "test": "@dev:test"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/integration/features/bootstrap/FeatureContext.php:
--------------------------------------------------------------------------------
1 | 'localhost',
56 | 'port' => 8013,
57 | 'protocol' => 'http',
58 | 'secure' => false,
59 | 'httpConfig' => new HttpConfig(
60 | $client,
61 | $httpFactory,
62 | $httpFactory,
63 | ),
64 | ],
65 | );
66 |
67 | $api->setProvider($provider);
68 |
69 | $this->client = $api->getClient('features', '1.0');
70 | }
71 |
72 | /**
73 | * @Given a provider is registered with cache disabled
74 | */
75 | public function aProviderIsRegisteredWithCacheDisabled()
76 | {
77 | // TODO: we disable cache by default already. Will update once we implement caching.
78 | }
79 |
80 | /**
81 | * @When a boolean flag with key :flagKey is evaluated with default value :defaultValue
82 | */
83 | public function aBooleanFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, bool $defaultValue)
84 | {
85 | $this->flagType = FlagValueType::BOOLEAN;
86 | $this->inputFlagKey = $flagKey;
87 | $this->inputFlagDefaultValue = $defaultValue;
88 | }
89 |
90 | /**
91 | * @Then the resolved boolean value should be :resolvedValue
92 | */
93 | public function theResolvedBooleanValueShouldBe(bool $resolvedValue)
94 | {
95 | Assert::assertEquals(
96 | $resolvedValue,
97 | $this->calculateValue(),
98 | );
99 | }
100 |
101 | /**
102 | * @When a string flag with key :flagKey is evaluated with default value :defaultValue
103 | */
104 | public function aStringFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, string $defaultValue)
105 | {
106 | $this->flagType = FlagValueType::STRING;
107 | $this->inputFlagKey = $flagKey;
108 | $this->inputFlagDefaultValue = $defaultValue;
109 | }
110 |
111 | /**
112 | * @Then the resolved string value should be :resolvedValue
113 | */
114 | public function theResolvedStringValueShouldBe(string $resolvedValue)
115 | {
116 | Assert::assertEquals(
117 | $resolvedValue,
118 | $this->calculateValue(),
119 | );
120 | }
121 |
122 | /**
123 | * @When an integer flag with key :flagKey is evaluated with default value :defaultValue
124 | */
125 | public function anIntegerFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, int $defaultValue)
126 | {
127 | $this->flagType = FlagValueType::INTEGER;
128 | $this->inputFlagKey = $flagKey;
129 | $this->inputFlagDefaultValue = $defaultValue;
130 | print_r("Setting integer...\n");
131 | }
132 |
133 | /**
134 | * @Then the resolved integer value should be :resolvedValue
135 | */
136 | public function theResolvedIntegerValueShouldBe(int $resolvedValue)
137 | {
138 | Assert::assertEquals(
139 | $resolvedValue,
140 | $this->calculateValue(),
141 | );
142 | }
143 |
144 | /**
145 | * @When a float flag with key :flagKey is evaluated with default value :defaultValue
146 | */
147 | public function aFloatFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, float $defaultValue)
148 | {
149 | $this->flagType = FlagValueType::FLOAT;
150 | $this->inputFlagKey = $flagKey;
151 | $this->inputFlagDefaultValue = $defaultValue;
152 | }
153 |
154 | /**
155 | * @Then the resolved float value should be :resolvedValue
156 | */
157 | public function theResolvedFloatValueShouldBe(float $resolvedValue)
158 | {
159 | Assert::assertEquals(
160 | $resolvedValue,
161 | $this->calculateValue(),
162 | );
163 | }
164 |
165 | /**
166 | * @When an object flag with key :flagKey is evaluated with a :defaultValue default value
167 | */
168 | public function anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue(string $flagKey, mixed $defaultValue)
169 | {
170 | $this->flagType = FlagValueType::OBJECT;
171 | $this->inputFlagKey = $flagKey;
172 | $this->inputFlagDefaultValue = $defaultValue;
173 | }
174 |
175 | /**
176 | * @Then the resolved object value should be contain fields :key1, :key2, and :key3, with values :value1, :value2 and :value3, respectively
177 | */
178 | public function theResolvedObjectValueShouldBeContainFieldsAndWithValuesAndRespectively(string $key1, string $key2, string $key3, mixed $value1, mixed $value2, mixed $value3)
179 | {
180 | Assert::assertEquals(
181 | [
182 | $key1 => $value1,
183 | $key2 => $value2,
184 | $key3 => $value3,
185 | ],
186 | $this->calculateValue(),
187 | );
188 | }
189 |
190 | /**
191 | * @When a boolean flag with key :flagKey is evaluated with details and default value :defaultValue
192 | */
193 | public function aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, bool $defaultValue)
194 | {
195 | $this->flagType = FlagValueType::BOOLEAN;
196 | $this->inputFlagKey = $flagKey;
197 | $this->inputFlagDefaultValue = $defaultValue;
198 | }
199 |
200 | /**
201 | * @Then the resolved boolean details value should be :value, the variant should be :variant, and the reason should be :reason
202 | */
203 | public function theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(bool $value, string $variant, string $reason)
204 | {
205 | $details = $this->calculateDetails();
206 |
207 | Assert::assertEquals($value, $details->getValue());
208 | Assert::assertEquals($variant, $details->getVariant());
209 | Assert::assertEquals($reason, $details->getReason());
210 | }
211 |
212 | /**
213 | * @When a string flag with key :flagKey is evaluated with details and default value :defaultValue
214 | */
215 | public function aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, string $defaultValue)
216 | {
217 | $this->flagType = FlagValueType::STRING;
218 | $this->inputFlagKey = $flagKey;
219 | $this->inputFlagDefaultValue = $defaultValue;
220 | }
221 |
222 | /**
223 | * @Then the resolved string details value should be :value, the variant should be :variant, and the reason should be :reason
224 | */
225 | public function theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(string $value, string $variant, string $reason)
226 | {
227 | $details = $this->calculateDetails();
228 |
229 | Assert::assertEquals($value, $details->getValue());
230 | Assert::assertEquals($variant, $details->getVariant());
231 | Assert::assertEquals($reason, $details->getReason());
232 | }
233 |
234 | /**
235 | * @When an integer flag with key :flagKey is evaluated with details and default value :defaultValue
236 | */
237 | public function anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, int $defaultValue)
238 | {
239 | $this->flagType = FlagValueType::INTEGER;
240 | $this->inputFlagKey = $flagKey;
241 | $this->inputFlagDefaultValue = $defaultValue;
242 | }
243 |
244 | /**
245 | * @Then the resolved integer details value should be :value, the variant should be :variant, and the reason should be :reason
246 | */
247 | public function theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(int $value, string $variant, string $reason)
248 | {
249 | $details = $this->calculateDetails();
250 |
251 | Assert::assertEquals($value, $details->getValue());
252 | Assert::assertEquals($variant, $details->getVariant());
253 | Assert::assertEquals($reason, $details->getReason());
254 | }
255 |
256 | /**
257 | * @When a float flag with key :flagKey is evaluated with details and default value :defaultValue
258 | */
259 | public function aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, float $defaultValue)
260 | {
261 | $this->flagType = FlagValueType::FLOAT;
262 | $this->inputFlagKey = $flagKey;
263 | $this->inputFlagDefaultValue = $defaultValue;
264 | }
265 |
266 | /**
267 | * @Then the resolved float details value should be :value, the variant should be :variant, and the reason should be :reason
268 | */
269 | public function theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe(float $value, string $variant, string $reason)
270 | {
271 | $details = $this->calculateDetails();
272 |
273 | Assert::assertEquals($value, $details->getValue());
274 | Assert::assertEquals($variant, $details->getVariant());
275 | Assert::assertEquals($reason, $details->getReason());
276 | }
277 |
278 | /**
279 | * @When an object flag with key :flagKey is evaluated with details and a :defaultValue default value
280 | */
281 | public function anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue(string $flagKey, mixed $defaultValue)
282 | {
283 | $this->flagType = FlagValueType::OBJECT;
284 | $this->inputFlagKey = $flagKey;
285 | $this->inputFlagDefaultValue = $defaultValue;
286 | }
287 |
288 | /**
289 | * @Then the resolved object details value should be contain fields :key1, :key2, and :key3, with values :value1, :value2 and :value3, respectively
290 | */
291 | public function theResolvedObjectDetailsValueShouldBeContainFieldsAndWithValuesAndRespectively(string $key1, string $key2, string $key3, mixed $value1, mixed $value2, mixed $value3)
292 | {
293 | $details = $this->calculateDetails();
294 |
295 | Assert::assertEquals([
296 | $key1 => $value1,
297 | $key2 => $value2,
298 | $key3 => $value3,
299 | ], $details->getValue());
300 | }
301 |
302 | /**
303 | * @Then the variant should be :variant, and the reason should be :reason
304 | */
305 | public function theVariantShouldBeAndTheReasonShouldBe(string $variant, string $reason)
306 | {
307 | $details = $this->calculateDetails();
308 |
309 | Assert::assertEquals($variant, $details->getVariant());
310 | Assert::assertEquals($reason, $details->getReason());
311 | }
312 |
313 | /**
314 | * @When context contains keys :key1, :key2, :key3, :key4 with values :value1, :value2, :value3, :value4
315 | */
316 | public function contextContainsKeysWithValues(string $key1, string $key2, string $key3, string $key4, mixed $value1, mixed $value2, mixed $value3, mixed $value4)
317 | {
318 | if ($this->isBooleanLikeString($value1)) {
319 | $value1 = $this->stringAsBool($value1);
320 | }
321 |
322 | if ($this->isBooleanLikeString($value2)) {
323 | $value2 = $this->stringAsBool($value2);
324 | }
325 |
326 | if ($this->isBooleanLikeString($value3)) {
327 | $value3 = $this->stringAsBool($value3);
328 | }
329 |
330 | if ($this->isBooleanLikeString($value4)) {
331 | $value4 = $this->stringAsBool($value4);
332 | }
333 |
334 | $this->inputContext = (new MutableEvaluationContext(null, new Attributes([
335 | $key1 => $value1,
336 | $key2 => $value2,
337 | $key3 => $value3,
338 | $key4 => $value4,
339 | ])));
340 | }
341 |
342 | /**
343 | * @When a flag with key :flagKey is evaluated with default value :defaultValue
344 | */
345 | public function aFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, mixed $defaultValue)
346 | {
347 | $this->inputFlagKey = $flagKey;
348 | $this->inputFlagDefaultValue = $defaultValue;
349 | $this->setFlagTypeIfNullByValue($defaultValue);
350 | }
351 |
352 | /**
353 | * @Then the resolved string response should be :resolvedValue
354 | */
355 | public function theResolvedStringResponseShouldBe(string $resolvedValue)
356 | {
357 | Assert::assertEquals($resolvedValue, $this->calculateValue());
358 | }
359 |
360 | /**
361 | * @Then the resolved flag value is :value when the context is empty
362 | */
363 | public function theResolvedFlagValueIsWhenTheContextIsEmpty(mixed $value)
364 | {
365 | $this->inputContext = null;
366 |
367 | Assert::assertEquals(
368 | $value,
369 | $this->calculateValue(),
370 | );
371 | }
372 |
373 | /**
374 | * @When a non-existent string flag with key :flagKey is evaluated with details and a default value :defaultValue
375 | */
376 | public function aNonExistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue(string $flagKey, string $defaultValue)
377 | {
378 | $this->inputFlagKey = $flagKey;
379 | $this->inputFlagDefaultValue = $defaultValue;
380 | $this->setFlagTypeIfNullByValue($defaultValue);
381 | }
382 |
383 | /**
384 | * @Then the default string value should be returned
385 | */
386 | public function thenTheDefaultStringValueShouldBeReturned()
387 | {
388 | Assert::assertEquals(
389 | $this->inputFlagDefaultValue,
390 | $this->calculateValue(),
391 | );
392 | }
393 |
394 | /**
395 | * @Then the reason should indicate an error and the error code should indicate a missing flag with :errorCode
396 | */
397 | public function theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith(string $errorCode)
398 | {
399 | $details = $this->calculateDetails();
400 |
401 | $error = $details->getError();
402 |
403 | Assert::assertNotNull($error);
404 | Assert::assertEquals($errorCode, (string) $error->getResolutionErrorCode());
405 | }
406 |
407 | /**
408 | * @When a string flag with key :flagKey is evaluated as an integer, with details and a default value :defaultValue
409 | */
410 | public function aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue(string $flagKey, int $defaultValue)
411 | {
412 | $this->flagType = FlagValueType::INTEGER;
413 | $this->inputFlagKey = $flagKey;
414 | $this->inputFlagDefaultValue = $defaultValue;
415 | }
416 |
417 | /**
418 | * @Then the default integer value should be returned
419 | */
420 | public function thenTheDefaultIntegerValueShouldBeReturned()
421 | {
422 | Assert::assertEquals(
423 | $this->inputFlagDefaultValue,
424 | $this->calculateValue(),
425 | );
426 | }
427 |
428 | /**
429 | * @Then the reason should indicate an error and the error code should indicate a type mismatch with :errorCode
430 | */
431 | public function theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateATypeMismatchWith(string $errorCode)
432 | {
433 | $details = $this->calculateDetails();
434 |
435 | $error = $details->getError();
436 |
437 | Assert::assertNotNull($error);
438 | Assert::assertEquals($error->getResolutionErrorCode(), ErrorCode::TYPE_MISMATCH());
439 | }
440 |
441 | /**
442 | * Ensures the value is only calculated once the first time this is called, memoizing its value
443 | *
444 | * @return mixed
445 | */
446 | private function calculateValue()
447 | {
448 | $value = null;
449 | switch ($this->flagType) {
450 | case FlagValueType::BOOLEAN:
451 | $value = $this->client->getBooleanValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
452 |
453 | break;
454 | case FlagValueType::FLOAT:
455 | $value = $this->client->getFloatValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
456 |
457 | break;
458 | case FlagValueType::INTEGER:
459 | $value = $this->client->getIntegerValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
460 |
461 | break;
462 | case FlagValueType::OBJECT:
463 | $value = $this->client->getObjectValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
464 |
465 | break;
466 | case FlagValueType::STRING:
467 | $value = $this->client->getStringValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
468 |
469 | break;
470 | }
471 |
472 | return $value;
473 | }
474 |
475 | /**
476 | * Ensures the details are only calculated once the first time this is called, memoizing its details
477 | */
478 | private function calculateDetails(): EvaluationDetails
479 | {
480 | $details = null;
481 | switch ($this->flagType) {
482 | case FlagValueType::BOOLEAN:
483 | $details = $this->client->getBooleanDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
484 |
485 | break;
486 | case FlagValueType::FLOAT:
487 | $details = $this->client->getFloatDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
488 |
489 | break;
490 | case FlagValueType::INTEGER:
491 | $details = $this->client->getIntegerDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
492 |
493 | break;
494 | case FlagValueType::OBJECT:
495 | $details = $this->client->getObjectDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
496 |
497 | break;
498 | case FlagValueType::STRING:
499 | $details = $this->client->getStringDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions);
500 |
501 | break;
502 | }
503 |
504 | return $details;
505 | }
506 |
507 | private function setFlagTypeIfNullByValue(mixed $value): void
508 | {
509 | if (!isset($this->flagType)) {
510 | $flagType = $this->getFlagTypeOf($value);
511 |
512 | if (!is_null($flagType)) {
513 | $this->flagType = $flagType;
514 | }
515 | }
516 | }
517 |
518 | private function getFlagTypeOf(mixed $value): ?string
519 | {
520 | if (is_string($value)) {
521 | return FlagValueType::STRING;
522 | }
523 |
524 | if (is_array($value)) {
525 | return FlagValueType::OBJECT;
526 | }
527 |
528 | if (is_float($value)) {
529 | return FlagValueType::FLOAT;
530 | }
531 |
532 | if (is_int($value)) {
533 | return FlagValueType::INTEGER;
534 | }
535 |
536 | if (is_bool($value)) {
537 | return FlagValueType::BOOLEAN;
538 | }
539 | }
540 |
541 | private function isBooleanLikeString(mixed $value): bool
542 | {
543 | return $value === 'true' || $value === 'false';
544 | }
545 |
546 | private function stringAsBool(string $value): bool
547 | {
548 | return $value === 'true';
549 | }
550 | }
551 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bootstrap-sha": "f7d468380d71496bfbc44ba5dc8c9adc6e006055",
3 | "packages": {
4 | ".": {
5 | "release-type": "php",
6 | "prerelease": false,
7 | "bump-minor-pre-major": true,
8 | "bump-patch-for-minor-pre-major": true,
9 | "include-v-in-tag": false,
10 | "extra-files": ["README.md"],
11 | "changelog-sections": [
12 | {
13 | "type": "fix",
14 | "section": "🐛 Bug Fixes"
15 | },
16 | {
17 | "type": "feat",
18 | "section": "✨ New Features"
19 | },
20 | {
21 | "type": "chore",
22 | "section": "🧹 Chore"
23 | },
24 | {
25 | "type": "docs",
26 | "section": "📚 Documentation"
27 | },
28 | {
29 | "type": "perf",
30 | "section": "🚀 Performance"
31 | },
32 | {
33 | "type": "build",
34 | "hidden": true,
35 | "section": "🛠️ Build"
36 | },
37 | {
38 | "type": "deps",
39 | "section": "📦 Dependencies"
40 | },
41 | {
42 | "type": "ci",
43 | "hidden": true,
44 | "section": "🚦 CI"
45 | },
46 | {
47 | "type": "refactor",
48 | "section": "🔄 Refactoring"
49 | },
50 | {
51 | "type": "revert",
52 | "section": "🔙 Reverts"
53 | },
54 | {
55 | "type": "style",
56 | "hidden": true,
57 | "section": "🎨 Styling"
58 | },
59 | {
60 | "type": "test",
61 | "hidden": true,
62 | "section": "🧪 Tests"
63 | }
64 | ]
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/OpenFeatureAPI.php:
--------------------------------------------------------------------------------
1 | provider = new NoOpProvider();
62 | }
63 |
64 | public function getProvider(): Provider
65 | {
66 | return $this->provider;
67 | }
68 |
69 | /**
70 | * -----------------
71 | * Requirement 1.1.2
72 | * -----------------
73 | * The API MUST provide a function to set the global provider singleton, which
74 | * accepts an API-conformant provider implementation.
75 | */
76 | public function setProvider(Provider $provider): void
77 | {
78 | $this->provider = $provider;
79 | }
80 |
81 | /**
82 | * -----------------
83 | * Requirement 1.1.4
84 | * -----------------
85 | * The API MUST provide a function for retrieving the metadata field of the
86 | * configured provider.
87 | */
88 | public function getProviderMetadata(): Metadata
89 | {
90 | return $this->getProvider()->getMetadata();
91 | }
92 |
93 | /**
94 | * -----------------
95 | * Requirement 1.1.4
96 | * -----------------
97 | * The API MUST provide a function for creating a client which accepts the following options:
98 | * name (optional): A logical string identifier for the client.
99 | */
100 | public function getClient(?string $name = null, ?string $version = null): Client
101 | {
102 | $name = $name ?? 'OpenFeature';
103 | $version = $version ?? 'OpenFeature';
104 |
105 | try {
106 | $client = new OpenFeatureClient($this, $name, $version);
107 | $client->setLogger($this->getLogger());
108 |
109 | return $client;
110 | } catch (Throwable $err) {
111 | return new NoOpClient();
112 | }
113 | }
114 |
115 | /**
116 | * @return Hook[]
117 | */
118 | public function getHooks(): array
119 | {
120 | return $this->hooks;
121 | }
122 |
123 | /**
124 | * -----------------
125 | * Requirement 1.1.3
126 | * -----------------
127 | * The API MUST provide a function to add hooks which accepts one or more API-conformant
128 | * hooks, and appends them to the collection of any previously added hooks. When new
129 | * hooks are added, previously added hooks are not removed.
130 | */
131 | public function addHooks(Hook ...$hooks): void
132 | {
133 | $this->hooks = array_merge($this->hooks, $hooks);
134 | }
135 |
136 | public function clearHooks(): void
137 | {
138 | $this->hooks = [];
139 | }
140 |
141 | public function getEvaluationContext(): ?EvaluationContext
142 | {
143 | return $this->evaluationContext;
144 | }
145 |
146 | public function setEvaluationContext(EvaluationContext $context): void
147 | {
148 | $this->evaluationContext = $context;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/OpenFeatureClient.php:
--------------------------------------------------------------------------------
1 | api = $api;
63 | $this->name = $name;
64 | $this->version = $version;
65 | $this->hooks = [];
66 | }
67 |
68 | public function getVersion(): string
69 | {
70 | return $this->version;
71 | }
72 |
73 | /**
74 | * Return an optional client-level evaluation context.
75 | */
76 | public function getEvaluationContext(): ?EvaluationContextInterface
77 | {
78 | return $this->evaluationContext;
79 | }
80 |
81 | /**
82 | * Set the client-level evaluation context.
83 | *
84 | * @param EvaluationContextInterface $context Client level context.
85 | */
86 | public function setEvaluationContext(EvaluationContextInterface $context): void
87 | {
88 | $this->evaluationContext = $context;
89 | }
90 |
91 | /**
92 | * -----------------
93 | * Requirement 1.2.1
94 | * -----------------
95 | * The client MUST provide a method to add hooks which accepts one or more
96 | * API-conformant hooks, and appends them to the collection of any previously
97 | * added hooks. When new hooks are added, previously added hooks are not removed.
98 | *
99 | * Adds hooks for evaluation.
100 | * Hooks are run in the order they're added in the before stage. They are run in
101 | * reverse order for all other stages.
102 | */
103 | public function addHooks(Hook ...$hooks): void
104 | {
105 | $this->hooks = array_merge($this->hooks, $hooks);
106 | }
107 |
108 | /**
109 | * -----------------
110 | * Requirement 1.2.2
111 | * -----------------
112 | * The client interface MUST define a metadata member or accessor, containing
113 | * an immutable name field or accessor of type string, which corresponds to
114 | * the name value supplied during client creation.
115 | *
116 | * Returns the metadata for the current resource
117 | */
118 | public function getMetadata(): MetadataInterface
119 | {
120 | return new Metadata($this->name);
121 | }
122 |
123 | /**
124 | * -----------------
125 | * Requirement 1.3.1
126 | * -----------------
127 | * The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure,
128 | * with parameters flag key (string, required), default value (boolean | number | string | structure, required),
129 | * evaluation context (optional), and evaluation options (optional), which returns the flag value.
130 | */
131 | public function getBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): bool
132 | {
133 | /** @var bool $value */
134 | $value = $this->getBooleanDetails($flagKey, $defaultValue, $context, $options)->getValue() ?? $defaultValue;
135 |
136 | return $value;
137 | }
138 |
139 | /**
140 | * -----------------
141 | * Requirement 1.4.1
142 | * -----------------
143 | * The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required),
144 | * default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation
145 | * options (optional), which returns an evaluation details structure.
146 | */
147 | public function getBooleanDetails(string $flagKey, bool $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface
148 | {
149 | return $this->evaluateFlag(FlagValueType::BOOLEAN, $flagKey, $defaultValue, $context, $options);
150 | }
151 |
152 | /**
153 | * -----------------
154 | * Requirement 1.3.1
155 | * -----------------
156 | * The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure,
157 | * with parameters flag key (string, required), default value (boolean | number | string | structure, required),
158 | * evaluation context (optional), and evaluation options (optional), which returns the flag value.
159 | */
160 | public function getStringValue(string $flagKey, string $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): string
161 | {
162 | /** @var string $value */
163 | $value = $this->getStringDetails($flagKey, $defaultValue, $context, $options)->getValue() ?? $defaultValue;
164 |
165 | return $value;
166 | }
167 |
168 | /**
169 | * -----------------
170 | * Requirement 1.4.1
171 | * -----------------
172 | * The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required),
173 | * default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation
174 | * options (optional), which returns an evaluation details structure.
175 | */
176 | public function getStringDetails(string $flagKey, string $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface
177 | {
178 | return $this->evaluateFlag(FlagValueType::STRING, $flagKey, $defaultValue, $context, $options);
179 | }
180 |
181 | /**
182 | * -----------------
183 | * Requirement 1.3.1
184 | * -----------------
185 | * The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure,
186 | * with parameters flag key (string, required), default value (boolean | number | string | structure, required),
187 | * evaluation context (optional), and evaluation options (optional), which returns the flag value.
188 | *
189 | * -----------------
190 | * Conditional Requirement 1.3.2.1
191 | * -----------------
192 | * The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.
193 | */
194 | public function getIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): int
195 | {
196 | /** @var int $value */
197 | $value = $this->getIntegerDetails($flagKey, $defaultValue, $context, $options)->getValue() ?? $defaultValue;
198 |
199 | return $value;
200 | }
201 |
202 | /**
203 | * -----------------
204 | * Requirement 1.4.1
205 | * -----------------
206 | * The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required),
207 | * default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation
208 | * options (optional), which returns an evaluation details structure.
209 | */
210 | public function getIntegerDetails(string $flagKey, int $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface
211 | {
212 | return $this->evaluateFlag(FlagValueType::INTEGER, $flagKey, $defaultValue, $context, $options);
213 | }
214 |
215 | /**
216 | * -----------------
217 | * Requirement 1.3.1
218 | * -----------------
219 | * The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure,
220 | * with parameters flag key (string, required), default value (boolean | number | string | structure, required),
221 | * evaluation context (optional), and evaluation options (optional), which returns the flag value.
222 | *
223 | * -----------------
224 | * Conditional Requirement 1.3.2.1
225 | * -----------------
226 | * The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.
227 | */
228 | public function getFloatValue(string $flagKey, float $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): float
229 | {
230 | /** @var float $value */
231 | $value = $this->getFloatDetails($flagKey, $defaultValue, $context, $options)->getValue() ?? $defaultValue;
232 |
233 | return $value;
234 | }
235 |
236 | /**
237 | * -----------------
238 | * Requirement 1.4.1
239 | * -----------------
240 | * The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required),
241 | * default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation
242 | * options (optional), which returns an evaluation details structure.
243 | */
244 | public function getFloatDetails(string $flagKey, float $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface
245 | {
246 | return $this->evaluateFlag(FlagValueType::FLOAT, $flagKey, $defaultValue, $context, $options);
247 | }
248 |
249 | /**
250 | * -----------------
251 | * Requirement 1.3.1
252 | * -----------------
253 | * The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure,
254 | * with parameters flag key (string, required), default value (boolean | number | string | structure, required),
255 | * evaluation context (optional), and evaluation options (optional), which returns the flag value.
256 | *
257 | * @inheritdoc
258 | */
259 | public function getObjectValue(string $flagKey, $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null)
260 | {
261 | /** @var mixed[] $value */
262 | $value = $this->getObjectDetails($flagKey, $defaultValue, $context, $options)->getValue() ?? $defaultValue;
263 |
264 | return $value;
265 | }
266 |
267 | /**
268 | * -----------------
269 | * Requirement 1.3.1
270 | * -----------------
271 | * The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag val4e.
272 | *
273 | * The client MUST provide methods for detailed flag value evaluation with parameters flag key (string, required),
274 | * default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation
275 | * options (optional), which returns an evaluation details structure.
276 | *
277 | * @inheritdoc
278 | */
279 | public function getObjectDetails(string $flagKey, $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface
280 | {
281 | return $this->evaluateFlag(FlagValueType::OBJECT, $flagKey, $defaultValue, $context, $options);
282 | }
283 |
284 | /**
285 | * -----------------
286 | * Requirement 1.4.9
287 | * -----------------
288 | * Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the default value in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.
289 | *
290 | * @param bool|string|int|float|DateTime|mixed[]|null $defaultValue
291 | */
292 | private function evaluateFlag(
293 | string $flagValueType,
294 | string $flagKey,
295 | bool | string | int | float | DateTime | array | null $defaultValue,
296 | ?EvaluationContextInterface $invocationContext = null,
297 | ?EvaluationOptionsInterface $options = null,
298 | ): EvaluationDetailsInterface {
299 | $api = $this->api;
300 | $provider = $api->getProvider();
301 | /** @var EvaluationOptionsInterface $options */
302 | $options = $options ?? new EvaluationOptions();
303 | $hookHints = $options->getHookHints() ?? new HookHints();
304 | $hookExecutor = new HookExecutor($this->logger);
305 |
306 | $mergedContext = EvaluationContext::merge(
307 | $api->getEvaluationContext(),
308 | $this->getEvaluationContext(),
309 | $invocationContext,
310 | );
311 |
312 | $hookContext = HookContextFactory::from(
313 | $flagKey,
314 | $flagValueType,
315 | $defaultValue,
316 | $mergedContext,
317 | $this->getMetadata(),
318 | $provider->getMetadata(),
319 | );
320 |
321 | // -----------------
322 | // Requirement 4.4.2
323 | // -----------------
324 | // Hooks MUST be evaluated in the following order:
325 | // before: API, Client, Invocation, Provider
326 | // after: Provider, Invocation, Client, API
327 | // error (if applicable): Provider, Invocation, Client, API
328 | // finally: Provider, Invocation, Client, API
329 | $mergedBeforeHooks = array_merge(
330 | $api->getHooks(),
331 | $this->getHooks(),
332 | $options->getHooks(),
333 | $provider->getHooks(),
334 | );
335 |
336 | $mergedRemainingHooks = array_reverse(array_merge([], $mergedBeforeHooks));
337 |
338 | try {
339 | $contextFromBeforeHook = $hookExecutor->beforeHooks($flagValueType, $hookContext, $mergedBeforeHooks, $hookHints);
340 |
341 | $mergedContext = EvaluationContext::merge($mergedContext, $contextFromBeforeHook);
342 | $hookContext = (new HookContextBuilder())
343 | ->withFlagKey($hookContext->getFlagKey())
344 | ->withType($hookContext->getType())
345 | ->withDefaultValue($hookContext->getDefaultValue())
346 | ->withEvaluationContext($mergedContext)
347 | ->withClientMetadata($hookContext->getClientMetadata())
348 | ->withProviderMetadata($hookContext->getProviderMetadata())
349 | ->build();
350 |
351 | $resolutionDetails = $this->createProviderEvaluation(
352 | $flagValueType,
353 | $flagKey,
354 | $defaultValue,
355 | $provider,
356 | $mergedContext,
357 | );
358 |
359 | if (!$resolutionDetails->getError() && !ValueTypeValidator::is($flagValueType, $resolutionDetails->getValue())) {
360 | throw new InvalidResolutionValueError($flagValueType);
361 | }
362 |
363 | $details = EvaluationDetailsFactory::fromResolution($flagKey, $resolutionDetails);
364 |
365 | $hookExecutor->afterHooks($flagValueType, $hookContext, $resolutionDetails, $mergedRemainingHooks, $hookHints);
366 | } catch (Throwable $err) {
367 | $this->getLogger()->error(
368 | sprintf(
369 | "An error occurred during feature flag evaluation of flag '%s': %s",
370 | $flagKey,
371 | $err->getMessage(),
372 | ),
373 | );
374 |
375 | $error = $err instanceof ThrowableWithResolutionError ? $err->getResolutionError() : new ResolutionError(ErrorCode::GENERAL(), $err->getMessage());
376 |
377 | $details = (new EvaluationDetailsBuilder())
378 | ->withFlagKey($flagKey)
379 | ->withValue($defaultValue)
380 | ->withReason(Reason::ERROR)
381 | ->withError($error)
382 | ->build();
383 |
384 | $hookExecutor->errorHooks($flagValueType, $hookContext, $err, $mergedRemainingHooks, $hookHints);
385 | } finally {
386 | $hookExecutor->finallyHooks($flagValueType, $hookContext, $mergedRemainingHooks, $hookHints);
387 | }
388 |
389 | return $details;
390 | }
391 |
392 | private function createProviderEvaluation(
393 | string $type,
394 | string $key,
395 | mixed $defaultValue,
396 | Provider $provider,
397 | EvaluationContextInterface $context,
398 | ): ResolutionDetails {
399 | switch ($type) {
400 | case FlagValueType::BOOLEAN:
401 | /** @var bool $defaultValue */
402 | $defaultValue = $defaultValue;
403 |
404 | return $provider->resolveBooleanValue($key, $defaultValue, $context);
405 | case FlagValueType::STRING:
406 | /** @var string $defaultValue */
407 | $defaultValue = $defaultValue;
408 |
409 | return $provider->resolveStringValue($key, $defaultValue, $context);
410 | case FlagValueType::INTEGER:
411 | /** @var int $defaultValue */
412 | $defaultValue = $defaultValue;
413 |
414 | return $provider->resolveIntegerValue($key, $defaultValue, $context);
415 | case FlagValueType::FLOAT:
416 | /** @var float $defaultValue */
417 | $defaultValue = $defaultValue;
418 |
419 | return $provider->resolveFloatValue($key, $defaultValue, $context);
420 | case FlagValueType::OBJECT:
421 | /** @var mixed[] $defaultValue */
422 | $defaultValue = $defaultValue;
423 |
424 | return $provider->resolveObjectValue($key, $defaultValue, $context);
425 | default:
426 | throw new FlagValueTypeError($type);
427 | }
428 | }
429 | }
430 |
--------------------------------------------------------------------------------
/src/implementation/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/open-feature/php-sdk/c71bf429267f0a3cfd50348aaf8d091d68b1491b/src/implementation/.gitkeep
--------------------------------------------------------------------------------
/src/implementation/common/ArrayHelper.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public static function getStringKeys(array $array)
21 | {
22 | $keys = array_keys($array);
23 |
24 | if (sizeof($keys) === 0 || is_int($keys[0])) {
25 | return [];
26 | }
27 |
28 | /** @var array $stringKeys */
29 | $stringKeys = $keys;
30 |
31 | return $stringKeys;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/implementation/common/Metadata.php:
--------------------------------------------------------------------------------
1 | name = $name;
16 | }
17 |
18 | public function getName(): string
19 | {
20 | return $this->name;
21 | }
22 |
23 | public function setName(string $name): void
24 | {
25 | $this->name = $name;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/implementation/common/StringHelper.php:
--------------------------------------------------------------------------------
1 | $attributesMap */
16 | protected array $attributesMap;
17 |
18 | /**
19 | * @param Array $attributesMap
20 | */
21 | public function __construct(array $attributesMap = [])
22 | {
23 | $this->attributesMap = $attributesMap;
24 | }
25 |
26 | /**
27 | * Return key-type pairs of the attributes
28 | *
29 | * @return Array
30 | */
31 | public function keys(): array
32 | {
33 | return ArrayHelper::getStringKeys($this->attributesMap);
34 | }
35 |
36 | /**
37 | * @return bool|string|int|float|DateTime|mixed[]|null
38 | */
39 | public function get(string $key): bool | string | int | float | DateTime | array | null
40 | {
41 | if (isset($this->attributesMap[$key])) {
42 | return $this->attributesMap[$key];
43 | }
44 |
45 | return null;
46 | }
47 |
48 | /**
49 | * @return Array
50 | */
51 | public function toArray(): array
52 | {
53 | return array_merge([], $this->attributesMap);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/implementation/flags/AttributesMerger.php:
--------------------------------------------------------------------------------
1 | mergeWith($initialAttributes);
30 | }
31 |
32 | foreach ($attributes as $additionalAttributes) {
33 | $mutableAttributes->mergeWith($additionalAttributes);
34 | }
35 |
36 | return $mutableAttributes;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/implementation/flags/EvaluationContext.php:
--------------------------------------------------------------------------------
1 | targetingKey = $targetingKey;
20 | $this->attributes = $attributes ?? new Attributes();
21 | }
22 |
23 | public function getTargetingKey(): ?string
24 | {
25 | return $this->targetingKey;
26 | }
27 |
28 | public function setTargetingKey(?string $targetingKey): void
29 | {
30 | $this->targetingKey = $targetingKey;
31 | }
32 |
33 | public function getAttributes(): AttributesInterface
34 | {
35 | return $this->attributes;
36 | }
37 |
38 | public static function createNull(): EvaluationContext
39 | {
40 | return new EvaluationContext();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/implementation/flags/EvaluationContextMerger.php:
--------------------------------------------------------------------------------
1 | getTargetingKey();
46 | $additionalTargetingKey = $additionalContext->getTargetingKey();
47 |
48 | /** @var ?string $newTargetingKey */
49 | $newTargetingKey = null;
50 | if (!is_null($additionalTargetingKey) && strlen($additionalTargetingKey) > 0) {
51 | $newTargetingKey = $additionalTargetingKey;
52 | } elseif (!is_null($calculatedTargetingKey) && strlen($calculatedTargetingKey) > 0) {
53 | $newTargetingKey = $calculatedTargetingKey;
54 | }
55 |
56 | $mergedAttributes = AttributesMerger::merge(
57 | $calculatedContext->getAttributes(),
58 | $additionalContext->getAttributes(),
59 | );
60 |
61 | $calculatedContext = new MutableEvaluationContext(
62 | $newTargetingKey,
63 | $mergedAttributes,
64 | );
65 | }
66 |
67 | $i = $i + 1;
68 | }
69 |
70 | return $calculatedContext;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/implementation/flags/EvaluationDetails.php:
--------------------------------------------------------------------------------
1 | value = null;
24 | }
25 |
26 | public function getFlagKey(): string
27 | {
28 | return $this->flagKey;
29 | }
30 |
31 | public function setFlagKey(string $flagKey): void
32 | {
33 | $this->flagKey = $flagKey;
34 | }
35 |
36 | /**
37 | * -----------------
38 | * Requirement 1.4.2
39 | * -----------------
40 | * The evaluation details structure's value field MUST contain the evaluated flag value.
41 | *
42 | * @return bool|string|int|float|DateTime|mixed[]|null
43 | */
44 | public function getValue(): bool | string | int | float | DateTime | array | null
45 | {
46 | return $this->value;
47 | }
48 |
49 | /**
50 | * @param bool|string|int|float|DateTime|mixed[]|null $value
51 | */
52 | public function setValue(bool | string | int | float | DateTime | array | null $value): void
53 | {
54 | $this->value = $value;
55 | }
56 |
57 | public function getError(): ?ResolutionError
58 | {
59 | return $this->error;
60 | }
61 |
62 | public function setError(?ResolutionError $error): void
63 | {
64 | $this->error = $error;
65 | }
66 |
67 | public function getReason(): ?string
68 | {
69 | return $this->reason;
70 | }
71 |
72 | public function setReason(?string $reason): void
73 | {
74 | $this->reason = $reason;
75 | }
76 |
77 | public function getVariant(): ?string
78 | {
79 | return $this->variant;
80 | }
81 |
82 | public function setVariant(?string $variant): void
83 | {
84 | $this->variant = $variant;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/implementation/flags/EvaluationDetailsBuilder.php:
--------------------------------------------------------------------------------
1 | details = new EvaluationDetails();
18 | }
19 |
20 | public function withFlagKey(string $flagKey): EvaluationDetailsBuilder
21 | {
22 | $this->details->setFlagKey($flagKey);
23 |
24 | return $this;
25 | }
26 |
27 | /**
28 | * @param bool|string|int|float|DateTime|mixed[]|null $value
29 | */
30 | public function withValue(bool | string | int | float | DateTime | array | null $value): EvaluationDetailsBuilder
31 | {
32 | $this->details->setValue($value);
33 |
34 | return $this;
35 | }
36 |
37 | public function withError(?ResolutionError $errorCode): EvaluationDetailsBuilder
38 | {
39 | $this->details->setError($errorCode);
40 |
41 | return $this;
42 | }
43 |
44 | public function withReason(?string $reason): EvaluationDetailsBuilder
45 | {
46 | $this->details->setReason($reason);
47 |
48 | return $this;
49 | }
50 |
51 | public function withVariant(?string $variant): EvaluationDetailsBuilder
52 | {
53 | $this->details->setVariant($variant);
54 |
55 | return $this;
56 | }
57 |
58 | public function build(): EvaluationDetailsInterface
59 | {
60 | return $this->details;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/implementation/flags/EvaluationDetailsFactory.php:
--------------------------------------------------------------------------------
1 | withFlagKey($flagKey)
22 | ->withValue($value)
23 | ->build();
24 | }
25 |
26 | /**
27 | * Provides a simple method for building EvaluationDetails from Flag Resolution Details
28 | */
29 | public static function fromResolution(string $flagKey, ResolutionDetails $details): EvaluationDetails
30 | {
31 | return (new EvaluationDetailsBuilder())
32 | ->withFlagKey($flagKey)
33 | ->withValue($details->getValue())
34 | ->withError($details->getError())
35 | ->withReason($details->getReason())
36 | ->withVariant($details->getVariant())
37 | ->build();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/implementation/flags/EvaluationOptions.php:
--------------------------------------------------------------------------------
1 | setHooks($hooks);
24 | $this->hookHints = $hookHints;
25 | }
26 |
27 | public function getHookHints(): ?HookHints
28 | {
29 | return $this->hookHints;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/implementation/flags/MutableAttributes.php:
--------------------------------------------------------------------------------
1 | keys(),
19 | /**
20 | * @param Array $map
21 | *
22 | * @return Array
23 | */
24 | function (array $map, string $key) use ($attributes) {
25 | $map[$key] = $attributes->get($key);
26 |
27 | return $map;
28 | },
29 | [],
30 | );
31 |
32 | return new MutableAttributes($attributeMap);
33 | }
34 |
35 | public function add(string $key, bool | string | int | float | DateTime | array | null $value): void
36 | {
37 | $this->attributesMap[$key] = $value;
38 | }
39 |
40 | /**
41 | * Merges an Attributes object into another Attributes
42 | */
43 | public function mergeWith(AttributesInterface $additionalAttributes): AttributesInterface
44 | {
45 | $mutableAttributes = $this;
46 |
47 | foreach ($additionalAttributes->keys() as $key) {
48 | $mutableAttributes->add($key, $additionalAttributes->get($key));
49 | }
50 |
51 | return $mutableAttributes;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/implementation/flags/MutableEvaluationContext.php:
--------------------------------------------------------------------------------
1 | attributes->toArray());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/implementation/flags/NoOpClient.php:
--------------------------------------------------------------------------------
1 | evaluationContext = EvaluationContext::createNull();
36 | $this->clientMetadata = new Metadata('empty-client');
37 | $this->providerMetadata = new Metadata('empty-provider');
38 |
39 | if ($hookContext instanceof HookContext) {
40 | $this->flagKey = $hookContext->getFlagKey();
41 | $this->type = $hookContext->getType();
42 | $this->evaluationContext = $hookContext->getEvaluationContext();
43 | $this->clientMetadata = $hookContext->getClientMetadata();
44 | $this->providerMetadata = $hookContext->getProviderMetadata();
45 | } elseif (is_array($hookContext)) {
46 | /** @var string $property */
47 | /** @var mixed $value */
48 | foreach ($hookContext as $property => $value) {
49 | $this->$property = $value;
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/implementation/hooks/BooleanHook.php:
--------------------------------------------------------------------------------
1 | hookContext = new MutableHookContext();
20 | }
21 |
22 | public function withFlagKey(string $flagKey): self
23 | {
24 | $this->hookContext->setFlagKey($flagKey);
25 |
26 | return $this;
27 | }
28 |
29 | public function withType(string $type): self
30 | {
31 | $this->hookContext->setType($type);
32 |
33 | return $this;
34 | }
35 |
36 | /**
37 | * @param bool|string|int|float|DateTime|mixed[]|null $defaultValue
38 | */
39 | public function withDefaultValue(bool | string | int | float | DateTime | array | null $defaultValue): self
40 | {
41 | $this->hookContext->setDefaultValue($defaultValue);
42 |
43 | return $this;
44 | }
45 |
46 | public function withEvaluationContext(EvaluationContext $evaluationContext): self
47 | {
48 | $this->hookContext->setEvaluationContext($evaluationContext);
49 |
50 | return $this;
51 | }
52 |
53 | public function withClientMetadata(Metadata $clientMetadata): self
54 | {
55 | $this->hookContext->setClientMetadata($clientMetadata);
56 |
57 | return $this;
58 | }
59 |
60 | public function withProviderMetadata(Metadata $providerMetadata): self
61 | {
62 | $this->hookContext->setProviderMetadata($providerMetadata);
63 |
64 | return $this;
65 | }
66 |
67 | public function asMutable(): self
68 | {
69 | $this->isImmutable = false;
70 |
71 | return $this;
72 | }
73 |
74 | public function asImmutable(): self
75 | {
76 | $this->isImmutable = true;
77 |
78 | return $this;
79 | }
80 |
81 | public function build(): HookContextInterface
82 | {
83 | $context = $this->hookContext;
84 |
85 | if ($this->isImmutable) {
86 | $context = HookContextTransformer::toImmutable($context);
87 | }
88 |
89 | return $context;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/implementation/hooks/HookContextFactory.php:
--------------------------------------------------------------------------------
1 | withFlagKey($flagKey)
30 | ->withType($type)
31 | ->withDefaultValue($defaultValue)
32 | ->withEvaluationContext($evaluationContext ?? EvaluationContext::createNull())
33 | ->withClientMetadata($clientMetadata)
34 | ->withProviderMetadata($providerMetadata)
35 | ->build();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/implementation/hooks/HookContextTransformer.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
29 | }
30 |
31 | /**
32 | * The beforeHook differentiate from the other lifecycle hooks in that the return value
33 | * is a new EvaluationContext to be used in the evaluation of the flags, whereas no other
34 | * lifecycle hook provides context mutation.
35 | *
36 | * @param Hook[] $mergedHooks
37 | */
38 | public function beforeHooks(string $type, HookContext $hookContext, array $mergedHooks, HookHintsInterface $hints): ?EvaluationContext
39 | {
40 | $additionalContext = new MutableEvaluationContext();
41 |
42 | foreach ($mergedHooks as $hook) {
43 | if ($hook->supportsFlagValueType($type)) {
44 | $additionalContext = FlagsEvaluationContext::merge(
45 | $additionalContext,
46 | $this->executeHookUnsafe(fn () => $hook->before($hookContext, $hints)),
47 | );
48 | }
49 | }
50 |
51 | return $additionalContext;
52 | }
53 |
54 | /**
55 | * @param Hook[] $mergedHooks
56 | */
57 | public function afterHooks(string $type, HookContext $hookContext, ResolutionDetails $details, array $mergedHooks, HookHintsInterface $hints): void
58 | {
59 | foreach ($mergedHooks as $hook) {
60 | if ($hook->supportsFlagValueType($type)) {
61 | $this->executeHookUnsafe(fn () => $hook->after($hookContext, $details, $hints));
62 | }
63 | }
64 | }
65 |
66 | /**
67 | * @param Hook[] $mergedHooks
68 | */
69 | public function errorHooks(string $type, HookContext $hookContext, Throwable $err, array $mergedHooks, HookHintsInterface $hints): void
70 | {
71 | foreach ($mergedHooks as $hook) {
72 | if ($hook->supportsFlagValueType($type)) {
73 | $this->executeHookSafe(fn () => $hook->error($hookContext, $err, $hints), 'error');
74 | }
75 | }
76 | }
77 |
78 | /**
79 | * @param Hook[] $mergedHooks
80 | */
81 | public function finallyHooks(string $type, HookContext $hookContext, array $mergedHooks, HookHintsInterface $hints): void
82 | {
83 | foreach ($mergedHooks as $hook) {
84 | if ($hook->supportsFlagValueType($type)) {
85 | $this->executeHookSafe(fn () => $hook->finally($hookContext, $hints), 'finally');
86 | }
87 | }
88 | }
89 |
90 | private function executeHookSafe(callable $func, string $hookMethod): ?EvaluationContext
91 | {
92 | try {
93 | return $this->executeHookUnsafe($func);
94 | } catch (Throwable $err) {
95 | $this->getLogger()->error(
96 | sprintf('Error %s occurred while executing the %s hook', $err->getMessage(), $hookMethod),
97 | );
98 |
99 | return null;
100 | }
101 | }
102 |
103 | private function executeHookUnsafe(callable $func): ?EvaluationContext
104 | {
105 | /** @var EvaluationContext|null $value */
106 | $value = call_user_func($func);
107 |
108 | if ($value instanceof EvaluationContext) {
109 | return $value;
110 | }
111 |
112 | return null;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/implementation/hooks/HookHints.php:
--------------------------------------------------------------------------------
1 | $hints */
16 | private array $hints = [];
17 |
18 | /**
19 | * @return bool|string|int|float|DateTime|mixed[]|null
20 | */
21 | public function get(string $key): bool | string | int | float | DateTime | array | null
22 | {
23 | if (key_exists($key, $this->hints)) {
24 | return $this->hints[$key];
25 | }
26 |
27 | return null;
28 | }
29 |
30 | /**
31 | * @return string[]
32 | */
33 | public function keys(): array
34 | {
35 | return array_keys($this->hints);
36 | }
37 |
38 | /**
39 | * @param Array $hints
40 | */
41 | public function __construct(array $hints = [])
42 | {
43 | $this->hints = $hints;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/implementation/hooks/ImmutableHookContext.php:
--------------------------------------------------------------------------------
1 | flagKey;
17 | }
18 |
19 | public function getType(): string
20 | {
21 | return $this->type;
22 | }
23 |
24 | /**
25 | * @return bool|string|int|float|DateTime|mixed[]|null
26 | */
27 | public function getDefaultValue(): bool | string | int | float | DateTime | array | null
28 | {
29 | return $this->defaultValue;
30 | }
31 |
32 | public function getEvaluationContext(): EvaluationContext
33 | {
34 | return $this->evaluationContext;
35 | }
36 |
37 | public function getClientMetadata(): Metadata
38 | {
39 | return $this->clientMetadata;
40 | }
41 |
42 | public function getProviderMetadata(): Metadata
43 | {
44 | return $this->providerMetadata;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/implementation/hooks/IntegerHook.php:
--------------------------------------------------------------------------------
1 | flagKey = $flagKey;
18 | }
19 |
20 | public function setType(string $type): void
21 | {
22 | $this->type = $type;
23 | }
24 |
25 | public function setDefaultValue(bool | string | int | float | DateTime | array | null $defaultValue): void
26 | {
27 | $this->defaultValue = $defaultValue;
28 | }
29 |
30 | public function setEvaluationContext(EvaluationContext $evaluationContext): void
31 | {
32 | $this->evaluationContext = $evaluationContext;
33 | }
34 |
35 | public function setClientMetadata(Metadata $clientMetadata): void
36 | {
37 | $this->clientMetadata = $clientMetadata;
38 | }
39 |
40 | public function setProviderMetadata(Metadata $providerMetadata): void
41 | {
42 | $this->providerMetadata = $providerMetadata;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/implementation/hooks/ObjectHook.php:
--------------------------------------------------------------------------------
1 | hooks;
48 | }
49 |
50 | /**
51 | * @param Hook[] $hooks
52 | */
53 | public function setHooks(array $hooks): void
54 | {
55 | $this->hooks = $hooks;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/implementation/provider/NoOpProvider.php:
--------------------------------------------------------------------------------
1 | value;
25 | }
26 |
27 | /**
28 | * @param bool|string|int|float|DateTime|mixed[]|null $value
29 | */
30 | public function setValue(bool | string | int | float | DateTime | array | null $value): void
31 | {
32 | $this->value = $value;
33 | }
34 |
35 | public function getError(): ?ResolutionError
36 | {
37 | return $this->error;
38 | }
39 |
40 | public function setError(?ResolutionError $error): void
41 | {
42 | $this->error = $error;
43 | }
44 |
45 | public function getReason(): ?string
46 | {
47 | return $this->reason;
48 | }
49 |
50 | public function setReason(?string $reason): void
51 | {
52 | $this->reason = $reason;
53 | }
54 |
55 | public function getVariant(): ?string
56 | {
57 | return $this->variant;
58 | }
59 |
60 | public function setVariant(?string $variant): void
61 | {
62 | $this->variant = $variant;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/implementation/provider/ResolutionDetailsBuilder.php:
--------------------------------------------------------------------------------
1 | details = new ResolutionDetails();
18 | }
19 |
20 | /**
21 | * @param bool|string|int|float|DateTime|mixed[]|null $value
22 | */
23 | public function withValue(bool | string | int | float | DateTime | array | null $value): ResolutionDetailsBuilder
24 | {
25 | $this->details->setValue($value);
26 |
27 | return $this;
28 | }
29 |
30 | public function withError(ResolutionError $errorCode): ResolutionDetailsBuilder
31 | {
32 | $this->details->setError($errorCode);
33 |
34 | return $this;
35 | }
36 |
37 | public function withReason(string $reason): ResolutionDetailsBuilder
38 | {
39 | $this->details->setReason($reason);
40 |
41 | return $this;
42 | }
43 |
44 | public function withVariant(string $variant): ResolutionDetailsBuilder
45 | {
46 | $this->details->setVariant($variant);
47 |
48 | return $this;
49 | }
50 |
51 | public function build(): ResolutionDetailsInterface
52 | {
53 | return $this->details;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/implementation/provider/ResolutionDetailsFactory.php:
--------------------------------------------------------------------------------
1 | withValue($value)
19 | ->build();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/implementation/provider/ResolutionError.php:
--------------------------------------------------------------------------------
1 | resolutionErrorCode = $code;
20 | $this->resolutionErrorMessage = $message;
21 | }
22 |
23 | public function getResolutionErrorCode(): ErrorCode
24 | {
25 | return $this->resolutionErrorCode;
26 | }
27 |
28 | public function getResolutionErrorMessage(): ?string
29 | {
30 | return $this->resolutionErrorMessage;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/interfaces/common/LoggerAwareTrait.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
28 | }
29 |
30 | /**
31 | * Gets the logger, defaulting to NullLogger if not set
32 | */
33 | public function getLogger(): LoggerInterface
34 | {
35 | if (!is_null($this->logger)) {
36 | return $this->logger;
37 | }
38 |
39 | return new NullLogger();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/interfaces/common/Metadata.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | public function keys(): array;
17 |
18 | /**
19 | * @return bool|string|int|float|DateTime|mixed[]|null
20 | */
21 | public function get(string $key): bool | string | int | float | DateTime | array | null;
22 |
23 | /**
24 | * @return Array
25 | */
26 | public function toArray(): array;
27 | }
28 |
--------------------------------------------------------------------------------
/src/interfaces/flags/Client.php:
--------------------------------------------------------------------------------
1 | hooks;
18 | }
19 |
20 | /**
21 | * @param Hook[] $hooks
22 | */
23 | public function setHooks(array $hooks): void
24 | {
25 | $this->hooks = $hooks;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/interfaces/hooks/HooksGetter.php:
--------------------------------------------------------------------------------
1 |
23 | *
24 | * @psalm-immutable
25 | */
26 | final class ErrorCode extends Enum
27 | {
28 | private const PROVIDER_NOT_READY = 'PROVIDER_NOT_READY';
29 | private const FLAG_NOT_FOUND = 'FLAG_NOT_FOUND';
30 | private const PARSE_ERROR = 'PARSE_ERROR';
31 | private const TYPE_MISMATCH = 'TYPE_MISMATCH';
32 | private const TARGETING_KEY_MISSING = 'TARGETING_KEY_MISSING';
33 | private const INVALID_CONTEXT = 'INVALID_CONTEXT';
34 | private const GENERAL = 'GENERAL';
35 | }
36 |
--------------------------------------------------------------------------------
/src/interfaces/provider/Provider.php:
--------------------------------------------------------------------------------
1 |