├── .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 | OpenFeature Logo 7 | 8 |

9 | 10 |

OpenFeature PHP SDK

11 | 12 | 13 | 14 |

15 | 16 | 17 | Specification 18 | 19 | 20 | 21 | 22 | Release 23 | 24 | 25 | 26 |
27 | 28 | 29 | Total Downloads 30 | 31 | 32 | 33 | PHP 8.0+ 34 | 35 | 36 | 37 | License 38 | 39 | 40 | 41 | OpenSSF Best Practices 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 | Pictures of the folks who have contributed to the project 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 |