From 2f80975613745c1cdadb62268204bde58425fcce Mon Sep 17 00:00:00 2001 From: legoheld Date: Wed, 22 Nov 2023 09:41:27 +0100 Subject: [PATCH 01/18] feat: Add flipt provider with PSR-16 caching support Signed-off-by: legoheld --- .vscode/launch.json | 51 +++++ providers/Flipt/.gitignore | 4 + providers/Flipt/LICENSE | 201 ++++++++++++++++++++ providers/Flipt/README.md | 60 ++++++ providers/Flipt/composer.json | 34 ++++ providers/Flipt/phpunit.xml | 11 ++ providers/Flipt/src/Cache.php | 64 +++++++ providers/Flipt/src/FliptProvider.php | 137 +++++++++++++ providers/Flipt/tests/FliptProviderTest.php | 147 ++++++++++++++ 9 files changed, 709 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 providers/Flipt/.gitignore create mode 100644 providers/Flipt/LICENSE create mode 100644 providers/Flipt/README.md create mode 100644 providers/Flipt/composer.json create mode 100644 providers/Flipt/phpunit.xml create mode 100644 providers/Flipt/src/Cache.php create mode 100644 providers/Flipt/src/FliptProvider.php create mode 100644 providers/Flipt/tests/FliptProviderTest.php diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..bde783d3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,51 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9000, + "pathMappings": { + "/app": "${workspaceFolder}/providers/Flipt" + }, + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 0, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } + }, + { + "name": "Launch Built-in web server", + "type": "php", + "request": "launch", + "runtimeArgs": [ + "-dxdebug.mode=debug", + "-dxdebug.start_with_request=yes", + "-S", + "localhost:0" + ], + "program": "", + "cwd": "${workspaceRoot}", + "port": 9003, + "serverReadyAction": { + "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", + "uriFormat": "http://localhost:%s", + "action": "openExternally" + } + } + ] +} \ No newline at end of file diff --git a/providers/Flipt/.gitignore b/providers/Flipt/.gitignore new file mode 100644 index 00000000..2606a2d0 --- /dev/null +++ b/providers/Flipt/.gitignore @@ -0,0 +1,4 @@ +composer.lock +composer.phar +/vendor/ +.phpunit.result.cache \ No newline at end of file diff --git a/providers/Flipt/LICENSE b/providers/Flipt/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/providers/Flipt/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/providers/Flipt/README.md b/providers/Flipt/README.md new file mode 100644 index 00000000..54e222da --- /dev/null +++ b/providers/Flipt/README.md @@ -0,0 +1,60 @@ +# FliptProvider for OpenFeature PHP SDK + +The `FliptProvider` is an integration for the [OpenFeature](https://github.com/open-feature/php-sdk) PHP SDK, allowing you to use [Flipt](https://flipt.io) as a feature flagging service. + +## Installation + +Before using the `FliptProvider`, you must have the OpenFeature PHP SDK installed. If you have not already installed the SDK, you can do so with Composer: + +```bash +composer require open-feature/php-sdk +``` + +Next, include the FliptProvider in your project: + +```bash +composer require open-feature/flipt-provider +``` + +## Usage +To use the FliptProvider, you'll need to create an instance of it by passing your Flipt host, API token, and namespace (if applicable). Then, set this provider for OpenFeature. + +Here's a quick example: + +```php + +use OpenFeature\OpenFeature; +use OpenFeature\Providers\Flipt\FliptProvider; + +// Replace these with your actual Flipt host and API token +$host = 'http://your-flipt-instance'; +$apiToken = 'your-api-token'; +$namespace = 'namespace'; + +$provider = new FliptProvider($host, $apiToken, $namespace); + +OpenFeature::setProvider($provider); + +// Now you can evaluate your feature flags as follows +$booleanFlagValue = OpenFeature::getBooleanValue('your-boolean-flag-key', false); +$stringFlagValue = OpenFeature::getStringValue('your-string-flag-key', 'default-value'); +$integerFlagValue = OpenFeature::getIntegerValue('your-integer-flag-key', 0); +$floatFlagValue = OpenFeature::getFloatValue('your-float-flag-key', 0.0); +$objectFlagValue = OpenFeature::getObjectValue('your-object-flag-key', ['default' => 'value']); +``` + +### Caching + +If you like to cache the feature flag results you can pass a [PSR-16](https://www.php-fig.org/psr/psr-16/) compatible cache storage into the provider constructor like this: + +```php + +$cache = ''; +$provider = new FliptProvider($host, $apiToken, $namespace, $cache); + +OpenFeature::setProvider($provider); + + +// to clear the cache you can call +$provider->cacheClear(); +``` diff --git a/providers/Flipt/composer.json b/providers/Flipt/composer.json new file mode 100644 index 00000000..76614a6b --- /dev/null +++ b/providers/Flipt/composer.json @@ -0,0 +1,34 @@ +{ + "name": "open-feature/flipt-provider", + "description": "The flipt.io provider package for open-feature", + "keywords": [ + "flipt", + "open-feature", + "feature flags" + ], + "homepage": "https://github.com/legoheld/flipt-provider", + "require": { + "php": ">=8.0", + "open-feature/sdk": "^2.0", + "flipt-io/flipt": "^0.0.1", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpunit/php-code-coverage": "^10", + "phpunit/phpunit": "^10.4" + }, + "autoload": { + "psr-4": { + "OpenFeature\\Providers\\Flipt\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Providers\\Flagd\\Test\\": "tests/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/providers/Flipt/phpunit.xml b/providers/Flipt/phpunit.xml new file mode 100644 index 00000000..bda511f3 --- /dev/null +++ b/providers/Flipt/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests + + + \ No newline at end of file diff --git a/providers/Flipt/src/Cache.php b/providers/Flipt/src/Cache.php new file mode 100644 index 00000000..850b587f --- /dev/null +++ b/providers/Flipt/src/Cache.php @@ -0,0 +1,64 @@ +cache = $cache; + } + + + + /** + * Retrievies a value from the cache + */ + public function get( $key ) { + + if( empty( $this->cache ) ) return null; + + + $entries = $this->cache->get( self::CACHE_KEY, [] ); + + if( array_key_exists( $key, $entries ) ) return $entries[ $key ]; + + return null; + } + + + /** + * Sets the $value into the cache + */ + public function set( $key, $value ) { + + if( empty( $this->cache ) ) return; + + $entries = $this->cache->get( self::CACHE_KEY, []); + $entries[ $key ] = $value; + + $this->cache->set( self::CACHE_KEY, $entries ); + } + + + /** + * Clears the cached records + */ + public function clear() { + if( empty( $this->cache ) ) return; + $this->cache->delete( self::CACHE_KEY ); + } + + + public function key( array $params ) { + return md5( json_encode( $params ) ); + } +} + diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php new file mode 100644 index 00000000..a9caf435 --- /dev/null +++ b/providers/Flipt/src/FliptProvider.php @@ -0,0 +1,137 @@ +client = ( is_string( $hostOrClient ) ) ? new FliptClient( $hostOrClient, $apiToken, $namespace ) : $hostOrClient; + $this->cache = new Cache( $cache ); + } + + + public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::BOOLEAN, $defaultValue, $context); + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::STRING, $defaultValue, $context); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::INTEGER, $defaultValue, $context); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::FLOAT, $defaultValue, $context); + } + + /** + * @param mixed[] $defaultValue + */ + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::OBJECT, $defaultValue, $context); + } + + + /** + * Clears the cache of all requests + */ + public function clearCache() { + $this->cache->clear(); + } + + /** + * @param bool|string|int|float|mixed[] $defaultValue + */ + private function resolveValue(string $flagKey, string $flagType, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + + // check null context + if( empty( $context ) ) { + $attributes = []; + $id = null; + } else { + $attributes = $context->getAttributes()->toArray(); + $id = $context->getTargetingKey(); + } + + + // check if cache has already result + $cacheKey = $this->cache->key( [ 'flag' => $flagKey, 'type' => $flagType, 'default' => $defaultValue, 'context' => $attributes, 'key' => $id ] ); + $cached = $this->cache->get( $cacheKey ); + if( isset( $cached ) ) return $cached; + + + // booleans need a dedicated function + if( $flagType == FlagValueType::BOOLEAN ) { + $result = $this->client->boolean( $flagKey, $attributes, $id ); + } else { + $result = $this->client->variant( $flagKey, $attributes, $id ); + } + + + // there is a match + // not sure yet as the variant result has a getMatch() but not the boolean result. + if( $result->getReason() == 'MATCH_EVALUATION_REASON' || $result->getReason() == "DEFAULT_EVALUATION_REASON" ) { + $result = ResolutionDetailsFactory::fromSuccess( $this->castResult( $result, $flagType ) ); + } else { + $result = (new ResolutionDetailsBuilder()) + ->withValue( $defaultValue ) + ->withError( + // not sure if thie reason to error mapping is correct + new ResolutionError(ErrorCode::GENERAL(), $result->getReason() ), + ) + ->build(); + } + + // write result into cache + $this->cache->set( $cacheKey, $result ); + + return $result; + } + + + + private function castResult( VariantEvaluationResult|BooleanEvaluationResult $result, string $type ) { + switch ($type) { + case FlagValueType::BOOLEAN: + return filter_var($result->getEnabled(), FILTER_VALIDATE_BOOLEAN); + case FlagValueType::FLOAT: + return (float) $result->getVariantKey(); + case FlagValueType::INTEGER: + return (int) $result->getVariantKey(); + case FlagValueType::OBJECT: + return json_decode( $result->getVariantAttachment(), true); + case FlagValueType::STRING: + return $result->getVariantKey(); + default: + return null; + } + } + +} \ No newline at end of file diff --git a/providers/Flipt/tests/FliptProviderTest.php b/providers/Flipt/tests/FliptProviderTest.php new file mode 100644 index 00000000..1006866d --- /dev/null +++ b/providers/Flipt/tests/FliptProviderTest.php @@ -0,0 +1,147 @@ +mockClient = Mockery::mock(); + $this->provider = new FliptProvider( $this->mockClient ); + } + + + protected function tearDown(): void + { + Mockery::close(); + } + + public function testBoolean() + { + $this->mockClient->shouldReceive( 'boolean') + ->withArgs( function( $flag, $context, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $context, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + + $result = $this->provider->resolveBooleanValue( 'flag', false, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), true ); + + } + + public function testInteger() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); + + $result = $this->provider->resolveIntegerValue( 'flag', 10, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), 20 ); + + } + + public function testFloat() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); + + $result = $this->provider->resolveFloatValue( 'flag', 0.1111, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), 0.2345 ); + + } + + public function testString() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + + $result = $this->provider->resolveStringValue( 'flag', 'base', new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), 'My string' ); + + } + + + public function testObject() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + + $result = $this->provider->resolveObjectValue( 'flag', [], new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), [ "json" => 1 ] ); + + } + + + public function testCache() + { + $this->mockClient->shouldReceive( 'boolean')->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + + $cache = Mockery::mock( CacheInterface::class ); + $cache->shouldReceive( 'get')->with( 'openfeature-flipt', [] )->andReturn( [] ); + $cache->shouldReceive( 'set')->withArgs( function( $key, $content ) { + $this->assertEquals( $key, 'openfeature-flipt' ); + $this->assertArrayHasKey( '21349ebf7629df2cb162aa15b2ec6335', $content ); + $this->assertInstanceOf( ResolutionDetails::class, $content['21349ebf7629df2cb162aa15b2ec6335' ]); + return true; + }); + + // create new provider with mocked cache + $provider = new FliptProvider( $this->mockClient, 'token', 'ns', $cache ); + + $result = $provider->resolveBooleanValue( 'flag1', false ); + $this->assertEquals( $result->getValue(), true ); + } + + +} From ebc9e75c8e41aedfd89d95d862edd612df3cac6e Mon Sep 17 00:00:00 2001 From: legoheld Date: Thu, 23 Nov 2023 09:08:40 +0100 Subject: [PATCH 02/18] docs: Change to MIT license Signed-off-by: legoheld --- providers/Flipt/LICENSE | 222 ++++------------------------------------ 1 file changed, 21 insertions(+), 201 deletions(-) diff --git a/providers/Flipt/LICENSE b/providers/Flipt/LICENSE index 261eeb9e..4954f661 100644 --- a/providers/Flipt/LICENSE +++ b/providers/Flipt/LICENSE @@ -1,201 +1,21 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2023 Flipt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From ab6b04ce0f97d8731032077802cd61f93ddd877b Mon Sep 17 00:00:00 2001 From: legoheld Date: Thu, 23 Nov 2023 09:13:16 +0100 Subject: [PATCH 03/18] fix: Remove unnecessary launch.json Signed-off-by: legoheld --- .vscode/launch.json | 51 --------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index bde783d3..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Listen for Xdebug", - "type": "php", - "request": "launch", - "port": 9000, - "pathMappings": { - "/app": "${workspaceFolder}/providers/Flipt" - }, - }, - { - "name": "Launch currently open script", - "type": "php", - "request": "launch", - "program": "${file}", - "cwd": "${fileDirname}", - "port": 0, - "runtimeArgs": [ - "-dxdebug.start_with_request=yes" - ], - "env": { - "XDEBUG_MODE": "debug,develop", - "XDEBUG_CONFIG": "client_port=${port}" - } - }, - { - "name": "Launch Built-in web server", - "type": "php", - "request": "launch", - "runtimeArgs": [ - "-dxdebug.mode=debug", - "-dxdebug.start_with_request=yes", - "-S", - "localhost:0" - ], - "program": "", - "cwd": "${workspaceRoot}", - "port": 9003, - "serverReadyAction": { - "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", - "uriFormat": "http://localhost:%s", - "action": "openExternally" - } - } - ] -} \ No newline at end of file From aff16ff3c90054bf95020fffd868618a5b2cbf3a Mon Sep 17 00:00:00 2001 From: legoheld Date: Thu, 23 Nov 2023 09:15:11 +0100 Subject: [PATCH 04/18] docs: Revert back to apache2 license according to @beeme1mr Signed-off-by: legoheld --- providers/Flipt/LICENSE | 222 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 201 insertions(+), 21 deletions(-) diff --git a/providers/Flipt/LICENSE b/providers/Flipt/LICENSE index 4954f661..261eeb9e 100644 --- a/providers/Flipt/LICENSE +++ b/providers/Flipt/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2023 Flipt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 80e0e2c253e8441199dec9977dcebd25f05755af Mon Sep 17 00:00:00 2001 From: legoheld Date: Mon, 27 Nov 2023 10:40:50 +0100 Subject: [PATCH 05/18] feat: add CacheProvider to extract caching funcionality from FliptProvider fix: Add ResponseReasons constants fix: proper FliptProvider constructor type fix: Add ksort for context attributes --- providers/Flipt/src/Cache.php | 64 -------- providers/Flipt/src/CacheProvider.php | 162 ++++++++++++++++++++ providers/Flipt/src/FliptProvider.php | 23 +-- providers/Flipt/src/ResponseReasons.php | 15 ++ providers/Flipt/tests/CacheProviderTest.php | 60 ++++++++ providers/Flipt/tests/FliptProviderTest.php | 38 ++--- 6 files changed, 250 insertions(+), 112 deletions(-) delete mode 100644 providers/Flipt/src/Cache.php create mode 100644 providers/Flipt/src/CacheProvider.php create mode 100644 providers/Flipt/src/ResponseReasons.php create mode 100644 providers/Flipt/tests/CacheProviderTest.php diff --git a/providers/Flipt/src/Cache.php b/providers/Flipt/src/Cache.php deleted file mode 100644 index 850b587f..00000000 --- a/providers/Flipt/src/Cache.php +++ /dev/null @@ -1,64 +0,0 @@ -cache = $cache; - } - - - - /** - * Retrievies a value from the cache - */ - public function get( $key ) { - - if( empty( $this->cache ) ) return null; - - - $entries = $this->cache->get( self::CACHE_KEY, [] ); - - if( array_key_exists( $key, $entries ) ) return $entries[ $key ]; - - return null; - } - - - /** - * Sets the $value into the cache - */ - public function set( $key, $value ) { - - if( empty( $this->cache ) ) return; - - $entries = $this->cache->get( self::CACHE_KEY, []); - $entries[ $key ] = $value; - - $this->cache->set( self::CACHE_KEY, $entries ); - } - - - /** - * Clears the cached records - */ - public function clear() { - if( empty( $this->cache ) ) return; - $this->cache->delete( self::CACHE_KEY ); - } - - - public function key( array $params ) { - return md5( json_encode( $params ) ); - } -} - diff --git a/providers/Flipt/src/CacheProvider.php b/providers/Flipt/src/CacheProvider.php new file mode 100644 index 00000000..ae7aaf3e --- /dev/null +++ b/providers/Flipt/src/CacheProvider.php @@ -0,0 +1,162 @@ +provider = $provider; + $this->storage = $storage; + $this->key = $key; + } + + + public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveBooleanValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveStringValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveIntegerValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveFloatValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveObjectValue($flagKey, $defaultValue, $context) + ); + } + + + protected function cacheCheck(string $key, Closure $next) + { + + $cached = $this->get($key); + if (isset($cached)) return $cached; + + $result = $next(); + + $this->set($key, $result); + + return $result; + } + + + /** + * Retrievies a value from the cache + */ + protected function get($key) + { + + $entries = $this->storage->get($this->key, []); + + if (array_key_exists($key, $entries)) return $entries[$key]; + + return null; + } + + + /** + * Sets the $value into the cache + */ + protected function set($key, $value) + { + + + $entries = $this->storage->get($this->key, []); + $entries[$key] = $value; + + $this->storage->set($this->key, $entries); + } + + + + /** + * Clears the cached records + */ + public function clear() + { + $this->storage->delete($this->key); + } + + + + protected function hash(string $flag, mixed $default, ?EvaluationContext $context) + { + + // check null context + if (empty($context)) { + $attributes = []; + $id = null; + } else { + $attributes = $context->getAttributes()->toArray(); + $id = $context->getTargetingKey(); + } + + // generate hash on request arguments + return md5(json_encode([ + 'flag' => $flag, + 'default' => $default, + 'id' => $id, 'context' => $this->sortArray($attributes) + ])); + } + + + + protected function sortArray(array $array) + { + $sortedArray = []; + + foreach ($array as $key => $value) { + if (is_array($value)) { + // Recursively sort nested arrays + $value = $this->sortArray($value); + } + $sortedArray[$key] = $value; + } + + // Sort the array by keys + ksort($sortedArray); + + return $sortedArray; + } +} diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php index a9caf435..5a4dedb2 100644 --- a/providers/Flipt/src/FliptProvider.php +++ b/providers/Flipt/src/FliptProvider.php @@ -14,18 +14,15 @@ use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; use OpenFeature\interfaces\provider\ResolutionDetails; -use Psr\SimpleCache\CacheInterface; class FliptProvider extends AbstractProvider implements Provider { protected const NAME = 'FliptProvider'; protected $client; - protected Cache $cache; - public function __construct( mixed $hostOrClient, string $apiToken = '', string $namespace = '', CacheInterface $cache = null ) { + public function __construct( string|FliptClient $hostOrClient, string $apiToken = '', string $namespace = '' ) { $this->client = ( is_string( $hostOrClient ) ) ? new FliptClient( $hostOrClient, $apiToken, $namespace ) : $hostOrClient; - $this->cache = new Cache( $cache ); } @@ -58,12 +55,6 @@ public function resolveObjectValue(string $flagKey, array $defaultValue, ?Evalua } - /** - * Clears the cache of all requests - */ - public function clearCache() { - $this->cache->clear(); - } /** * @param bool|string|int|float|mixed[] $defaultValue @@ -80,13 +71,6 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV $id = $context->getTargetingKey(); } - - // check if cache has already result - $cacheKey = $this->cache->key( [ 'flag' => $flagKey, 'type' => $flagType, 'default' => $defaultValue, 'context' => $attributes, 'key' => $id ] ); - $cached = $this->cache->get( $cacheKey ); - if( isset( $cached ) ) return $cached; - - // booleans need a dedicated function if( $flagType == FlagValueType::BOOLEAN ) { $result = $this->client->boolean( $flagKey, $attributes, $id ); @@ -97,7 +81,7 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV // there is a match // not sure yet as the variant result has a getMatch() but not the boolean result. - if( $result->getReason() == 'MATCH_EVALUATION_REASON' || $result->getReason() == "DEFAULT_EVALUATION_REASON" ) { + if( $result->getReason() == ResponseReasons::MATCH_EVALUATION_REASON || $result->getReason() == ResponseReasons::DEFAULT_EVALUATION_REASON ) { $result = ResolutionDetailsFactory::fromSuccess( $this->castResult( $result, $flagType ) ); } else { $result = (new ResolutionDetailsBuilder()) @@ -109,9 +93,6 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV ->build(); } - // write result into cache - $this->cache->set( $cacheKey, $result ); - return $result; } diff --git a/providers/Flipt/src/ResponseReasons.php b/providers/Flipt/src/ResponseReasons.php new file mode 100644 index 00000000..de721436 --- /dev/null +++ b/providers/Flipt/src/ResponseReasons.php @@ -0,0 +1,15 @@ +mockProvider = Mockery::mock( FliptProvider::class ); + $this->storage = Mockery::mock( CacheInterface::class ); + $this->provider = new CacheProvider( $this->mockProvider, $this->storage ); + } + + + protected function tearDown(): void + { + Mockery::close(); + } + + public function testSet() + { + + $context = new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ); + + $this->storage->shouldReceive( 'get')->with( 'open-feature', [] )->andReturn( [] ); + $this->storage->shouldReceive( 'set')->withArgs( function( $key, $content ) { + $this->assertEquals( $key, 'open-feature' ); + $this->assertArrayHasKey( 'fa89ea8c3b16386166360dd163f10e6d', $content ); + $this->assertInstanceOf( ResolutionDetails::class, $content['fa89ea8c3b16386166360dd163f10e6d' ]); + return true; + }); + $this->mockProvider->shouldReceive( 'resolveBooleanValue') + ->with( 'flag', true, $context ) + ->andReturn( ResolutionDetailsFactory::fromSuccess( true ) ); + + $result = $this->provider->resolveBooleanValue( 'flag', true, $context ); + + $this->assertEquals( $result->getValue(), true ); + } +} \ No newline at end of file diff --git a/providers/Flipt/tests/FliptProviderTest.php b/providers/Flipt/tests/FliptProviderTest.php index 1006866d..d7772806 100644 --- a/providers/Flipt/tests/FliptProviderTest.php +++ b/providers/Flipt/tests/FliptProviderTest.php @@ -2,6 +2,7 @@ namespace Tests; +use Flipt\Client\FliptClient; use Flipt\Models\DefaultBooleanEvaluationResult; use Flipt\Models\DefaultVariantEvaluationResult; use Mockery; @@ -10,18 +11,19 @@ use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\implementation\provider\ResolutionDetails; use OpenFeature\Providers\Flipt\FliptProvider; +use OpenFeature\Providers\Flipt\ResponseReasons; use PHPUnit\Framework\TestCase; use Psr\SimpleCache\CacheInterface; class FliptProviderTest extends TestCase { - protected MockInterface $mockClient; + protected FliptClient&MockInterface $mockClient; protected FliptProvider $provider; protected function setUp(): void { - $this->mockClient = Mockery::mock(); + $this->mockClient = Mockery::mock( 'overload:' . FliptClient::class ); $this->provider = new FliptProvider( $this->mockClient ); } @@ -31,6 +33,7 @@ protected function tearDown(): void Mockery::close(); } + public function testBoolean() { $this->mockClient->shouldReceive( 'boolean') @@ -40,7 +43,7 @@ public function testBoolean() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + ->andReturn( new DefaultBooleanEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245' ) ); $result = $this->provider->resolveBooleanValue( 'flag', false, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -58,7 +61,7 @@ public function testInteger() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); $result = $this->provider->resolveIntegerValue( 'flag', 10, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -76,7 +79,7 @@ public function testFloat() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); $result = $this->provider->resolveFloatValue( 'flag', 0.1111, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -94,7 +97,7 @@ public function testString() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveStringValue( 'flag', 'base', new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -113,7 +116,7 @@ public function testObject() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveObjectValue( 'flag', [], new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -122,26 +125,7 @@ public function testObject() } - - public function testCache() - { - $this->mockClient->shouldReceive( 'boolean')->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); - - $cache = Mockery::mock( CacheInterface::class ); - $cache->shouldReceive( 'get')->with( 'openfeature-flipt', [] )->andReturn( [] ); - $cache->shouldReceive( 'set')->withArgs( function( $key, $content ) { - $this->assertEquals( $key, 'openfeature-flipt' ); - $this->assertArrayHasKey( '21349ebf7629df2cb162aa15b2ec6335', $content ); - $this->assertInstanceOf( ResolutionDetails::class, $content['21349ebf7629df2cb162aa15b2ec6335' ]); - return true; - }); - - // create new provider with mocked cache - $provider = new FliptProvider( $this->mockClient, 'token', 'ns', $cache ); - - $result = $provider->resolveBooleanValue( 'flag1', false ); - $this->assertEquals( $result->getValue(), true ); - } + } From db2d4bfa099b8aae7ead53ea0cc9e53bda2b1c82 Mon Sep 17 00:00:00 2001 From: legoheld Date: Mon, 27 Nov 2023 10:40:50 +0100 Subject: [PATCH 06/18] feat: add CacheProvider to extract caching funcionality from FliptProvider fix: Add ResponseReasons constants fix: proper FliptProvider constructor type fix: Add ksort for context attributes Signed-off-by: legoheld --- providers/Flipt/src/Cache.php | 64 -------- providers/Flipt/src/CacheProvider.php | 162 ++++++++++++++++++++ providers/Flipt/src/FliptProvider.php | 23 +-- providers/Flipt/src/ResponseReasons.php | 15 ++ providers/Flipt/tests/CacheProviderTest.php | 60 ++++++++ providers/Flipt/tests/FliptProviderTest.php | 38 ++--- 6 files changed, 250 insertions(+), 112 deletions(-) delete mode 100644 providers/Flipt/src/Cache.php create mode 100644 providers/Flipt/src/CacheProvider.php create mode 100644 providers/Flipt/src/ResponseReasons.php create mode 100644 providers/Flipt/tests/CacheProviderTest.php diff --git a/providers/Flipt/src/Cache.php b/providers/Flipt/src/Cache.php deleted file mode 100644 index 850b587f..00000000 --- a/providers/Flipt/src/Cache.php +++ /dev/null @@ -1,64 +0,0 @@ -cache = $cache; - } - - - - /** - * Retrievies a value from the cache - */ - public function get( $key ) { - - if( empty( $this->cache ) ) return null; - - - $entries = $this->cache->get( self::CACHE_KEY, [] ); - - if( array_key_exists( $key, $entries ) ) return $entries[ $key ]; - - return null; - } - - - /** - * Sets the $value into the cache - */ - public function set( $key, $value ) { - - if( empty( $this->cache ) ) return; - - $entries = $this->cache->get( self::CACHE_KEY, []); - $entries[ $key ] = $value; - - $this->cache->set( self::CACHE_KEY, $entries ); - } - - - /** - * Clears the cached records - */ - public function clear() { - if( empty( $this->cache ) ) return; - $this->cache->delete( self::CACHE_KEY ); - } - - - public function key( array $params ) { - return md5( json_encode( $params ) ); - } -} - diff --git a/providers/Flipt/src/CacheProvider.php b/providers/Flipt/src/CacheProvider.php new file mode 100644 index 00000000..ae7aaf3e --- /dev/null +++ b/providers/Flipt/src/CacheProvider.php @@ -0,0 +1,162 @@ +provider = $provider; + $this->storage = $storage; + $this->key = $key; + } + + + public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveBooleanValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveStringValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveIntegerValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveFloatValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveObjectValue($flagKey, $defaultValue, $context) + ); + } + + + protected function cacheCheck(string $key, Closure $next) + { + + $cached = $this->get($key); + if (isset($cached)) return $cached; + + $result = $next(); + + $this->set($key, $result); + + return $result; + } + + + /** + * Retrievies a value from the cache + */ + protected function get($key) + { + + $entries = $this->storage->get($this->key, []); + + if (array_key_exists($key, $entries)) return $entries[$key]; + + return null; + } + + + /** + * Sets the $value into the cache + */ + protected function set($key, $value) + { + + + $entries = $this->storage->get($this->key, []); + $entries[$key] = $value; + + $this->storage->set($this->key, $entries); + } + + + + /** + * Clears the cached records + */ + public function clear() + { + $this->storage->delete($this->key); + } + + + + protected function hash(string $flag, mixed $default, ?EvaluationContext $context) + { + + // check null context + if (empty($context)) { + $attributes = []; + $id = null; + } else { + $attributes = $context->getAttributes()->toArray(); + $id = $context->getTargetingKey(); + } + + // generate hash on request arguments + return md5(json_encode([ + 'flag' => $flag, + 'default' => $default, + 'id' => $id, 'context' => $this->sortArray($attributes) + ])); + } + + + + protected function sortArray(array $array) + { + $sortedArray = []; + + foreach ($array as $key => $value) { + if (is_array($value)) { + // Recursively sort nested arrays + $value = $this->sortArray($value); + } + $sortedArray[$key] = $value; + } + + // Sort the array by keys + ksort($sortedArray); + + return $sortedArray; + } +} diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php index a9caf435..5a4dedb2 100644 --- a/providers/Flipt/src/FliptProvider.php +++ b/providers/Flipt/src/FliptProvider.php @@ -14,18 +14,15 @@ use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; use OpenFeature\interfaces\provider\ResolutionDetails; -use Psr\SimpleCache\CacheInterface; class FliptProvider extends AbstractProvider implements Provider { protected const NAME = 'FliptProvider'; protected $client; - protected Cache $cache; - public function __construct( mixed $hostOrClient, string $apiToken = '', string $namespace = '', CacheInterface $cache = null ) { + public function __construct( string|FliptClient $hostOrClient, string $apiToken = '', string $namespace = '' ) { $this->client = ( is_string( $hostOrClient ) ) ? new FliptClient( $hostOrClient, $apiToken, $namespace ) : $hostOrClient; - $this->cache = new Cache( $cache ); } @@ -58,12 +55,6 @@ public function resolveObjectValue(string $flagKey, array $defaultValue, ?Evalua } - /** - * Clears the cache of all requests - */ - public function clearCache() { - $this->cache->clear(); - } /** * @param bool|string|int|float|mixed[] $defaultValue @@ -80,13 +71,6 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV $id = $context->getTargetingKey(); } - - // check if cache has already result - $cacheKey = $this->cache->key( [ 'flag' => $flagKey, 'type' => $flagType, 'default' => $defaultValue, 'context' => $attributes, 'key' => $id ] ); - $cached = $this->cache->get( $cacheKey ); - if( isset( $cached ) ) return $cached; - - // booleans need a dedicated function if( $flagType == FlagValueType::BOOLEAN ) { $result = $this->client->boolean( $flagKey, $attributes, $id ); @@ -97,7 +81,7 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV // there is a match // not sure yet as the variant result has a getMatch() but not the boolean result. - if( $result->getReason() == 'MATCH_EVALUATION_REASON' || $result->getReason() == "DEFAULT_EVALUATION_REASON" ) { + if( $result->getReason() == ResponseReasons::MATCH_EVALUATION_REASON || $result->getReason() == ResponseReasons::DEFAULT_EVALUATION_REASON ) { $result = ResolutionDetailsFactory::fromSuccess( $this->castResult( $result, $flagType ) ); } else { $result = (new ResolutionDetailsBuilder()) @@ -109,9 +93,6 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV ->build(); } - // write result into cache - $this->cache->set( $cacheKey, $result ); - return $result; } diff --git a/providers/Flipt/src/ResponseReasons.php b/providers/Flipt/src/ResponseReasons.php new file mode 100644 index 00000000..de721436 --- /dev/null +++ b/providers/Flipt/src/ResponseReasons.php @@ -0,0 +1,15 @@ +mockProvider = Mockery::mock( FliptProvider::class ); + $this->storage = Mockery::mock( CacheInterface::class ); + $this->provider = new CacheProvider( $this->mockProvider, $this->storage ); + } + + + protected function tearDown(): void + { + Mockery::close(); + } + + public function testSet() + { + + $context = new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ); + + $this->storage->shouldReceive( 'get')->with( 'open-feature', [] )->andReturn( [] ); + $this->storage->shouldReceive( 'set')->withArgs( function( $key, $content ) { + $this->assertEquals( $key, 'open-feature' ); + $this->assertArrayHasKey( 'fa89ea8c3b16386166360dd163f10e6d', $content ); + $this->assertInstanceOf( ResolutionDetails::class, $content['fa89ea8c3b16386166360dd163f10e6d' ]); + return true; + }); + $this->mockProvider->shouldReceive( 'resolveBooleanValue') + ->with( 'flag', true, $context ) + ->andReturn( ResolutionDetailsFactory::fromSuccess( true ) ); + + $result = $this->provider->resolveBooleanValue( 'flag', true, $context ); + + $this->assertEquals( $result->getValue(), true ); + } +} \ No newline at end of file diff --git a/providers/Flipt/tests/FliptProviderTest.php b/providers/Flipt/tests/FliptProviderTest.php index 1006866d..d7772806 100644 --- a/providers/Flipt/tests/FliptProviderTest.php +++ b/providers/Flipt/tests/FliptProviderTest.php @@ -2,6 +2,7 @@ namespace Tests; +use Flipt\Client\FliptClient; use Flipt\Models\DefaultBooleanEvaluationResult; use Flipt\Models\DefaultVariantEvaluationResult; use Mockery; @@ -10,18 +11,19 @@ use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\implementation\provider\ResolutionDetails; use OpenFeature\Providers\Flipt\FliptProvider; +use OpenFeature\Providers\Flipt\ResponseReasons; use PHPUnit\Framework\TestCase; use Psr\SimpleCache\CacheInterface; class FliptProviderTest extends TestCase { - protected MockInterface $mockClient; + protected FliptClient&MockInterface $mockClient; protected FliptProvider $provider; protected function setUp(): void { - $this->mockClient = Mockery::mock(); + $this->mockClient = Mockery::mock( 'overload:' . FliptClient::class ); $this->provider = new FliptProvider( $this->mockClient ); } @@ -31,6 +33,7 @@ protected function tearDown(): void Mockery::close(); } + public function testBoolean() { $this->mockClient->shouldReceive( 'boolean') @@ -40,7 +43,7 @@ public function testBoolean() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + ->andReturn( new DefaultBooleanEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245' ) ); $result = $this->provider->resolveBooleanValue( 'flag', false, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -58,7 +61,7 @@ public function testInteger() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); $result = $this->provider->resolveIntegerValue( 'flag', 10, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -76,7 +79,7 @@ public function testFloat() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); $result = $this->provider->resolveFloatValue( 'flag', 0.1111, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -94,7 +97,7 @@ public function testString() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveStringValue( 'flag', 'base', new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -113,7 +116,7 @@ public function testObject() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveObjectValue( 'flag', [], new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -122,26 +125,7 @@ public function testObject() } - - public function testCache() - { - $this->mockClient->shouldReceive( 'boolean')->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); - - $cache = Mockery::mock( CacheInterface::class ); - $cache->shouldReceive( 'get')->with( 'openfeature-flipt', [] )->andReturn( [] ); - $cache->shouldReceive( 'set')->withArgs( function( $key, $content ) { - $this->assertEquals( $key, 'openfeature-flipt' ); - $this->assertArrayHasKey( '21349ebf7629df2cb162aa15b2ec6335', $content ); - $this->assertInstanceOf( ResolutionDetails::class, $content['21349ebf7629df2cb162aa15b2ec6335' ]); - return true; - }); - - // create new provider with mocked cache - $provider = new FliptProvider( $this->mockClient, 'token', 'ns', $cache ); - - $result = $provider->resolveBooleanValue( 'flag1', false ); - $this->assertEquals( $result->getValue(), true ); - } + } From 3c7d2f65691bed2db2916a6d940d6a33593d9f80 Mon Sep 17 00:00:00 2001 From: legoheld Date: Mon, 27 Nov 2023 10:45:53 +0100 Subject: [PATCH 07/18] chore: fix readme Signed-off-by: legoheld --- providers/Flipt/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/Flipt/README.md b/providers/Flipt/README.md index 54e222da..8667f6fe 100644 --- a/providers/Flipt/README.md +++ b/providers/Flipt/README.md @@ -36,11 +36,11 @@ $provider = new FliptProvider($host, $apiToken, $namespace); OpenFeature::setProvider($provider); // Now you can evaluate your feature flags as follows -$booleanFlagValue = OpenFeature::getBooleanValue('your-boolean-flag-key', false); -$stringFlagValue = OpenFeature::getStringValue('your-string-flag-key', 'default-value'); -$integerFlagValue = OpenFeature::getIntegerValue('your-integer-flag-key', 0); -$floatFlagValue = OpenFeature::getFloatValue('your-float-flag-key', 0.0); -$objectFlagValue = OpenFeature::getObjectValue('your-object-flag-key', ['default' => 'value']); +$booleanFlagValue = OpenFeatureAPI::getInstance()->getClient()->getBooleanValue('your-boolean-flag-key', false); +$stringFlagValue = OpenFeatureAPI::getInstance()->getClient()->getStringValue('your-string-flag-key', 'default-value'); +$integerFlagValue = OpenFeatureAPI::getInstance()->getClient()->getIntegerValue('your-integer-flag-key', 0); +$floatFlagValue = OpenFeatureAPI::getInstance()->getClient()->getFloatValue('your-float-flag-key', 0.0); +$objectFlagValue = OpenFeatureAPI::getInstance()->getClient()->getObjectValue('your-object-flag-key', ['default' => 'value']); ``` ### Caching From 07f274b848e5704ac211291c645d5a7e553d603e Mon Sep 17 00:00:00 2001 From: legoheld Date: Wed, 29 Nov 2023 09:00:48 +0100 Subject: [PATCH 08/18] chore: Update to client lib constants Signed-off-by: legoheld --- providers/Flipt/composer.json | 2 +- providers/Flipt/src/FliptProvider.php | 1 + providers/Flipt/src/ResponseReasons.php | 15 --------------- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 providers/Flipt/src/ResponseReasons.php diff --git a/providers/Flipt/composer.json b/providers/Flipt/composer.json index 76614a6b..93e9f156 100644 --- a/providers/Flipt/composer.json +++ b/providers/Flipt/composer.json @@ -10,7 +10,7 @@ "require": { "php": ">=8.0", "open-feature/sdk": "^2.0", - "flipt-io/flipt": "^0.0.1", + "flipt-io/flipt": "^0.2.0", "psr/simple-cache": "^3.0.0" }, "require-dev": { diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php index 5a4dedb2..5eb9288c 100644 --- a/providers/Flipt/src/FliptProvider.php +++ b/providers/Flipt/src/FliptProvider.php @@ -4,6 +4,7 @@ use Flipt\Client\FliptClient; use Flipt\Models\BooleanEvaluationResult; +use Flipt\Models\ResponseReasons; use Flipt\Models\VariantEvaluationResult; use OpenFeature\implementation\provider\AbstractProvider; use OpenFeature\implementation\provider\ResolutionDetailsBuilder; diff --git a/providers/Flipt/src/ResponseReasons.php b/providers/Flipt/src/ResponseReasons.php deleted file mode 100644 index de721436..00000000 --- a/providers/Flipt/src/ResponseReasons.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Wed, 1 Nov 2023 17:40:44 -0400 Subject: [PATCH 09/18] docs: update README.md to include integrations terminology Signed-off-by: Tom Carrio Signed-off-by: legoheld --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0946782f..42afa3a0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Overview -The `php-contrib-sdk` repository is a monorepository containing various providers and hooks for OpenFeature's PHP SDK. Packages include: +The `php-contrib-sdk` repository is a monorepository containing various providers, hooks, and other integrations for OpenFeature's PHP SDK. Packages include: - Providers - [Flagd](./providers/Flagd/README.md) From 5ec4cdfa314e3ff1376d00ba9e207001723d7d1b Mon Sep 17 00:00:00 2001 From: legoheld Date: Wed, 22 Nov 2023 09:41:27 +0100 Subject: [PATCH 10/18] feat: Add flipt provider with PSR-16 caching support Signed-off-by: legoheld --- .vscode/launch.json | 51 +++++ providers/Flipt/.gitignore | 4 + providers/Flipt/LICENSE | 201 ++++++++++++++++++++ providers/Flipt/README.md | 60 ++++++ providers/Flipt/composer.json | 34 ++++ providers/Flipt/phpunit.xml | 11 ++ providers/Flipt/src/Cache.php | 64 +++++++ providers/Flipt/src/FliptProvider.php | 137 +++++++++++++ providers/Flipt/tests/FliptProviderTest.php | 147 ++++++++++++++ 9 files changed, 709 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 providers/Flipt/.gitignore create mode 100644 providers/Flipt/LICENSE create mode 100644 providers/Flipt/README.md create mode 100644 providers/Flipt/composer.json create mode 100644 providers/Flipt/phpunit.xml create mode 100644 providers/Flipt/src/Cache.php create mode 100644 providers/Flipt/src/FliptProvider.php create mode 100644 providers/Flipt/tests/FliptProviderTest.php diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..bde783d3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,51 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9000, + "pathMappings": { + "/app": "${workspaceFolder}/providers/Flipt" + }, + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 0, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } + }, + { + "name": "Launch Built-in web server", + "type": "php", + "request": "launch", + "runtimeArgs": [ + "-dxdebug.mode=debug", + "-dxdebug.start_with_request=yes", + "-S", + "localhost:0" + ], + "program": "", + "cwd": "${workspaceRoot}", + "port": 9003, + "serverReadyAction": { + "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", + "uriFormat": "http://localhost:%s", + "action": "openExternally" + } + } + ] +} \ No newline at end of file diff --git a/providers/Flipt/.gitignore b/providers/Flipt/.gitignore new file mode 100644 index 00000000..2606a2d0 --- /dev/null +++ b/providers/Flipt/.gitignore @@ -0,0 +1,4 @@ +composer.lock +composer.phar +/vendor/ +.phpunit.result.cache \ No newline at end of file diff --git a/providers/Flipt/LICENSE b/providers/Flipt/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/providers/Flipt/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/providers/Flipt/README.md b/providers/Flipt/README.md new file mode 100644 index 00000000..54e222da --- /dev/null +++ b/providers/Flipt/README.md @@ -0,0 +1,60 @@ +# FliptProvider for OpenFeature PHP SDK + +The `FliptProvider` is an integration for the [OpenFeature](https://github.com/open-feature/php-sdk) PHP SDK, allowing you to use [Flipt](https://flipt.io) as a feature flagging service. + +## Installation + +Before using the `FliptProvider`, you must have the OpenFeature PHP SDK installed. If you have not already installed the SDK, you can do so with Composer: + +```bash +composer require open-feature/php-sdk +``` + +Next, include the FliptProvider in your project: + +```bash +composer require open-feature/flipt-provider +``` + +## Usage +To use the FliptProvider, you'll need to create an instance of it by passing your Flipt host, API token, and namespace (if applicable). Then, set this provider for OpenFeature. + +Here's a quick example: + +```php + +use OpenFeature\OpenFeature; +use OpenFeature\Providers\Flipt\FliptProvider; + +// Replace these with your actual Flipt host and API token +$host = 'http://your-flipt-instance'; +$apiToken = 'your-api-token'; +$namespace = 'namespace'; + +$provider = new FliptProvider($host, $apiToken, $namespace); + +OpenFeature::setProvider($provider); + +// Now you can evaluate your feature flags as follows +$booleanFlagValue = OpenFeature::getBooleanValue('your-boolean-flag-key', false); +$stringFlagValue = OpenFeature::getStringValue('your-string-flag-key', 'default-value'); +$integerFlagValue = OpenFeature::getIntegerValue('your-integer-flag-key', 0); +$floatFlagValue = OpenFeature::getFloatValue('your-float-flag-key', 0.0); +$objectFlagValue = OpenFeature::getObjectValue('your-object-flag-key', ['default' => 'value']); +``` + +### Caching + +If you like to cache the feature flag results you can pass a [PSR-16](https://www.php-fig.org/psr/psr-16/) compatible cache storage into the provider constructor like this: + +```php + +$cache = ''; +$provider = new FliptProvider($host, $apiToken, $namespace, $cache); + +OpenFeature::setProvider($provider); + + +// to clear the cache you can call +$provider->cacheClear(); +``` diff --git a/providers/Flipt/composer.json b/providers/Flipt/composer.json new file mode 100644 index 00000000..76614a6b --- /dev/null +++ b/providers/Flipt/composer.json @@ -0,0 +1,34 @@ +{ + "name": "open-feature/flipt-provider", + "description": "The flipt.io provider package for open-feature", + "keywords": [ + "flipt", + "open-feature", + "feature flags" + ], + "homepage": "https://github.com/legoheld/flipt-provider", + "require": { + "php": ">=8.0", + "open-feature/sdk": "^2.0", + "flipt-io/flipt": "^0.0.1", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpunit/php-code-coverage": "^10", + "phpunit/phpunit": "^10.4" + }, + "autoload": { + "psr-4": { + "OpenFeature\\Providers\\Flipt\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Providers\\Flagd\\Test\\": "tests/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/providers/Flipt/phpunit.xml b/providers/Flipt/phpunit.xml new file mode 100644 index 00000000..bda511f3 --- /dev/null +++ b/providers/Flipt/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests + + + \ No newline at end of file diff --git a/providers/Flipt/src/Cache.php b/providers/Flipt/src/Cache.php new file mode 100644 index 00000000..850b587f --- /dev/null +++ b/providers/Flipt/src/Cache.php @@ -0,0 +1,64 @@ +cache = $cache; + } + + + + /** + * Retrievies a value from the cache + */ + public function get( $key ) { + + if( empty( $this->cache ) ) return null; + + + $entries = $this->cache->get( self::CACHE_KEY, [] ); + + if( array_key_exists( $key, $entries ) ) return $entries[ $key ]; + + return null; + } + + + /** + * Sets the $value into the cache + */ + public function set( $key, $value ) { + + if( empty( $this->cache ) ) return; + + $entries = $this->cache->get( self::CACHE_KEY, []); + $entries[ $key ] = $value; + + $this->cache->set( self::CACHE_KEY, $entries ); + } + + + /** + * Clears the cached records + */ + public function clear() { + if( empty( $this->cache ) ) return; + $this->cache->delete( self::CACHE_KEY ); + } + + + public function key( array $params ) { + return md5( json_encode( $params ) ); + } +} + diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php new file mode 100644 index 00000000..a9caf435 --- /dev/null +++ b/providers/Flipt/src/FliptProvider.php @@ -0,0 +1,137 @@ +client = ( is_string( $hostOrClient ) ) ? new FliptClient( $hostOrClient, $apiToken, $namespace ) : $hostOrClient; + $this->cache = new Cache( $cache ); + } + + + public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::BOOLEAN, $defaultValue, $context); + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::STRING, $defaultValue, $context); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::INTEGER, $defaultValue, $context); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::FLOAT, $defaultValue, $context); + } + + /** + * @param mixed[] $defaultValue + */ + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolveValue($flagKey, FlagValueType::OBJECT, $defaultValue, $context); + } + + + /** + * Clears the cache of all requests + */ + public function clearCache() { + $this->cache->clear(); + } + + /** + * @param bool|string|int|float|mixed[] $defaultValue + */ + private function resolveValue(string $flagKey, string $flagType, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + + // check null context + if( empty( $context ) ) { + $attributes = []; + $id = null; + } else { + $attributes = $context->getAttributes()->toArray(); + $id = $context->getTargetingKey(); + } + + + // check if cache has already result + $cacheKey = $this->cache->key( [ 'flag' => $flagKey, 'type' => $flagType, 'default' => $defaultValue, 'context' => $attributes, 'key' => $id ] ); + $cached = $this->cache->get( $cacheKey ); + if( isset( $cached ) ) return $cached; + + + // booleans need a dedicated function + if( $flagType == FlagValueType::BOOLEAN ) { + $result = $this->client->boolean( $flagKey, $attributes, $id ); + } else { + $result = $this->client->variant( $flagKey, $attributes, $id ); + } + + + // there is a match + // not sure yet as the variant result has a getMatch() but not the boolean result. + if( $result->getReason() == 'MATCH_EVALUATION_REASON' || $result->getReason() == "DEFAULT_EVALUATION_REASON" ) { + $result = ResolutionDetailsFactory::fromSuccess( $this->castResult( $result, $flagType ) ); + } else { + $result = (new ResolutionDetailsBuilder()) + ->withValue( $defaultValue ) + ->withError( + // not sure if thie reason to error mapping is correct + new ResolutionError(ErrorCode::GENERAL(), $result->getReason() ), + ) + ->build(); + } + + // write result into cache + $this->cache->set( $cacheKey, $result ); + + return $result; + } + + + + private function castResult( VariantEvaluationResult|BooleanEvaluationResult $result, string $type ) { + switch ($type) { + case FlagValueType::BOOLEAN: + return filter_var($result->getEnabled(), FILTER_VALIDATE_BOOLEAN); + case FlagValueType::FLOAT: + return (float) $result->getVariantKey(); + case FlagValueType::INTEGER: + return (int) $result->getVariantKey(); + case FlagValueType::OBJECT: + return json_decode( $result->getVariantAttachment(), true); + case FlagValueType::STRING: + return $result->getVariantKey(); + default: + return null; + } + } + +} \ No newline at end of file diff --git a/providers/Flipt/tests/FliptProviderTest.php b/providers/Flipt/tests/FliptProviderTest.php new file mode 100644 index 00000000..1006866d --- /dev/null +++ b/providers/Flipt/tests/FliptProviderTest.php @@ -0,0 +1,147 @@ +mockClient = Mockery::mock(); + $this->provider = new FliptProvider( $this->mockClient ); + } + + + protected function tearDown(): void + { + Mockery::close(); + } + + public function testBoolean() + { + $this->mockClient->shouldReceive( 'boolean') + ->withArgs( function( $flag, $context, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $context, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + + $result = $this->provider->resolveBooleanValue( 'flag', false, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), true ); + + } + + public function testInteger() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); + + $result = $this->provider->resolveIntegerValue( 'flag', 10, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), 20 ); + + } + + public function testFloat() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); + + $result = $this->provider->resolveFloatValue( 'flag', 0.1111, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), 0.2345 ); + + } + + public function testString() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + + $result = $this->provider->resolveStringValue( 'flag', 'base', new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), 'My string' ); + + } + + + public function testObject() + { + $this->mockClient->shouldReceive( 'variant') + ->withArgs( function( $flag, $contextRecv, $entityId ) { + $this->assertEquals( $flag, 'flag' ); + $this->assertEquals( $contextRecv, [ 'context' => 'demo' ] ); + $this->assertEquals( $entityId, 'id' ); + return true; + }) + ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + + $result = $this->provider->resolveObjectValue( 'flag', [], new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); + + $this->assertInstanceOf( ResolutionDetails::class, $result ); + $this->assertEquals( $result->getValue(), [ "json" => 1 ] ); + + } + + + public function testCache() + { + $this->mockClient->shouldReceive( 'boolean')->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + + $cache = Mockery::mock( CacheInterface::class ); + $cache->shouldReceive( 'get')->with( 'openfeature-flipt', [] )->andReturn( [] ); + $cache->shouldReceive( 'set')->withArgs( function( $key, $content ) { + $this->assertEquals( $key, 'openfeature-flipt' ); + $this->assertArrayHasKey( '21349ebf7629df2cb162aa15b2ec6335', $content ); + $this->assertInstanceOf( ResolutionDetails::class, $content['21349ebf7629df2cb162aa15b2ec6335' ]); + return true; + }); + + // create new provider with mocked cache + $provider = new FliptProvider( $this->mockClient, 'token', 'ns', $cache ); + + $result = $provider->resolveBooleanValue( 'flag1', false ); + $this->assertEquals( $result->getValue(), true ); + } + + +} From d9a21dcae098236dac5c6674a0b64b68cf51b454 Mon Sep 17 00:00:00 2001 From: legoheld Date: Thu, 23 Nov 2023 09:08:40 +0100 Subject: [PATCH 11/18] docs: Change to MIT license Signed-off-by: legoheld --- providers/Flipt/LICENSE | 222 ++++------------------------------------ 1 file changed, 21 insertions(+), 201 deletions(-) diff --git a/providers/Flipt/LICENSE b/providers/Flipt/LICENSE index 261eeb9e..4954f661 100644 --- a/providers/Flipt/LICENSE +++ b/providers/Flipt/LICENSE @@ -1,201 +1,21 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2023 Flipt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From 0f5b477a81f5cbc3165b03b873b55a70a99f492a Mon Sep 17 00:00:00 2001 From: legoheld Date: Thu, 23 Nov 2023 09:13:16 +0100 Subject: [PATCH 12/18] fix: Remove unnecessary launch.json Signed-off-by: legoheld --- .vscode/launch.json | 51 --------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index bde783d3..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Listen for Xdebug", - "type": "php", - "request": "launch", - "port": 9000, - "pathMappings": { - "/app": "${workspaceFolder}/providers/Flipt" - }, - }, - { - "name": "Launch currently open script", - "type": "php", - "request": "launch", - "program": "${file}", - "cwd": "${fileDirname}", - "port": 0, - "runtimeArgs": [ - "-dxdebug.start_with_request=yes" - ], - "env": { - "XDEBUG_MODE": "debug,develop", - "XDEBUG_CONFIG": "client_port=${port}" - } - }, - { - "name": "Launch Built-in web server", - "type": "php", - "request": "launch", - "runtimeArgs": [ - "-dxdebug.mode=debug", - "-dxdebug.start_with_request=yes", - "-S", - "localhost:0" - ], - "program": "", - "cwd": "${workspaceRoot}", - "port": 9003, - "serverReadyAction": { - "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", - "uriFormat": "http://localhost:%s", - "action": "openExternally" - } - } - ] -} \ No newline at end of file From e5df73627ad683b82df322bc2814f1fe0e820589 Mon Sep 17 00:00:00 2001 From: legoheld Date: Thu, 23 Nov 2023 09:15:11 +0100 Subject: [PATCH 13/18] docs: Revert back to apache2 license according to @beeme1mr Signed-off-by: legoheld --- providers/Flipt/LICENSE | 222 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 201 insertions(+), 21 deletions(-) diff --git a/providers/Flipt/LICENSE b/providers/Flipt/LICENSE index 4954f661..261eeb9e 100644 --- a/providers/Flipt/LICENSE +++ b/providers/Flipt/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2023 Flipt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From f842e2cc2827fe34bbb18881edbb3e0b7fc38f51 Mon Sep 17 00:00:00 2001 From: legoheld Date: Mon, 27 Nov 2023 10:40:50 +0100 Subject: [PATCH 14/18] feat: add CacheProvider to extract caching funcionality from FliptProvider fix: Add ResponseReasons constants fix: proper FliptProvider constructor type fix: Add ksort for context attributes Signed-off-by: legoheld Signed-off-by: legoheld --- providers/Flipt/src/Cache.php | 64 -------- providers/Flipt/src/CacheProvider.php | 162 ++++++++++++++++++++ providers/Flipt/src/FliptProvider.php | 23 +-- providers/Flipt/src/ResponseReasons.php | 15 ++ providers/Flipt/tests/CacheProviderTest.php | 60 ++++++++ providers/Flipt/tests/FliptProviderTest.php | 38 ++--- 6 files changed, 250 insertions(+), 112 deletions(-) delete mode 100644 providers/Flipt/src/Cache.php create mode 100644 providers/Flipt/src/CacheProvider.php create mode 100644 providers/Flipt/src/ResponseReasons.php create mode 100644 providers/Flipt/tests/CacheProviderTest.php diff --git a/providers/Flipt/src/Cache.php b/providers/Flipt/src/Cache.php deleted file mode 100644 index 850b587f..00000000 --- a/providers/Flipt/src/Cache.php +++ /dev/null @@ -1,64 +0,0 @@ -cache = $cache; - } - - - - /** - * Retrievies a value from the cache - */ - public function get( $key ) { - - if( empty( $this->cache ) ) return null; - - - $entries = $this->cache->get( self::CACHE_KEY, [] ); - - if( array_key_exists( $key, $entries ) ) return $entries[ $key ]; - - return null; - } - - - /** - * Sets the $value into the cache - */ - public function set( $key, $value ) { - - if( empty( $this->cache ) ) return; - - $entries = $this->cache->get( self::CACHE_KEY, []); - $entries[ $key ] = $value; - - $this->cache->set( self::CACHE_KEY, $entries ); - } - - - /** - * Clears the cached records - */ - public function clear() { - if( empty( $this->cache ) ) return; - $this->cache->delete( self::CACHE_KEY ); - } - - - public function key( array $params ) { - return md5( json_encode( $params ) ); - } -} - diff --git a/providers/Flipt/src/CacheProvider.php b/providers/Flipt/src/CacheProvider.php new file mode 100644 index 00000000..ae7aaf3e --- /dev/null +++ b/providers/Flipt/src/CacheProvider.php @@ -0,0 +1,162 @@ +provider = $provider; + $this->storage = $storage; + $this->key = $key; + } + + + public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveBooleanValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveStringValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveIntegerValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveFloatValue($flagKey, $defaultValue, $context) + ); + } + + public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->cacheCheck( + $this->hash($flagKey, $defaultValue, $context), + fn () => $this->provider->resolveObjectValue($flagKey, $defaultValue, $context) + ); + } + + + protected function cacheCheck(string $key, Closure $next) + { + + $cached = $this->get($key); + if (isset($cached)) return $cached; + + $result = $next(); + + $this->set($key, $result); + + return $result; + } + + + /** + * Retrievies a value from the cache + */ + protected function get($key) + { + + $entries = $this->storage->get($this->key, []); + + if (array_key_exists($key, $entries)) return $entries[$key]; + + return null; + } + + + /** + * Sets the $value into the cache + */ + protected function set($key, $value) + { + + + $entries = $this->storage->get($this->key, []); + $entries[$key] = $value; + + $this->storage->set($this->key, $entries); + } + + + + /** + * Clears the cached records + */ + public function clear() + { + $this->storage->delete($this->key); + } + + + + protected function hash(string $flag, mixed $default, ?EvaluationContext $context) + { + + // check null context + if (empty($context)) { + $attributes = []; + $id = null; + } else { + $attributes = $context->getAttributes()->toArray(); + $id = $context->getTargetingKey(); + } + + // generate hash on request arguments + return md5(json_encode([ + 'flag' => $flag, + 'default' => $default, + 'id' => $id, 'context' => $this->sortArray($attributes) + ])); + } + + + + protected function sortArray(array $array) + { + $sortedArray = []; + + foreach ($array as $key => $value) { + if (is_array($value)) { + // Recursively sort nested arrays + $value = $this->sortArray($value); + } + $sortedArray[$key] = $value; + } + + // Sort the array by keys + ksort($sortedArray); + + return $sortedArray; + } +} diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php index a9caf435..5a4dedb2 100644 --- a/providers/Flipt/src/FliptProvider.php +++ b/providers/Flipt/src/FliptProvider.php @@ -14,18 +14,15 @@ use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; use OpenFeature\interfaces\provider\ResolutionDetails; -use Psr\SimpleCache\CacheInterface; class FliptProvider extends AbstractProvider implements Provider { protected const NAME = 'FliptProvider'; protected $client; - protected Cache $cache; - public function __construct( mixed $hostOrClient, string $apiToken = '', string $namespace = '', CacheInterface $cache = null ) { + public function __construct( string|FliptClient $hostOrClient, string $apiToken = '', string $namespace = '' ) { $this->client = ( is_string( $hostOrClient ) ) ? new FliptClient( $hostOrClient, $apiToken, $namespace ) : $hostOrClient; - $this->cache = new Cache( $cache ); } @@ -58,12 +55,6 @@ public function resolveObjectValue(string $flagKey, array $defaultValue, ?Evalua } - /** - * Clears the cache of all requests - */ - public function clearCache() { - $this->cache->clear(); - } /** * @param bool|string|int|float|mixed[] $defaultValue @@ -80,13 +71,6 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV $id = $context->getTargetingKey(); } - - // check if cache has already result - $cacheKey = $this->cache->key( [ 'flag' => $flagKey, 'type' => $flagType, 'default' => $defaultValue, 'context' => $attributes, 'key' => $id ] ); - $cached = $this->cache->get( $cacheKey ); - if( isset( $cached ) ) return $cached; - - // booleans need a dedicated function if( $flagType == FlagValueType::BOOLEAN ) { $result = $this->client->boolean( $flagKey, $attributes, $id ); @@ -97,7 +81,7 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV // there is a match // not sure yet as the variant result has a getMatch() but not the boolean result. - if( $result->getReason() == 'MATCH_EVALUATION_REASON' || $result->getReason() == "DEFAULT_EVALUATION_REASON" ) { + if( $result->getReason() == ResponseReasons::MATCH_EVALUATION_REASON || $result->getReason() == ResponseReasons::DEFAULT_EVALUATION_REASON ) { $result = ResolutionDetailsFactory::fromSuccess( $this->castResult( $result, $flagType ) ); } else { $result = (new ResolutionDetailsBuilder()) @@ -109,9 +93,6 @@ private function resolveValue(string $flagKey, string $flagType, mixed $defaultV ->build(); } - // write result into cache - $this->cache->set( $cacheKey, $result ); - return $result; } diff --git a/providers/Flipt/src/ResponseReasons.php b/providers/Flipt/src/ResponseReasons.php new file mode 100644 index 00000000..de721436 --- /dev/null +++ b/providers/Flipt/src/ResponseReasons.php @@ -0,0 +1,15 @@ +mockProvider = Mockery::mock( FliptProvider::class ); + $this->storage = Mockery::mock( CacheInterface::class ); + $this->provider = new CacheProvider( $this->mockProvider, $this->storage ); + } + + + protected function tearDown(): void + { + Mockery::close(); + } + + public function testSet() + { + + $context = new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ); + + $this->storage->shouldReceive( 'get')->with( 'open-feature', [] )->andReturn( [] ); + $this->storage->shouldReceive( 'set')->withArgs( function( $key, $content ) { + $this->assertEquals( $key, 'open-feature' ); + $this->assertArrayHasKey( 'fa89ea8c3b16386166360dd163f10e6d', $content ); + $this->assertInstanceOf( ResolutionDetails::class, $content['fa89ea8c3b16386166360dd163f10e6d' ]); + return true; + }); + $this->mockProvider->shouldReceive( 'resolveBooleanValue') + ->with( 'flag', true, $context ) + ->andReturn( ResolutionDetailsFactory::fromSuccess( true ) ); + + $result = $this->provider->resolveBooleanValue( 'flag', true, $context ); + + $this->assertEquals( $result->getValue(), true ); + } +} \ No newline at end of file diff --git a/providers/Flipt/tests/FliptProviderTest.php b/providers/Flipt/tests/FliptProviderTest.php index 1006866d..d7772806 100644 --- a/providers/Flipt/tests/FliptProviderTest.php +++ b/providers/Flipt/tests/FliptProviderTest.php @@ -2,6 +2,7 @@ namespace Tests; +use Flipt\Client\FliptClient; use Flipt\Models\DefaultBooleanEvaluationResult; use Flipt\Models\DefaultVariantEvaluationResult; use Mockery; @@ -10,18 +11,19 @@ use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\implementation\provider\ResolutionDetails; use OpenFeature\Providers\Flipt\FliptProvider; +use OpenFeature\Providers\Flipt\ResponseReasons; use PHPUnit\Framework\TestCase; use Psr\SimpleCache\CacheInterface; class FliptProviderTest extends TestCase { - protected MockInterface $mockClient; + protected FliptClient&MockInterface $mockClient; protected FliptProvider $provider; protected function setUp(): void { - $this->mockClient = Mockery::mock(); + $this->mockClient = Mockery::mock( 'overload:' . FliptClient::class ); $this->provider = new FliptProvider( $this->mockClient ); } @@ -31,6 +33,7 @@ protected function tearDown(): void Mockery::close(); } + public function testBoolean() { $this->mockClient->shouldReceive( 'boolean') @@ -40,7 +43,7 @@ public function testBoolean() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); + ->andReturn( new DefaultBooleanEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245' ) ); $result = $this->provider->resolveBooleanValue( 'flag', false, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -58,7 +61,7 @@ public function testInteger() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); $result = $this->provider->resolveIntegerValue( 'flag', 10, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -76,7 +79,7 @@ public function testFloat() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); $result = $this->provider->resolveFloatValue( 'flag', 0.1111, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -94,7 +97,7 @@ public function testString() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveStringValue( 'flag', 'base', new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -113,7 +116,7 @@ public function testObject() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveObjectValue( 'flag', [], new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -122,26 +125,7 @@ public function testObject() } - - public function testCache() - { - $this->mockClient->shouldReceive( 'boolean')->andReturn( new DefaultBooleanEvaluationResult( true, 'MATCH_EVALUATION_REASON', 0.1, 'rid', '13245' ) ); - - $cache = Mockery::mock( CacheInterface::class ); - $cache->shouldReceive( 'get')->with( 'openfeature-flipt', [] )->andReturn( [] ); - $cache->shouldReceive( 'set')->withArgs( function( $key, $content ) { - $this->assertEquals( $key, 'openfeature-flipt' ); - $this->assertArrayHasKey( '21349ebf7629df2cb162aa15b2ec6335', $content ); - $this->assertInstanceOf( ResolutionDetails::class, $content['21349ebf7629df2cb162aa15b2ec6335' ]); - return true; - }); - - // create new provider with mocked cache - $provider = new FliptProvider( $this->mockClient, 'token', 'ns', $cache ); - - $result = $provider->resolveBooleanValue( 'flag1', false ); - $this->assertEquals( $result->getValue(), true ); - } + } From f38c9c2bdb60cbda892644a731430ce5f4f94546 Mon Sep 17 00:00:00 2001 From: legoheld Date: Mon, 27 Nov 2023 10:45:53 +0100 Subject: [PATCH 15/18] chore: fix readme Signed-off-by: legoheld Signed-off-by: legoheld --- providers/Flipt/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/Flipt/README.md b/providers/Flipt/README.md index 54e222da..8667f6fe 100644 --- a/providers/Flipt/README.md +++ b/providers/Flipt/README.md @@ -36,11 +36,11 @@ $provider = new FliptProvider($host, $apiToken, $namespace); OpenFeature::setProvider($provider); // Now you can evaluate your feature flags as follows -$booleanFlagValue = OpenFeature::getBooleanValue('your-boolean-flag-key', false); -$stringFlagValue = OpenFeature::getStringValue('your-string-flag-key', 'default-value'); -$integerFlagValue = OpenFeature::getIntegerValue('your-integer-flag-key', 0); -$floatFlagValue = OpenFeature::getFloatValue('your-float-flag-key', 0.0); -$objectFlagValue = OpenFeature::getObjectValue('your-object-flag-key', ['default' => 'value']); +$booleanFlagValue = OpenFeatureAPI::getInstance()->getClient()->getBooleanValue('your-boolean-flag-key', false); +$stringFlagValue = OpenFeatureAPI::getInstance()->getClient()->getStringValue('your-string-flag-key', 'default-value'); +$integerFlagValue = OpenFeatureAPI::getInstance()->getClient()->getIntegerValue('your-integer-flag-key', 0); +$floatFlagValue = OpenFeatureAPI::getInstance()->getClient()->getFloatValue('your-float-flag-key', 0.0); +$objectFlagValue = OpenFeatureAPI::getInstance()->getClient()->getObjectValue('your-object-flag-key', ['default' => 'value']); ``` ### Caching From 1abf181524fd1337557bc0595858ac8701e5e23b Mon Sep 17 00:00:00 2001 From: legoheld Date: Wed, 29 Nov 2023 09:00:48 +0100 Subject: [PATCH 16/18] chore: Update to client lib constants Signed-off-by: legoheld Signed-off-by: legoheld --- providers/Flipt/composer.json | 2 +- providers/Flipt/src/FliptProvider.php | 1 + providers/Flipt/src/ResponseReasons.php | 15 --------------- 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 providers/Flipt/src/ResponseReasons.php diff --git a/providers/Flipt/composer.json b/providers/Flipt/composer.json index 76614a6b..93e9f156 100644 --- a/providers/Flipt/composer.json +++ b/providers/Flipt/composer.json @@ -10,7 +10,7 @@ "require": { "php": ">=8.0", "open-feature/sdk": "^2.0", - "flipt-io/flipt": "^0.0.1", + "flipt-io/flipt": "^0.2.0", "psr/simple-cache": "^3.0.0" }, "require-dev": { diff --git a/providers/Flipt/src/FliptProvider.php b/providers/Flipt/src/FliptProvider.php index 5a4dedb2..5eb9288c 100644 --- a/providers/Flipt/src/FliptProvider.php +++ b/providers/Flipt/src/FliptProvider.php @@ -4,6 +4,7 @@ use Flipt\Client\FliptClient; use Flipt\Models\BooleanEvaluationResult; +use Flipt\Models\ResponseReasons; use Flipt\Models\VariantEvaluationResult; use OpenFeature\implementation\provider\AbstractProvider; use OpenFeature\implementation\provider\ResolutionDetailsBuilder; diff --git a/providers/Flipt/src/ResponseReasons.php b/providers/Flipt/src/ResponseReasons.php deleted file mode 100644 index de721436..00000000 --- a/providers/Flipt/src/ResponseReasons.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Tue, 5 Dec 2023 17:02:18 +0100 Subject: [PATCH 17/18] fix: proper response reasons in test Signed-off-by: legoheld --- providers/Flipt/tests/FliptProviderTest.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/providers/Flipt/tests/FliptProviderTest.php b/providers/Flipt/tests/FliptProviderTest.php index d7772806..84030d94 100644 --- a/providers/Flipt/tests/FliptProviderTest.php +++ b/providers/Flipt/tests/FliptProviderTest.php @@ -5,15 +5,14 @@ use Flipt\Client\FliptClient; use Flipt\Models\DefaultBooleanEvaluationResult; use Flipt\Models\DefaultVariantEvaluationResult; +use Flipt\Models\ResponseReasons as ModelsResponseReasons; use Mockery; use Mockery\MockInterface; use OpenFeature\implementation\flags\Attributes; use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\implementation\provider\ResolutionDetails; use OpenFeature\Providers\Flipt\FliptProvider; -use OpenFeature\Providers\Flipt\ResponseReasons; use PHPUnit\Framework\TestCase; -use Psr\SimpleCache\CacheInterface; class FliptProviderTest extends TestCase { @@ -43,7 +42,7 @@ public function testBoolean() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultBooleanEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245' ) ); + ->andReturn( new DefaultBooleanEvaluationResult( true, ModelsResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245' ) ); $result = $this->provider->resolveBooleanValue( 'flag', false, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -61,7 +60,7 @@ public function testInteger() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ModelsResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '20', '{"json":1}' ) ); $result = $this->provider->resolveIntegerValue( 'flag', 10, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -79,7 +78,7 @@ public function testFloat() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ModelsResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], '0.2345', '{"json":1}' ) ); $result = $this->provider->resolveFloatValue( 'flag', 0.1111, new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -97,7 +96,7 @@ public function testString() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ModelsResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveStringValue( 'flag', 'base', new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); @@ -116,7 +115,7 @@ public function testObject() $this->assertEquals( $entityId, 'id' ); return true; }) - ->andReturn( new DefaultVariantEvaluationResult( true, ResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); + ->andReturn( new DefaultVariantEvaluationResult( true, ModelsResponseReasons::MATCH_EVALUATION_REASON, 0.1, 'rid', '13245', [], 'My string', '{"json":1}' ) ); $result = $this->provider->resolveObjectValue( 'flag', [], new EvaluationContext( 'id', new Attributes( [ 'context' => 'demo' ] ) ) ); From 41f447485a43f51084a8107c06fa9843ecbc357c Mon Sep 17 00:00:00 2001 From: legoheld Date: Wed, 6 Dec 2023 09:52:30 +0100 Subject: [PATCH 18/18] doc: adjust caching infos according to new caching provider approach Signed-off-by: legoheld --- providers/Flipt/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/providers/Flipt/README.md b/providers/Flipt/README.md index 8667f6fe..8fb4056a 100644 --- a/providers/Flipt/README.md +++ b/providers/Flipt/README.md @@ -45,16 +45,20 @@ $objectFlagValue = OpenFeatureAPI::getInstance()->getClient()->getObjectValue('y ### Caching -If you like to cache the feature flag results you can pass a [PSR-16](https://www.php-fig.org/psr/psr-16/) compatible cache storage into the provider constructor like this: +If you like to cache the feature flag results you need a [PSR-16](https://www.php-fig.org/psr/psr-16/) compatible cache storage. +Then you simple wrap the FliptProvider (or any other provider) with a new CacheProvider instance as the following example shows. +This way all the feature flag requests are cached to optimize performance. ```php +use OpenFeature\Providers\Flipt\FliptProvider; +use OpenFeature\Providers\Flipt\CacheProvider; -$cache = ''; -$provider = new FliptProvider($host, $apiToken, $namespace, $cache); +$fliptProvider = new FliptProvider($host, $apiToken, $namespace); +$cache = new CacheProvider( $fliptProvider,'' ); -OpenFeature::setProvider($provider); +OpenFeatureAPI::getInstance()->setProvider($cache); // to clear the cache you can call -$provider->cacheClear(); +$cache->clear(); ```