From 4098a0b4c780fedc31195aee012e4c8b68208bc7 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 18 Mar 2025 21:49:04 +0000 Subject: [PATCH 01/23] Initial Flagsmith provider. Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/.gitignore | 3 + providers/Flagsmith/CHANGELOG.md | 1 + providers/Flagsmith/README.md | 47 ++++++ providers/Flagsmith/composer.json | 138 ++++++++++++++++++ providers/Flagsmith/phpcs.xml.dist | 25 ++++ providers/Flagsmith/phpstan.neon.dist | 9 ++ providers/Flagsmith/phpunit.xml.dist | 25 ++++ providers/Flagsmith/psalm-baseline.xml | 2 + providers/Flagsmith/psalm.xml | 17 +++ providers/Flagsmith/src/FlagsmithProvider.php | 116 +++++++++++++++ providers/Flagsmith/tests/TestCase.php | 40 +++++ .../tests/Unit/FlagsmithProviderTest.php | 41 ++++++ 12 files changed, 464 insertions(+) create mode 100644 providers/Flagsmith/.gitignore create mode 100644 providers/Flagsmith/CHANGELOG.md create mode 100644 providers/Flagsmith/README.md create mode 100644 providers/Flagsmith/composer.json create mode 100644 providers/Flagsmith/phpcs.xml.dist create mode 100644 providers/Flagsmith/phpstan.neon.dist create mode 100644 providers/Flagsmith/phpunit.xml.dist create mode 100644 providers/Flagsmith/psalm-baseline.xml create mode 100644 providers/Flagsmith/psalm.xml create mode 100644 providers/Flagsmith/src/FlagsmithProvider.php create mode 100644 providers/Flagsmith/tests/TestCase.php create mode 100644 providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php diff --git a/providers/Flagsmith/.gitignore b/providers/Flagsmith/.gitignore new file mode 100644 index 00000000..2e1a6a68 --- /dev/null +++ b/providers/Flagsmith/.gitignore @@ -0,0 +1,3 @@ +/build +/composer.lock +/vendor diff --git a/providers/Flagsmith/CHANGELOG.md b/providers/Flagsmith/CHANGELOG.md new file mode 100644 index 00000000..825c32f0 --- /dev/null +++ b/providers/Flagsmith/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/providers/Flagsmith/README.md b/providers/Flagsmith/README.md new file mode 100644 index 00000000..28596345 --- /dev/null +++ b/providers/Flagsmith/README.md @@ -0,0 +1,47 @@ +# OpenFeature Flagsmith Provider for PHP + +[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Latest Stable Version](http://poser.pugx.org/open-feature/flagsmith-provider/v)](https://packagist.org/packages/open-feature/flagsmith-provider) +[![Total Downloads](http://poser.pugx.org/open-feature/flagsmith-provider/downloads)](https://packagist.org/packages/open-feature/flagsmith-provider) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) +[![License](http://poser.pugx.org/open-feature/flagsmith-provider/license)](https://packagist.org/packages/open-feature/flagsmith-provider) + +## Overview + +Flagsmith provides an all-in-one platform for developing, implementing, and managing your feature flags. This repository and package provides the client side code for interacting with it via the OpenFeature PHP SDK. + +This package also builds on various PSRs (PHP Standards Recommendations) such as the Logger interfaces (PSR-3) and the Basic and Extended Coding Standards (PSR-1 and PSR-12). + +## Installation + +```sh +composer require open-feature/flagsmith-provider +``` + +## Usage + +The `FlagsmithProvider` constructor takes a configured Flagsmith client as its only argument: + +```php +$flagsmith = new Flagsmith\Flagsmith('YOUR_FLAGSMITH_API_KEY'); + +OpenFeatureAPI::setProvider(new FlagsmithProvider($flagsmith)); +``` + +## Development + +### PHP Versioning + +This library targets PHP version and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK. + +This package also has a `.tool-versions` file for use with PHP version managers like `asdf`. + +### Installation and Dependencies + +Install dependencies with `composer install`. + +We value having as few runtime dependencies as possible. The addition of any dependencies requires careful consideration and review. + +### Testing + +Run tests with `composer run test`. diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json new file mode 100644 index 00000000..1e282af8 --- /dev/null +++ b/providers/Flagsmith/composer.json @@ -0,0 +1,138 @@ +{ + "name": "open-feature/flagsmith-provider", + "description": "The Flagsmith provider package for open-feature", + "license": "Apache-2.0", + "type": "library", + "keywords": [ + "featureflags", + "featureflagging", + "openfeature", + "flagsmith", + "provider" + ], + "authors": [ + { + "name": "OpenFeature PHP Maintainers", + "homepage": "https://github.com/orgs/open-feature/teams/php-maintainer" + }, + { + "name": "open-feature/php-sdk-contrib Contributors", + "homepage": "https://github.com/open-feature/php-sdk-contrib/graphs/contributors" + } + ], + "require": { + "php": "^8", + "flagsmith/flagsmith-php-client": "^4.1", + "open-feature/sdk": "^2.0", + "php-http/httplug": "^2.3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^2.0", + "psr/log": "^2.0 || ^3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.25", + "friendsofphp/php-cs-fixer": "^3.13", + "hamcrest/hamcrest-php": "^2.0", + "mdwheele/zalgo": "^0.3.1", + "mockery/mockery": "^1.5", + "phan/phan": "^5.4", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "~1.10.0", + "phpstan/phpstan-mockery": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "psalm/plugin-mockery": "^0.11.0", + "psalm/plugin-phpunit": "^0.18.0", + "ramsey/coding-standard": "^2.0.3", + "ramsey/composer-repl": "^1.4", + "ramsey/conventional-commits": "^1.3", + "roave/security-advisories": "dev-latest", + "spatie/phpunit-snapshot-assertions": "^4.2", + "vimeo/psalm": "~4.30.0" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "OpenFeature\\Providers\\Flagsmith\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Providers\\Flagsmith\\Test\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true, + "captainhook/plugin-composer": true, + "ramsey/composer-repl": true, + "php-http/discovery": false + }, + "lock": false, + "sort-packages": true + }, + "extra": { + "captainhook": { + "force-install": false + } + }, + "scripts": { + "dev:analyze": [ + "@dev:analyze:phpstan", + "@dev:analyze:psalm" + ], + "dev:analyze:phpstan": "phpstan analyse --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "psalm", + "dev:build:clean": "git clean -fX build/", + "dev:grpc": [ + "@dev:grpc:init", + "@dev:grpc:generate", + "@dev:grpc:stage" + ], + "dev:grpc:generate": "export GOPATH=\"$(pwd)/schemas/vendor\" && pushd schemas && make gen-php && popd", + "dev:grpc:init": "git submodule update --recursive", + "dev:grpc:stage": "git add --force ./proto", + "dev:lint": [ + "@dev:lint:syntax", + "@dev:lint:style" + ], + "dev:lint:fix": "phpcbf", + "dev:lint:style": "phpcs --colors", + "dev:lint:syntax": "parallel-lint --colors src/ tests/", + "dev:test": [ + "@dev:lint", + "@dev:analyze", + "@dev:test:unit", + "@dev:test:integration" + ], + "dev:test:coverage:ci": "phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", + "dev:test:coverage:html": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": "phpunit --colors=always --testdox", + "dev:test:unit:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "dev:test:unit:setup": "echo 'Setup for unit tests...'", + "dev:test:unit:teardown": "echo 'Tore down unit tests...'", + "dev:test:integration:setup": "echo 'Setup for integration tests...'", + "dev:test:integration:teardown": "echo 'Tore down integration tests...'", + "test": "@dev:test" + }, + "scripts-descriptions": { + "dev:analyze": "Runs all static analysis checks.", + "dev:analyze:phpstan": "Runs the PHPStan static analyzer.", + "dev:analyze:psalm": "Runs the Psalm static analyzer.", + "dev:build:clean": "Cleans the build/ directory.", + "dev:lint": "Runs all linting checks.", + "dev:lint:fix": "Auto-fixes coding standards issues, if possible.", + "dev:lint:style": "Checks for coding standards issues.", + "dev:lint:syntax": "Checks for syntax errors.", + "dev:test": "Runs linting, static analysis, and unit tests.", + "dev:test:coverage:ci": "Runs unit tests and generates CI coverage reports.", + "dev:test:coverage:html": "Runs unit tests and generates HTML coverage report.", + "dev:test:unit": "Runs unit tests.", + "test": "Runs linting, static analysis, and unit tests." + } +} diff --git a/providers/Flagsmith/phpcs.xml.dist b/providers/Flagsmith/phpcs.xml.dist new file mode 100644 index 00000000..55d9d3a1 --- /dev/null +++ b/providers/Flagsmith/phpcs.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + ./src + ./tests + + */tests/fixtures/* + */tests/*/fixtures/* + + + + + + + + + + + + + diff --git a/providers/Flagsmith/phpstan.neon.dist b/providers/Flagsmith/phpstan.neon.dist new file mode 100644 index 00000000..93c5b2d2 --- /dev/null +++ b/providers/Flagsmith/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + tmpDir: ./build/cache/phpstan + level: max + paths: + - ./src + - ./tests + excludePaths: + - */tests/fixtures/* + - */tests/*/fixtures/* diff --git a/providers/Flagsmith/phpunit.xml.dist b/providers/Flagsmith/phpunit.xml.dist new file mode 100644 index 00000000..f96ebda6 --- /dev/null +++ b/providers/Flagsmith/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./tests/Unit + + + + + + ./src + + + + + + + + diff --git a/providers/Flagsmith/psalm-baseline.xml b/providers/Flagsmith/psalm-baseline.xml new file mode 100644 index 00000000..ceaa5778 --- /dev/null +++ b/providers/Flagsmith/psalm-baseline.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/providers/Flagsmith/psalm.xml b/providers/Flagsmith/psalm.xml new file mode 100644 index 00000000..c3e6c03c --- /dev/null +++ b/providers/Flagsmith/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php new file mode 100644 index 00000000..326206a5 --- /dev/null +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -0,0 +1,116 @@ +contextualFlagStore($context)->getFlag($flagKey)->getEnabled(), + ); + } catch (FlagsmithThrowable $throwable) { + return (new ResolutionDetailsBuilder()) + ->withValue($defaultValue) + ->withError( + new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()), + )->build(); + } + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + $resolutionDetails = (new ResolutionDetailsBuilder())->build(); + + try { + $resolutionDetails->setValue( + $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), + ); + } catch (FlagsmithThrowable) { + $resolutionDetails->setValue($defaultValue); + } + + return $resolutionDetails; + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + $resolutionDetails = (new ResolutionDetailsBuilder())->build(); + + try { + $resolutionDetails->setValue( + $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), + ); + } catch (FlagsmithThrowable) { + $resolutionDetails->setValue($defaultValue); + } + + return $resolutionDetails; + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + $resolutionDetails = (new ResolutionDetailsBuilder())->build(); + + try { + $resolutionDetails->setValue( + $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), + ); + } catch (FlagsmithThrowable) { + $resolutionDetails->setValue($defaultValue); + } + + return $resolutionDetails; + } + + public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + $builder = new ResolutionDetailsBuilder(); + + try { + $builder->withValue( + $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), + ); + } catch (FlagsmithThrowable $throwable) { + $builder->withValue($defaultValue); + $builder->withError( + new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()), + ); + } + + return $builder->build(); + } + + private function contextualFlagStore(?EvaluationContext $context = null): Flags + { + if ($context && !is_null($identifier = $context->getTargetingKey())) { + return $this->flagsmith->getIdentityFlags($identifier, (object) $context->getAttributes()->toArray()); + } + + return $this->flagsmith->getEnvironmentFlags(); + } +} diff --git a/providers/Flagsmith/tests/TestCase.php b/providers/Flagsmith/tests/TestCase.php new file mode 100644 index 00000000..b3e28657 --- /dev/null +++ b/providers/Flagsmith/tests/TestCase.php @@ -0,0 +1,40 @@ + $class + * @param mixed ...$arguments + * + * @return T & MockInterface + * + * @template T + * + * phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + public function mockery(string $class, ...$arguments) + { + /** @var T & MockInterface $mock */ + $mock = Mockery::mock($class, ...$arguments); + + return $mock; + } +} diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php new file mode 100644 index 00000000..ac9c26dd --- /dev/null +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -0,0 +1,41 @@ + [ + 'client' => $this->mockery(ClientInterface::class), + 'requestFactory' => $this->mockery(RequestFactoryInterface::class), + 'streamFactory' => $this->mockery(StreamFactoryInterface::class), + ], + ]; + $flagsmith = new Flagsmith('dummy-key'); + + // When + $instance = new FlagsmithProvider($flagsmith); + + // Then + $this->assertNotNull($instance); + $this->assertInstanceOf(Provider::class, $instance); + } +} From 9615e1e1e4df93b152826253d4cb6fff2af89ff5 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 18 Mar 2025 21:50:39 +0000 Subject: [PATCH 02/23] Added split config for Flagsmith. Signed-off-by: Chris Lightfoot-Wild --- .github/workflows/split_monorepo.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/split_monorepo.yaml b/.github/workflows/split_monorepo.yaml index 56230cf5..f1d4b291 100644 --- a/.github/workflows/split_monorepo.yaml +++ b/.github/workflows/split_monorepo.yaml @@ -24,6 +24,7 @@ jobs: - [hook, validators, Validators] - [provider, cloudbees, CloudBees] - [provider, flagd, Flagd] + - [provider, flagsmith, Flagsmith] - [provider, split, Split] - [provider, go-feature-flag, GoFeatureFlag] steps: From 9d23a4e3d345d1d78b78850f22ab7072c1b04a30 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Wed, 19 Mar 2025 14:50:29 +0000 Subject: [PATCH 03/23] Flagsmith: added .gitattributes Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/.gitattributes | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 providers/Flagsmith/.gitattributes diff --git a/providers/Flagsmith/.gitattributes b/providers/Flagsmith/.gitattributes new file mode 100644 index 00000000..a8073ebb --- /dev/null +++ b/providers/Flagsmith/.gitattributes @@ -0,0 +1,15 @@ +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore +/.readthedocs.yml export-ignore +/build/ export-ignore +/CHANGELOG.md export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm-baseline.xml export-ignore +/psalm.xml export-ignore +/tests/ export-ignore + +* text=auto From 6793ad3a28f718204d066af6c097a84fcd97d02c Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Wed, 19 Mar 2025 15:10:47 +0000 Subject: [PATCH 04/23] Flagsmith: handle enabled/disabled state of flags. Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/src/FlagsmithProvider.php | 48 ++++++------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 326206a5..340dadb0 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -44,56 +44,33 @@ public function resolveBooleanValue(string $flagKey, bool $defaultValue, ?Evalua public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { - $resolutionDetails = (new ResolutionDetailsBuilder())->build(); - - try { - $resolutionDetails->setValue( - $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), - ); - } catch (FlagsmithThrowable) { - $resolutionDetails->setValue($defaultValue); - } - - return $resolutionDetails; + return $this->resolve($flagKey, $defaultValue, $context); } public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { - $resolutionDetails = (new ResolutionDetailsBuilder())->build(); - - try { - $resolutionDetails->setValue( - $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), - ); - } catch (FlagsmithThrowable) { - $resolutionDetails->setValue($defaultValue); - } - - return $resolutionDetails; + return $this->resolve($flagKey, $defaultValue, $context); } public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { - $resolutionDetails = (new ResolutionDetailsBuilder())->build(); - - try { - $resolutionDetails->setValue( - $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), - ); - } catch (FlagsmithThrowable) { - $resolutionDetails->setValue($defaultValue); - } - - return $resolutionDetails; + return $this->resolve($flagKey, $defaultValue, $context); } public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolve($flagKey, $defaultValue, $context); + } + + protected function resolve(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { $builder = new ResolutionDetailsBuilder(); try { + $flag = $this->contextualFlagStore($context)->getFlag($flagKey); + $builder->withValue( - $this->contextualFlagStore($context)->getFlag($flagKey)->getValue(), + $flag->getEnabled() ? $flag->getValue() : $defaultValue, ); } catch (FlagsmithThrowable $throwable) { $builder->withValue($defaultValue); @@ -105,6 +82,9 @@ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?Evalua return $builder->build(); } + /** + * @throws FlagsmithThrowable + */ private function contextualFlagStore(?EvaluationContext $context = null): Flags { if ($context && !is_null($identifier = $context->getTargetingKey())) { From 9f09785895cd24a260f2bf901081e990d5ff83b8 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 23 Mar 2025 13:27:51 +0000 Subject: [PATCH 05/23] (ci) add Flagsmith to test matrix Signed-off-by: Chris Lightfoot-Wild --- .github/workflows/php-ci.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/php-ci.yaml b/.github/workflows/php-ci.yaml index 109afb1f..daefd690 100644 --- a/.github/workflows/php-ci.yaml +++ b/.github/workflows/php-ci.yaml @@ -14,13 +14,14 @@ jobs: operating-system: [ubuntu-latest] php-version: ['8.0', '8.1', '8.2'] project-dir: - - hooks/OpenTelemetry - hooks/DDTrace + - hooks/OpenTelemetry - hooks/Validators + # - providers/CloudBees - providers/Flagd - - providers/Split + - providers/Flagsmith - providers/GoFeatureFlag - # - providers/CloudBees + - providers/Split fail-fast: false # todo exclude some matrix combinations based on php version requirements From 0485e53ccde2c65f399846024b7f7465f26bad46 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 23 Mar 2025 14:33:51 +0000 Subject: [PATCH 06/23] (feat) Flagsmith resolveObjectValue should expect flag value to be encoded JSON array Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/composer.json | 1 + providers/Flagsmith/src/FlagsmithProvider.php | 26 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json index 1e282af8..9703f7e0 100644 --- a/providers/Flagsmith/composer.json +++ b/providers/Flagsmith/composer.json @@ -22,6 +22,7 @@ ], "require": { "php": "^8", + "ext-json": "*", "flagsmith/flagsmith-php-client": "^4.1", "open-feature/sdk": "^2.0", "php-http/httplug": "^2.3.0", diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 340dadb0..17723508 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -7,6 +7,8 @@ use Flagsmith\Exceptions\FlagsmithThrowable; use Flagsmith\Flagsmith; use Flagsmith\Models\Flags; +use InvalidArgumentException; +use JsonException; use OpenFeature\implementation\provider\AbstractProvider; use OpenFeature\implementation\provider\ResolutionDetailsBuilder; use OpenFeature\implementation\provider\ResolutionDetailsFactory; @@ -59,7 +61,29 @@ public function resolveFloatValue(string $flagKey, float $defaultValue, ?Evaluat public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails { - return $this->resolve($flagKey, $defaultValue, $context); + $builder = new ResolutionDetailsBuilder(); + + try { + $value = json_decode( + $this->resolve($flagKey, $defaultValue, $context)->getValue(), + true, + flags: JSON_THROW_ON_ERROR, + ); + + if (!is_array($value)) { + throw new InvalidArgumentException("Flag [$flagKey] value must be a JSON encoded array"); + } + + $builder->withValue($value); + } catch (JsonException|InvalidArgumentException $exception) { + $builder + ->withValue($defaultValue) + ->withError( + new ResolutionError(ErrorCode::PARSE_ERROR(), $exception->getMessage()), + ); + } + + return $builder->build(); } protected function resolve(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails From e7545668b9e2271cdc308fc2f93015be7cc1b259 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 30 Mar 2025 20:06:35 +0100 Subject: [PATCH 07/23] (feat) Flagsmith resolveObjectValue should not call json_decode on defaultValue Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/src/FlagsmithProvider.php | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 17723508..2ee0997d 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -16,6 +16,7 @@ use OpenFeature\interfaces\flags\EvaluationContext; use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; +use OpenFeature\interfaces\provider\Reason; use OpenFeature\interfaces\provider\ResolutionDetails; use function is_null; @@ -64,12 +65,13 @@ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?Evalua $builder = new ResolutionDetailsBuilder(); try { - $value = json_decode( - $this->resolve($flagKey, $defaultValue, $context)->getValue(), - true, - flags: JSON_THROW_ON_ERROR, - ); + $value = $this->resolve($flagKey, $defaultValue, $context)->getValue(); + + if ($value !== $defaultValue) { + $value = json_decode($value, true, flags: JSON_THROW_ON_ERROR); + } + // Valid JSON document might not be an array, so error in this case. if (!is_array($value)) { throw new InvalidArgumentException("Flag [$flagKey] value must be a JSON encoded array"); } @@ -93,14 +95,20 @@ protected function resolve(string $flagKey, mixed $defaultValue, ?EvaluationCont try { $flag = $this->contextualFlagStore($context)->getFlag($flagKey); - $builder->withValue( - $flag->getEnabled() ? $flag->getValue() : $defaultValue, - ); + if ($flag->getEnabled()) { + $builder->withValue($flag->getValue()); + } else { + $builder + ->withValue($defaultValue) + ->withReason(Reason::DISABLED); + } } catch (FlagsmithThrowable $throwable) { - $builder->withValue($defaultValue); - $builder->withError( - new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()), - ); + $builder + ->withValue($defaultValue) + ->withReason(Reason::ERROR) + ->withError( + new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()), + ); } return $builder->build(); From 4bce1483af4ca8c19e00f532a4c637d35b0a51b3 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 30 Mar 2025 20:10:51 +0100 Subject: [PATCH 08/23] (style) FlagsmithProvider linting Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/src/FlagsmithProvider.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 2ee0997d..224468f8 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -19,7 +19,11 @@ use OpenFeature\interfaces\provider\Reason; use OpenFeature\interfaces\provider\ResolutionDetails; +use function is_array; use function is_null; +use function json_decode; + +use const JSON_THROW_ON_ERROR; class FlagsmithProvider extends AbstractProvider implements Provider { @@ -77,7 +81,7 @@ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?Evalua } $builder->withValue($value); - } catch (JsonException|InvalidArgumentException $exception) { + } catch (JsonException | InvalidArgumentException $exception) { $builder ->withValue($defaultValue) ->withError( From be6f4e254a915802750ec22498ec2599864bd0e9 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 30 Mar 2025 20:22:42 +0100 Subject: [PATCH 09/23] (chore) remove unused Flagsmith dependencies Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/composer.json | 7 +------ .../tests/Unit/FlagsmithProviderTest.php | 15 --------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json index 9703f7e0..3dd908e8 100644 --- a/providers/Flagsmith/composer.json +++ b/providers/Flagsmith/composer.json @@ -24,12 +24,7 @@ "php": "^8", "ext-json": "*", "flagsmith/flagsmith-php-client": "^4.1", - "open-feature/sdk": "^2.0", - "php-http/httplug": "^2.3.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^2.0", - "psr/log": "^2.0 || ^3.0" + "open-feature/sdk": "^2.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.25", diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index ac9c26dd..557e072e 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -5,30 +5,15 @@ namespace OpenFeature\Providers\Flagsmith\Test\Unit; use Flagsmith\Flagsmith; -use OpenFeature\Providers\Flagsmith\Config\ConfigFactory; -use OpenFeature\Providers\Flagsmith\Config\HttpConfig; use OpenFeature\Providers\Flagsmith\FlagsmithProvider; use OpenFeature\Providers\Flagsmith\Test\TestCase; use OpenFeature\interfaces\provider\Provider; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\StreamInterface; class FlagsmithProviderTest extends TestCase { public function testCanBeInstantiated(): void { // Given - $config = [ - 'httpConfig' => [ - 'client' => $this->mockery(ClientInterface::class), - 'requestFactory' => $this->mockery(RequestFactoryInterface::class), - 'streamFactory' => $this->mockery(StreamFactoryInterface::class), - ], - ]; $flagsmith = new Flagsmith('dummy-key'); // When From 25eaa2adf0c049ab264dcf11bee46e647383ffcd Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 30 Mar 2025 20:56:33 +0100 Subject: [PATCH 10/23] (chore) fix FlagsmithProvider complaints Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/src/FlagsmithProvider.php | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 224468f8..6052f2c2 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -4,6 +4,7 @@ namespace OpenFeature\Providers\Flagsmith; +use DateTime; use Flagsmith\Exceptions\FlagsmithThrowable; use Flagsmith\Flagsmith; use Flagsmith\Models\Flags; @@ -21,6 +22,7 @@ use function is_array; use function is_null; +use function is_string; use function json_decode; use const JSON_THROW_ON_ERROR; @@ -71,7 +73,8 @@ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?Evalua try { $value = $this->resolve($flagKey, $defaultValue, $context)->getValue(); - if ($value !== $defaultValue) { + if ($value !== $defaultValue && is_string($value)) { + /** @var array|bool|DateTime|float|int|string|null $value */ $value = json_decode($value, true, flags: JSON_THROW_ON_ERROR); } @@ -92,15 +95,24 @@ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?Evalua return $builder->build(); } - protected function resolve(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails - { + /** + * @param array|bool|DateTime|float|int|string|null $defaultValue + */ + protected function resolve( + string $flagKey, + array | bool | DateTime | float | int | string | null $defaultValue, + ?EvaluationContext $context = null, + ): ResolutionDetails { $builder = new ResolutionDetailsBuilder(); try { $flag = $this->contextualFlagStore($context)->getFlag($flagKey); if ($flag->getEnabled()) { - $builder->withValue($flag->getValue()); + /** @var array|bool|DateTime|float|int|string|null $value */ + $value = $flag->getValue(); + + $builder->withValue($value); } else { $builder ->withValue($defaultValue) From 9d2c70ebf42e0f4b3faae46c80a115228c899286 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Sun, 30 Mar 2025 20:56:45 +0100 Subject: [PATCH 11/23] (chore) fix FlagsmithProviderTest Signed-off-by: Chris Lightfoot-Wild --- .../tests/Fixtures/TestOfflineHandler.php | 22 +++++++++++++++++++ .../tests/Unit/FlagsmithProviderTest.php | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php diff --git a/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php new file mode 100644 index 00000000..9d7182ef --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php @@ -0,0 +1,22 @@ +environmentModel; + } +} diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index 557e072e..d347bbed 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -6,6 +6,7 @@ use Flagsmith\Flagsmith; use OpenFeature\Providers\Flagsmith\FlagsmithProvider; +use OpenFeature\Providers\Flagsmith\Test\Fixtures\TestOfflineHandler; use OpenFeature\Providers\Flagsmith\Test\TestCase; use OpenFeature\interfaces\provider\Provider; @@ -14,7 +15,7 @@ class FlagsmithProviderTest extends TestCase public function testCanBeInstantiated(): void { // Given - $flagsmith = new Flagsmith('dummy-key'); + $flagsmith = new Flagsmith('dummy-key', offlineMode: true, offlineHandler: new TestOfflineHandler()); // When $instance = new FlagsmithProvider($flagsmith); From 2a8e686be5bedc5b16e96d2b9accdb2db7222020 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Mon, 31 Mar 2025 23:16:56 +0100 Subject: [PATCH 12/23] (chore) bunp Flagsmith minimum PHP version to 8.1 Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json index 3dd908e8..cf318bab 100644 --- a/providers/Flagsmith/composer.json +++ b/providers/Flagsmith/composer.json @@ -21,7 +21,7 @@ } ], "require": { - "php": "^8", + "php": "^8.1", "ext-json": "*", "flagsmith/flagsmith-php-client": "^4.1", "open-feature/sdk": "^2.0" From 1d9187eaec15a741bfc5208be45224ebc7390242 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Mon, 31 Mar 2025 23:19:18 +0100 Subject: [PATCH 13/23] (style) fix Flagsmith TestOfflineHandler Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php index 9d7182ef..c8879607 100644 --- a/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php +++ b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php @@ -12,7 +12,6 @@ class TestOfflineHandler implements IOfflineHandler public function __construct( private ?EnvironmentModel $environmentModel = null, ) { - } public function getEnvironment(): ?EnvironmentModel From e0bf3b647208d5f79fd502eed05787b6e6368f2b Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Mon, 31 Mar 2025 23:20:42 +0100 Subject: [PATCH 14/23] (test) added resolveBooleanValue tests to FlagsmithProvider Signed-off-by: Chris Lightfoot-Wild --- .../tests/Fixtures/environments/boolean.json | 67 +++++++++++++++++++ .../tests/Unit/FlagsmithProviderTest.php | 60 ++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 providers/Flagsmith/tests/Fixtures/environments/boolean.json diff --git a/providers/Flagsmith/tests/Fixtures/environments/boolean.json b/providers/Flagsmith/tests/Fixtures/environments/boolean.json new file mode 100644 index 00000000..4f92a246 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/boolean.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "some_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "disabled_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index d347bbed..32d7414b 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -4,24 +4,78 @@ namespace OpenFeature\Providers\Flagsmith\Test\Unit; +use Flagsmith\Engine\Environments\EnvironmentModel; use Flagsmith\Flagsmith; use OpenFeature\Providers\Flagsmith\FlagsmithProvider; use OpenFeature\Providers\Flagsmith\Test\Fixtures\TestOfflineHandler; use OpenFeature\Providers\Flagsmith\Test\TestCase; +use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; +use function file_get_contents; +use function json_decode; + class FlagsmithProviderTest extends TestCase { + protected function buildProvider(string $environmentModelPath): Provider + { + /** @var string $encoded */ + $encoded = file_get_contents($environmentModelPath); + $modelData = json_decode($encoded,); + + // @phpstan-ignore-next-line EnvironmentModel::build() type-hint is string but implementation expects object + $offlineHandler = new TestOfflineHandler(EnvironmentModel::build($modelData)); + $flagsmith = new Flagsmith('dummy-key', offlineMode: true, offlineHandler: $offlineHandler); + + return new FlagsmithProvider($flagsmith); + } + public function testCanBeInstantiated(): void { // Given $flagsmith = new Flagsmith('dummy-key', offlineMode: true, offlineHandler: new TestOfflineHandler()); // When - $instance = new FlagsmithProvider($flagsmith); + $provider = new FlagsmithProvider($flagsmith); + + // Then + $this->assertInstanceOf(Provider::class, $provider); + } + + public function testBooleanResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json'); + + // When + $resolutionDetails = $provider->resolveBooleanValue('some_feature', false); + + // Then + $this->assertTrue($resolutionDetails->getValue()); + } + + public function testBooleanResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json'); + + // When + $resolutionDetails = $provider->resolveBooleanValue('disabled_feature', true); + + // Then + $this->assertFalse($resolutionDetails->getValue()); + } + + public function testBooleanResolutionWithDefaultValueFromFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json'); + + // When + $resolutionDetails = $provider->resolveBooleanValue('missing_feature', false); // Then - $this->assertNotNull($instance); - $this->assertInstanceOf(Provider::class, $instance); + $this->assertFalse($resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); } } From 381b541bb8ef855b18c6b248e0bbbb16f2d86d21 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 1 Apr 2025 00:01:40 +0100 Subject: [PATCH 15/23] (test) added resolveStringValue tests to FlagsmithProvider Signed-off-by: Chris Lightfoot-Wild --- .../tests/Fixtures/environments/string.json | 67 +++++++++++++++++++ .../tests/Unit/FlagsmithProviderTest.php | 39 +++++++++++ 2 files changed, 106 insertions(+) create mode 100644 providers/Flagsmith/tests/Fixtures/environments/string.json diff --git a/providers/Flagsmith/tests/Fixtures/environments/string.json b/providers/Flagsmith/tests/Fixtures/environments/string.json new file mode 100644 index 00000000..37259d05 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/string.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": "flag value", + "id": 1, + "feature": { + "name": "string_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "disabled_string_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index 32d7414b..5c511835 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -11,6 +11,7 @@ use OpenFeature\Providers\Flagsmith\Test\TestCase; use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; +use OpenFeature\interfaces\provider\Reason; use function file_get_contents; use function json_decode; @@ -78,4 +79,42 @@ public function testBooleanResolutionWithDefaultValueFromFlag(): void $this->assertFalse($resolutionDetails->getValue()); $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); } + + public function testStringResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json'); + + // When + $resolutionDetails = $provider->resolveStringValue('string_feature', 'default value'); + + // Then + $this->assertEquals('flag value', $resolutionDetails->getValue()); + } + + public function testStringResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json'); + + // When + $resolutionDetails = $provider->resolveStringValue('disabled_string_feature', 'default value'); + + // Then + $this->assertEquals('default value', $resolutionDetails->getValue()); + } + + public function testStringResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json'); + + // When + $resolutionDetails = $provider->resolveStringValue('missing_string_feature', 'default value'); + + // Then + $this->assertEquals('default value', $resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); + } } From 945fb943fbb7dff7876564b85c2fc993dcb6210c Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 1 Apr 2025 00:03:16 +0100 Subject: [PATCH 16/23] (style) fix FlagsmithProviderTest trailing comma Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index 5c511835..1964ccfa 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -22,7 +22,7 @@ protected function buildProvider(string $environmentModelPath): Provider { /** @var string $encoded */ $encoded = file_get_contents($environmentModelPath); - $modelData = json_decode($encoded,); + $modelData = json_decode($encoded); // @phpstan-ignore-next-line EnvironmentModel::build() type-hint is string but implementation expects object $offlineHandler = new TestOfflineHandler(EnvironmentModel::build($modelData)); From a33f93ce44b74f581cc16704701f2986778d9edb Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 1 Apr 2025 00:35:09 +0100 Subject: [PATCH 17/23] (test) added resolveIntegerValue tests to FlagsmithProvider Signed-off-by: Chris Lightfoot-Wild --- .../tests/Fixtures/environments/integer.json | 67 +++++++++++++++++++ .../tests/Unit/FlagsmithProviderTest.php | 38 +++++++++++ 2 files changed, 105 insertions(+) create mode 100644 providers/Flagsmith/tests/Fixtures/environments/integer.json diff --git a/providers/Flagsmith/tests/Fixtures/environments/integer.json b/providers/Flagsmith/tests/Fixtures/environments/integer.json new file mode 100644 index 00000000..90d22f1d --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/integer.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": 2, + "id": 1, + "feature": { + "name": "integer_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": -1, + "id": 1, + "feature": { + "name": "disabled_integer_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index 1964ccfa..f1bd3e2f 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -117,4 +117,42 @@ public function testStringResolutionWithMissingFlag(): void $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); } + + public function testIntegerResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json'); + + // When + $resolutionDetails = $provider->resolveIntegerValue('integer_feature', 1); + + // Then + $this->assertEquals(2, $resolutionDetails->getValue()); + } + + public function testIntegerResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json'); + + // When + $resolutionDetails = $provider->resolveIntegerValue('disabled_integer_feature', 456); + + // Then + $this->assertEquals(456, $resolutionDetails->getValue()); + } + + public function testIntegerResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json'); + + // When + $resolutionDetails = $provider->resolveIntegerValue('missing_integer_feature', 123); + + // Then + $this->assertEquals(123, $resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); + } } From 9b8b892c55f81c1bb42b17c84006ce969ae862d3 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 1 Apr 2025 00:42:34 +0100 Subject: [PATCH 18/23] (test) added resolveFloatValue tests to FlagsmithProvider Signed-off-by: Chris Lightfoot-Wild --- .../tests/Fixtures/environments/float.json | 67 +++++++++++++++++++ .../tests/Unit/FlagsmithProviderTest.php | 38 +++++++++++ 2 files changed, 105 insertions(+) create mode 100644 providers/Flagsmith/tests/Fixtures/environments/float.json diff --git a/providers/Flagsmith/tests/Fixtures/environments/float.json b/providers/Flagsmith/tests/Fixtures/environments/float.json new file mode 100644 index 00000000..06b86fb0 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/float.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": 2.345, + "id": 1, + "feature": { + "name": "float_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": -1.0, + "id": 1, + "feature": { + "name": "disabled_float_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index f1bd3e2f..1659f4c0 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -155,4 +155,42 @@ public function testIntegerResolutionWithMissingFlag(): void $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); } + + public function testFloatResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json'); + + // When + $resolutionDetails = $provider->resolveFloatValue('float_feature', 1.0); + + // Then + $this->assertEquals(2.345, $resolutionDetails->getValue()); + } + + public function testFloatResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json'); + + // When + $resolutionDetails = $provider->resolveFloatValue('disabled_float_feature', 9.99); + + // Then + $this->assertEquals(9.99, $resolutionDetails->getValue()); + } + + public function testFloatResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json'); + + // When + $resolutionDetails = $provider->resolveFloatValue('missing_float_feature', 0.123); + + // Then + $this->assertEquals(0.123, $resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); + } } From f10470cdb3a85b3c9f1dc57d8bcab24002749b9f Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 1 Apr 2025 00:54:51 +0100 Subject: [PATCH 19/23] (test) added resolveObjectValue tests to FlagsmithProvider Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/src/FlagsmithProvider.php | 3 +- .../tests/Fixtures/environments/object.json | 79 +++++++++++++++++++ .../tests/Unit/FlagsmithProviderTest.php | 48 +++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 providers/Flagsmith/tests/Fixtures/environments/object.json diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 6052f2c2..2f18a2ea 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -20,6 +20,7 @@ use OpenFeature\interfaces\provider\Reason; use OpenFeature\interfaces\provider\ResolutionDetails; +use function array_is_list; use function is_array; use function is_null; use function is_string; @@ -79,7 +80,7 @@ public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?Evalua } // Valid JSON document might not be an array, so error in this case. - if (!is_array($value)) { + if (!is_array($value) || array_is_list($value)) { throw new InvalidArgumentException("Flag [$flagKey] value must be a JSON encoded array"); } diff --git a/providers/Flagsmith/tests/Fixtures/environments/object.json b/providers/Flagsmith/tests/Fixtures/environments/object.json new file mode 100644 index 00000000..7dcbb52d --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/object.json @@ -0,0 +1,79 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": "{\"key\":\"value\"}", + "id": 1, + "feature": { + "name": "object_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "disabled_object_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "[\"foo\",\"bar\"]", + "id": 1, + "feature": { + "name": "invalid_object_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php index 1659f4c0..6c140701 100644 --- a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -193,4 +193,52 @@ public function testFloatResolutionWithMissingFlag(): void $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); } + + public function testObjectResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('object_feature', ['a' => 'b']); + + // Then + $this->assertEquals(['key' => 'value'], $resolutionDetails->getValue()); + } + + public function testObjectResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('disabled_object_feature', ['a' => 'b']); + + // Then + $this->assertEquals(['a' => 'b'], $resolutionDetails->getValue()); + } + + public function testObjectResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('missing_object_feature', ['c' => 3, 'p' => 'o']); + + // Then + $this->assertEquals(['c' => 3, 'p' => 'o'], $resolutionDetails->getValue()); + } + + public function testObjectResolutionWithEnabledFlagWithInvalidValue(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('invalid_object_feature', ['default' => 'value']); + + // Then + $this->assertEquals(['default' => 'value'], $resolutionDetails->getValue()); + } } From 2c931b38fe5b414315273063f1446636147e34a9 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 22 Apr 2025 05:41:13 +0100 Subject: [PATCH 20/23] Update providers/Flagsmith/src/FlagsmithProvider.php Co-authored-by: Michael Beemer Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/src/FlagsmithProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php index 2f18a2ea..7fb620bf 100644 --- a/providers/Flagsmith/src/FlagsmithProvider.php +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -30,7 +30,7 @@ class FlagsmithProvider extends AbstractProvider implements Provider { - protected static string $NAME = 'FlagsmithProvider'; + protected static string $NAME = 'Flagsmith'; public function __construct( private Flagsmith $flagsmith, From 69de543f6745a5402c04f20470a0a6fde1a18690 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 22 Apr 2025 06:20:21 +0100 Subject: [PATCH 21/23] chore(deps) updated flagsmith/flagsmith-php-client for FlagsmithProvider Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json index cf318bab..f7030fdf 100644 --- a/providers/Flagsmith/composer.json +++ b/providers/Flagsmith/composer.json @@ -22,8 +22,7 @@ ], "require": { "php": "^8.1", - "ext-json": "*", - "flagsmith/flagsmith-php-client": "^4.1", + "flagsmith/flagsmith-php-client": "^4.5.1", "open-feature/sdk": "^2.0" }, "require-dev": { From 3f6fdcaf9331ccdf708d0c967ff8ac8f3600e78d Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 22 Apr 2025 06:37:52 +0100 Subject: [PATCH 22/23] fix: use correct PHP runtime in composer scripts Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/composer.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json index f7030fdf..3d75c93c 100644 --- a/providers/Flagsmith/composer.json +++ b/providers/Flagsmith/composer.json @@ -81,8 +81,8 @@ "@dev:analyze:phpstan", "@dev:analyze:psalm" ], - "dev:analyze:phpstan": "phpstan analyse --ansi --debug --memory-limit=512M", - "dev:analyze:psalm": "psalm", + "dev:analyze:phpstan": "@php vendor/bin/phpstan analyse --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "@php vendor/bin/psalm", "dev:build:clean": "git clean -fX build/", "dev:grpc": [ "@dev:grpc:init", @@ -96,19 +96,19 @@ "@dev:lint:syntax", "@dev:lint:style" ], - "dev:lint:fix": "phpcbf", - "dev:lint:style": "phpcs --colors", - "dev:lint:syntax": "parallel-lint --colors src/ tests/", + "dev:lint:fix": "@php vendor/bin/phpcbf", + "dev:lint:style": "@php vendor/bin/phpcs --colors", + "dev:lint:syntax": "@php vendor/bin/parallel-lint --colors src/ tests/", "dev:test": [ "@dev:lint", "@dev:analyze", "@dev:test:unit", "@dev:test:integration" ], - "dev:test:coverage:ci": "phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", - "dev:test:coverage:html": "phpunit --colors=always --coverage-html build/coverage/coverage-html/", - "dev:test:unit": "phpunit --colors=always --testdox", - "dev:test:unit:debug": "phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "dev:test:coverage:ci": "@php vendor/bin/phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", + "dev:test:coverage:html": "@php vendor/bin/phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": "@php vendor/bin/phpunit --colors=always --testdox", + "dev:test:unit:debug": "@php vendor/bin/phpunit --colors=always --testdox -d xdebug.profiler_enable=on", "dev:test:unit:setup": "echo 'Setup for unit tests...'", "dev:test:unit:teardown": "echo 'Tore down unit tests...'", "dev:test:integration:setup": "echo 'Setup for integration tests...'", From a89fa7965c9669874a254b34fbb462443f1e8661 Mon Sep 17 00:00:00 2001 From: Chris Lightfoot-Wild Date: Tue, 22 Apr 2025 06:44:20 +0100 Subject: [PATCH 23/23] fix: update FlagsmithProvider supported PHP versions in README Signed-off-by: Chris Lightfoot-Wild --- providers/Flagsmith/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/Flagsmith/README.md b/providers/Flagsmith/README.md index 28596345..394997f8 100644 --- a/providers/Flagsmith/README.md +++ b/providers/Flagsmith/README.md @@ -32,7 +32,7 @@ OpenFeatureAPI::setProvider(new FlagsmithProvider($flagsmith)); ### PHP Versioning -This library targets PHP version and newer. As long as you have any compatible version of PHP on your system you should be able to utilize the OpenFeature SDK. +This library targets PHP 8.1 and above. As long as you have a compatible version of PHP on your system, you should be able to utilize the OpenFeature SDK. This package also has a `.tool-versions` file for use with PHP version managers like `asdf`.