diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 813e2b5..fefff01 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-version: ['8.0', '8.1', '8.2'] + php-version: ['8.1', '8.2', '8.3'] steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 0199447..bea6a4d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ /features/*.feature -/.devenv* \ No newline at end of file +/.direnv \ No newline at end of file diff --git a/.tool-versions b/.tool-versions index 89eae35..19a822a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -php 8.2.12 +php 8.3.0 +# php 8.2.12 # php 8.1.11 -# php 8.0.24 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91dccbe..ac3aa9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,11 +4,11 @@ ### System Requirements -PHP 8+ is required. +PHP 8.1+ is required. ### Compilation target(s) -We target compatibility with PHP versions 8.0, 8.1, and 8.2. +We target compatibility with supported versions of PHP. Currently, this includes PHP versions 8.1, 8.2, and 8.3. ### Installation and Dependencies @@ -16,6 +16,18 @@ Install dependencies with `composer install`. We value having as few runtime dependencies as possible. The addition of any dependencies requires careful consideration and review. +#### asdf + +This package has a `.tool-versions` file for use with PHP version managers like `asdf`. + +#### Nix + +This package includes a `flake.nix` file which defines reproducible development shells powered by [Nix](https://nixos.org/). You can manually drop into a shell with `nix develop`, or provide a specific PHP minor version target with `nix develop .#php82`. + +#### direnv + +This package includes a `.envrc` file which automatically infers the usage of the default shell for the project, which is set to the minimum supported version of PHP for the project. + ### Testing Run tests with `composer dev:test`. diff --git a/README.md b/README.md index 5ab73e2..c5fba03 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,15 @@

OpenFeature PHP SDK

+[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![codecov](https://codecov.io/gh/open-feature/php-sdk/branch/main/graph/badge.svg?token=3DC5XOEHMY)](https://codecov.io/gh/open-feature/php-sdk) +[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.5.1&color=yellow)](https://github.com/open-feature/spec/tree/v0.5.1) +[![Latest Stable Version](http://poser.pugx.org/open-feature/sdk/v)](https://packagist.org/packages/open-feature/sdk) +[![Total Downloads](http://poser.pugx.org/open-feature/sdk/downloads)](https://packagist.org/packages/open-feature/sdk) +![PHP 8.1+](https://img.shields.io/badge/php->=8.0-blue.svg) +[![License](http://poser.pugx.org/open-feature/sdk/license)](https://packagist.org/packages/open-feature/sdk) +[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/6853/badge)](https://bestpractices.coreinfrastructure.org/projects/6853) +

diff --git a/composer.json b/composer.json index d57931c..4c79d16 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,10 @@ } ], "require": { - "php": "^8", + "php": "^8.1", + "league/event": "^3.0", "myclabs/php-enum": "^1.8", + "psr/event-dispatcher": "^1.0", "psr/log": "^2.0 || ^3.0" }, "require-dev": { @@ -56,10 +58,10 @@ }, "config": { "allow-plugins": { - "phpstan/extension-installer": true, + "captainhook/plugin-composer": true, "dealerdirect/phpcodesniffer-composer-installer": true, "ergebnis/composer-normalize": true, - "captainhook/plugin-composer": true, + "phpstan/extension-installer": true, "ramsey/composer-repl": true }, "sort-packages": true diff --git a/devenv.lock b/devenv.lock deleted file mode 100644 index a79b9f0..0000000 --- a/devenv.lock +++ /dev/null @@ -1,152 +0,0 @@ -{ - "nodes": { - "devenv": { - "locked": { - "dir": "src/modules", - "lastModified": 1675875772, - "narHash": "sha256-sYXHPZ4tsjdG+UXK0mYnABhiS/RuzHiV9uGOU9YakwE=", - "owner": "cachix", - "repo": "devenv", - "rev": "eac5eb12eb42765f5f252972dc876d1f96b03dfe", - "type": "github" - }, - "original": { - "dir": "src/modules", - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-utils": { - "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1660459072, - "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1675758091, - "narHash": "sha256-7gFSQbSVAFUHtGCNHPF7mPc5CcqDk9M2+inlVPZSneg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "747927516efcb5e31ba03b7ff32f61f6d47e7d87", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1673800717, - "narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-22.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1671271357, - "narHash": "sha256-xRJdLbWK4v2SewmSStYrcLa0YGJpleufl44A19XSW8k=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "40f79f003b6377bd2f4ed4027dde1f8f922995dd", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": "nixpkgs_2", - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1675688762, - "narHash": "sha256-oit/SxMk0B380ASuztBGQLe8TttO1GJiXF8aZY9AYEc=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "ab608394886fb04b8a5df3cb0bab2598400e3634", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, - "root": { - "inputs": { - "devenv": "devenv", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/devenv.nix b/devenv.nix deleted file mode 100644 index 077cf85..0000000 --- a/devenv.nix +++ /dev/null @@ -1,52 +0,0 @@ -{ pkgs, ... }: - -{ - - # https://devenv.sh/packages/ - packages = [ pkgs.git ]; - - # https://devenv.sh/languages/ - languages.nix.enable = true; - languages.php.enable = true; - languages.php.package = pkgs.php80; - - # https://devenv.sh/basics/ - env.PROJECT_NAME = "openfeature-php-sdk"; - - # https://devenv.sh/scripts/ - scripts.hello.exec = "echo $ Started devenv shell in $PROJECT_NAME"; - - enterShell = '' - hello - echo - git --version - php --version - echo - - # optimization step -- files and directories that match entries - # in the .gitignore will still be traversed, and the .devenv - # directory contains over 5000 files and 121MB. - if ! grep -E "excludesfile.+\.gitignore" .git/config &>/dev/null - then - git config --local core.excludesfile .gitignore - fi - ''; - - ## https://devenv.sh/pre-commit-hooks/ - pre-commit.hooks = { - # # general formatting - # prettier.enable = true; - # github actions - actionlint.enable = true; - # nix - deadnix.enable = true; - nixfmt.enable = true; - # php - phpcbf.enable = true; - # # ensure Markdown code is executable - # mdsh.enable = true; - }; - - # https://devenv.sh/processes/ - # processes.ping.exec = "ping example.com"; -} diff --git a/devenv.yaml b/devenv.yaml deleted file mode 100644 index 292df95..0000000 --- a/devenv.yaml +++ /dev/null @@ -1,5 +0,0 @@ -inputs: - nixpkgs: - url: github:NixOS/nixpkgs/nixpkgs-unstable - pre-commit-hooks: - url: github:cachix/pre-commit-hooks.nix \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1dc06f2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1714441809, + "narHash": "sha256-+iU3vNtIlowMjw0aQ5PcrIbMRZKPOkzJVdTNjMaJ+xI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5610faa07ef6fa11035712f53d49290f6cb525d6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fadcf88 --- /dev/null +++ b/flake.nix @@ -0,0 +1,94 @@ +{ + description = "OpenFeature PHP SDK Nix flake dev shells"; + + # Flake inputs + inputs.nixpkgs.url = "github:NixOS/nixpkgs"; + + # Flake outputs + outputs = { self, nixpkgs }: + let + # Systems supported + allSystems = [ + "x86_64-linux" # 64-bit Intel/ARM Linux + "aarch64-linux" # 64-bit AMD Linux + "x86_64-darwin" # 64-bit Intel/ARM macOS + "aarch64-darwin" # 64-bit Apple Silicon + ]; + + # Helper to provide system-specific attributes + nameValuePair = name: value: { inherit name value; }; + genAttrs = names: f: builtins.listToAttrs (map (n: nameValuePair n (f n)) names); + forAllSystems = f: genAttrs allSystems (system: f { + pkgs = import nixpkgs { + inherit system; + }; + }); + in + { + # Development environment output + devShells = forAllSystems ({ pkgs }: + let + coreShellPackages = [ + pkgs.zsh + ]; + coreDevPackages = [ + pkgs.git + ]; + corePhpPackages = [ + pkgs.libpng + ]; + php81Packages = [ + pkgs.php81 + pkgs.php81.packages.composer + ]; + php82Packages = [ + pkgs.php82 + pkgs.php82.packages.composer + ]; + php83Packages = [ + pkgs.php83 + pkgs.php83.packages.composer + ]; + emptyStr = ""; + shellHookCommandFactory = { git ? true, php ? false, node ? false, yarn ? false, pnpm ? false, python ? false, bun ? false }: '' + echo $ Started devenv shell for OpenFeature PHP-SDK with $PHP_VERSION + echo + ${if git then ''git --version'' else emptyStr} + ${if php then ''php --version'' else emptyStr} + echo + ''; + phpShellHookCommand = shellHookCommandFactory { php = true; }; + in rec + { + php81 = pkgs.mkShell { + packages = coreShellPackages ++ coreDevPackages ++ corePhpPackages ++ php81Packages; + + PHP_VERSION = "PHP81"; + + shellHook = phpShellHookCommand; + }; + + php82 = pkgs.mkShell { + packages = coreShellPackages ++ coreDevPackages ++ corePhpPackages ++ php82Packages; + + PHP_VERSION = "PHP82"; + + shellHook = phpShellHookCommand; + }; + + php83 = pkgs.mkShell { + packages = coreShellPackages ++ coreDevPackages ++ corePhpPackages ++ php83Packages; + + PHP_VERSION = "PHP83"; + + shellHook = phpShellHookCommand; + }; + + # Default aliases, uses minimum supported PHP version + php = php81; + + default = php; + } + ); + }; +} \ No newline at end of file diff --git a/integration/composer.json b/integration/composer.json index 906f60d..b39c551 100644 --- a/integration/composer.json +++ b/integration/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^8", + "php": "^8.1", "open-feature/sdk": "^2.0.0", "open-feature/flagd-provider": "^0.7.0", "guzzlehttp/guzzle": "^7.5", diff --git a/integration/features/bootstrap/FeatureContext.php b/integration/features/bootstrap/FeatureContext.php index 78040bf..570f9a2 100644 --- a/integration/features/bootstrap/FeatureContext.php +++ b/integration/features/bootstrap/FeatureContext.php @@ -82,7 +82,7 @@ public function aProviderIsRegisteredWithCacheDisabled() */ public function aBooleanFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, bool $defaultValue) { - $this->flagType = FlagValueType::BOOLEAN; + $this->flagType = FlagValueType::Boolean; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -103,7 +103,7 @@ public function theResolvedBooleanValueShouldBe(bool $resolvedValue) */ public function aStringFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, string $defaultValue) { - $this->flagType = FlagValueType::STRING; + $this->flagType = FlagValueType::String; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -124,7 +124,7 @@ public function theResolvedStringValueShouldBe(string $resolvedValue) */ public function anIntegerFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, int $defaultValue) { - $this->flagType = FlagValueType::INTEGER; + $this->flagType = FlagValueType::Integer; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; print_r("Setting integer...\n"); @@ -146,7 +146,7 @@ public function theResolvedIntegerValueShouldBe(int $resolvedValue) */ public function aFloatFlagWithKeyIsEvaluatedWithDefaultValue(string $flagKey, float $defaultValue) { - $this->flagType = FlagValueType::FLOAT; + $this->flagType = FlagValueType::Float; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -167,7 +167,7 @@ public function theResolvedFloatValueShouldBe(float $resolvedValue) */ public function anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue(string $flagKey, mixed $defaultValue) { - $this->flagType = FlagValueType::OBJECT; + $this->flagType = FlagValueType::Object; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -192,7 +192,7 @@ public function theResolvedObjectValueShouldBeContainFieldsAndWithValuesAndRespe */ public function aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, bool $defaultValue) { - $this->flagType = FlagValueType::BOOLEAN; + $this->flagType = FlagValueType::Boolean; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -214,7 +214,7 @@ public function theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheRe */ public function aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, string $defaultValue) { - $this->flagType = FlagValueType::STRING; + $this->flagType = FlagValueType::String; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -236,7 +236,7 @@ public function theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheRea */ public function anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, int $defaultValue) { - $this->flagType = FlagValueType::INTEGER; + $this->flagType = FlagValueType::Integer; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -258,7 +258,7 @@ public function theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheRe */ public function aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue(string $flagKey, float $defaultValue) { - $this->flagType = FlagValueType::FLOAT; + $this->flagType = FlagValueType::Float; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -280,7 +280,7 @@ public function theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReas */ public function anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue(string $flagKey, mixed $defaultValue) { - $this->flagType = FlagValueType::OBJECT; + $this->flagType = FlagValueType::Object; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -409,7 +409,7 @@ public function theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMiss */ public function aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue(string $flagKey, int $defaultValue) { - $this->flagType = FlagValueType::INTEGER; + $this->flagType = FlagValueType::Integer; $this->inputFlagKey = $flagKey; $this->inputFlagDefaultValue = $defaultValue; } @@ -447,23 +447,23 @@ private function calculateValue() { $value = null; switch ($this->flagType) { - case FlagValueType::BOOLEAN: + case FlagValueType::Boolean: $value = $this->client->getBooleanValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::FLOAT: + case FlagValueType::Float: $value = $this->client->getFloatValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::INTEGER: + case FlagValueType::Integer: $value = $this->client->getIntegerValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::OBJECT: + case FlagValueType::Object: $value = $this->client->getObjectValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::STRING: + case FlagValueType::String: $value = $this->client->getStringValue($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; @@ -479,23 +479,23 @@ private function calculateDetails(): EvaluationDetails { $details = null; switch ($this->flagType) { - case FlagValueType::BOOLEAN: + case FlagValueType::Boolean: $details = $this->client->getBooleanDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::FLOAT: + case FlagValueType::Float: $details = $this->client->getFloatDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::INTEGER: + case FlagValueType::Integer: $details = $this->client->getIntegerDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::OBJECT: + case FlagValueType::Object: $details = $this->client->getObjectDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; - case FlagValueType::STRING: + case FlagValueType::String: $details = $this->client->getStringDetails($this->inputFlagKey, $this->inputFlagDefaultValue, $this->inputContext, $this->inputOptions); break; @@ -518,23 +518,23 @@ private function setFlagTypeIfNullByValue(mixed $value): void private function getFlagTypeOf(mixed $value): ?string { if (is_string($value)) { - return FlagValueType::STRING; + return FlagValueType::String; } if (is_array($value)) { - return FlagValueType::OBJECT; + return FlagValueType::Object; } if (is_float($value)) { - return FlagValueType::FLOAT; + return FlagValueType::Float; } if (is_int($value)) { - return FlagValueType::INTEGER; + return FlagValueType::Integer; } if (is_bool($value)) { - return FlagValueType::BOOLEAN; + return FlagValueType::Boolean; } } diff --git a/src/OpenFeatureAPI.php b/src/OpenFeatureAPI.php index 8971509..3e761f5 100644 --- a/src/OpenFeatureAPI.php +++ b/src/OpenFeatureAPI.php @@ -4,31 +4,50 @@ namespace OpenFeature; +use League\Event\EventDispatcher; +use OpenFeature\implementation\events\Event; use OpenFeature\implementation\flags\NoOpClient; -use OpenFeature\implementation\provider\NoOpProvider; +use OpenFeature\implementation\provider\ProviderAwareTrait; use OpenFeature\interfaces\common\LoggerAwareTrait; use OpenFeature\interfaces\common\Metadata; +use OpenFeature\interfaces\events\EventDetails; +use OpenFeature\interfaces\events\ProviderEvent; use OpenFeature\interfaces\flags\API; use OpenFeature\interfaces\flags\Client; use OpenFeature\interfaces\flags\EvaluationContext; use OpenFeature\interfaces\hooks\Hook; use OpenFeature\interfaces\provider\Provider; +use OpenFeature\interfaces\provider\ProviderAware; use Psr\Log\LoggerAwareInterface; use Throwable; -use function array_merge; use function is_null; +use function key_exists; final class OpenFeatureAPI implements API, LoggerAwareInterface { use LoggerAwareTrait; + /** + * ----------------- + * Requirement 1.1.2 + * ----------------- + * The API MUST provide a function to set the global provider singleton, which + * accepts an API-conformant provider implementation. + */ + use ProviderAwareTrait; private static ?OpenFeatureAPI $instance = null; - private Provider $provider; + private EventDispatcher $dispatcher; + + /** @var (Client & ProviderAware) | null $defaultClient */ + private $defaultClient; + /** @var Array $clientMap */ + private array $clientMap = []; /** @var Hook[] $hooks */ private array $hooks = []; + private ?EvaluationContext $evaluationContext = null; /** @@ -58,29 +77,28 @@ public static function getInstance(): API */ private function __construct() { - $this->provider = new NoOpProvider(); - } - - public function getProvider(): Provider - { - return $this->provider; + $this->dispatcher = new EventDispatcher(); } /** * ----------------- - * Requirement 1.1.2 + * Requirement 1.1.3 * ----------------- - * The API MUST provide a function to set the global provider singleton, which - * accepts an API-conformant provider implementation. + * The API MUST provide a function to bind a given provider to one or more client names. + * If the client-name already has a bound provider, it is overwritten with the new mapping. */ - public function setProvider(Provider $provider): void + public function setClientProvider(string $clientDomain, Provider $provider): void { - $this->provider = $provider; + if (!isset($this->clientMap[$clientDomain])) { + return; + } + + $this->clientMap[$clientDomain]->setProvider($provider); } /** * ----------------- - * Requirement 1.1.4 + * Requirement 1.1.5 * ----------------- * The API MUST provide a function for retrieving the metadata field of the * configured provider. @@ -92,22 +110,48 @@ public function getProviderMetadata(): Metadata /** * ----------------- - * Requirement 1.1.4 + * Requirement 1.1.6 * ----------------- * The API MUST provide a function for creating a client which accepts the following options: - * name (optional): A logical string identifier for the client. + * domain (optional): A logical string identifier for the client. */ - public function getClient(?string $name = null, ?string $version = null): Client + public function getClient(?string $domain = null): Client { - $name = $name ?? 'OpenFeature'; - $version = $version ?? 'OpenFeature'; - try { - $client = new OpenFeatureClient($this, $name, $version); - $client->setLogger($this->getLogger()); + $isDefaultClient = is_null($domain); + $domain = $domain ?? self::class; + + if ($isDefaultClient) { + $currentClient = $this->defaultClient ?? null; + } else { + $currentClient = key_exists($domain, $this->clientMap) ? $this->clientMap[$domain] : null; + } + + if (!is_null($currentClient)) { + return $currentClient; + } + + try { + $client = new OpenFeatureClient($this, $domain); + $client->setLogger($this->getLogger()); + } catch (Throwable $err) { + $client = new NoOpClient(); + } + + if ($isDefaultClient) { + $this->defaultClient = $client; + } else { + $this->clientMap[$domain] = $client; + } return $client; } catch (Throwable $err) { + /** + * ----------------- + * Requirement 1.1.7 + * ----------------- + * The client creation function MUST NOT throw, or otherwise abnormally terminate. + */ return new NoOpClient(); } } @@ -122,7 +166,7 @@ public function getHooks(): array /** * ----------------- - * Requirement 1.1.3 + * Requirement 1.1.4 * ----------------- * The API MUST provide a function to add hooks which accepts one or more API-conformant * hooks, and appends them to the collection of any previously added hooks. When new @@ -130,7 +174,7 @@ public function getHooks(): array */ public function addHooks(Hook ...$hooks): void { - $this->hooks = array_merge($this->hooks, $hooks); + $this->hooks = [...$this->hooks, ...$hooks]; } public function clearHooks(): void @@ -147,4 +191,55 @@ public function setEvaluationContext(EvaluationContext $context): void { $this->evaluationContext = $context; } + + /** + * ----------------- + * Requirement 1.6.1 + * ----------------- + * The API MUST define a shutdown function which, when called, must call the respective + * shutdown function on the active provider. + */ + public function dispose(): void + { + $this->getProvider()->dispose(); + } + + /** + * ----------------- + * Requirement 5.1.1 + * ----------------- + * The provider MAY define a mechanism for signaling the occurrence of one of a set + * of events, including PROVIDER_READY, PROVIDER_ERROR, PROVIDER_CONFIGURATION_CHANGED + * and PROVIDER_STALE, with a provider event details payload. + */ + public function dispatch(ProviderEvent $providerEvent, EventDetails $eventDetails): void + { + $this->dispatcher->dispatch(new Event($providerEvent->value, $eventDetails)); + } + + public function addHandler(ProviderEvent $providerEvent, callable $handler): void + { + $this->dispatcher->subscribeTo($providerEvent->value, $handler); + } + + public function removeHandler(ProviderEvent $providerEvent, callable $handler): void + { + } + + /** + * TESTING UTILITY + */ + protected function resetClients(): void + { + $this->defaultClient = null; + $this->clientMap = []; + } + + /** + * TESTING UTILITY + */ + protected function resetProviders(): void + { + $this->provider = null; + } } diff --git a/src/OpenFeatureClient.php b/src/OpenFeatureClient.php index 4181a60..28aae6c 100644 --- a/src/OpenFeatureClient.php +++ b/src/OpenFeatureClient.php @@ -4,10 +4,8 @@ namespace OpenFeature; -use DateTime; use OpenFeature\implementation\common\Metadata; use OpenFeature\implementation\common\ValueTypeValidator; -use OpenFeature\implementation\errors\FlagValueTypeError; use OpenFeature\implementation\errors\InvalidResolutionValueError; use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\implementation\flags\EvaluationDetailsBuilder; @@ -17,7 +15,7 @@ use OpenFeature\implementation\hooks\HookContextFactory; use OpenFeature\implementation\hooks\HookExecutor; use OpenFeature\implementation\hooks\HookHints; -use OpenFeature\implementation\provider\Reason; +use OpenFeature\implementation\provider\ProviderAwareTrait; use OpenFeature\implementation\provider\ResolutionError; use OpenFeature\interfaces\common\LoggerAwareTrait; use OpenFeature\interfaces\common\Metadata as MetadataInterface; @@ -31,45 +29,37 @@ use OpenFeature\interfaces\hooks\HooksAwareTrait; use OpenFeature\interfaces\provider\ErrorCode; use OpenFeature\interfaces\provider\Provider; +use OpenFeature\interfaces\provider\ProviderAware; +use OpenFeature\interfaces\provider\Reason; use OpenFeature\interfaces\provider\ResolutionDetails; use OpenFeature\interfaces\provider\ThrowableWithResolutionError; use Psr\Log\LoggerAwareInterface; use Throwable; -use function array_merge; use function array_reverse; use function sprintf; -class OpenFeatureClient implements Client, LoggerAwareInterface +class OpenFeatureClient implements Client, LoggerAwareInterface, ProviderAware { use HooksAwareTrait; use LoggerAwareTrait; + use ProviderAwareTrait; - private API $api; - private string $name; - private string $version; private ?EvaluationContextInterface $evaluationContext = null; /** * Client for evaluating the flag. There may be multiples of these floating around. * * @param API $api Backing global singleton - * @param string $name Name of the client (used by observability tools). - * @param string $version Version of the client (used by observability tools). + * @param string $domain Domain of the client (used by observability tools). */ - public function __construct(API $api, string $name, string $version) - { - $this->api = $api; - $this->name = $name; - $this->version = $version; + public function __construct( + private readonly API $api, + private readonly string $domain, + ) { $this->hooks = []; } - public function getVersion(): string - { - return $this->version; - } - /** * Return an optional client-level evaluation context. */ @@ -102,7 +92,7 @@ public function setEvaluationContext(EvaluationContextInterface $context): void */ public function addHooks(Hook ...$hooks): void { - $this->hooks = array_merge($this->hooks, $hooks); + $this->hooks = [...$this->hooks, ...$hooks]; } /** @@ -110,14 +100,14 @@ public function addHooks(Hook ...$hooks): void * Requirement 1.2.2 * ----------------- * The client interface MUST define a metadata member or accessor, containing - * an immutable name field or accessor of type string, which corresponds to - * the name value supplied during client creation. + * an immutable domain field or accessor of type string, which corresponds to + * the domain value supplied during client creation. * * Returns the metadata for the current resource */ public function getMetadata(): MetadataInterface { - return new Metadata($this->name); + return new Metadata($this->domain); } /** @@ -146,7 +136,7 @@ public function getBooleanValue(string $flagKey, bool $defaultValue, ?Evaluation */ public function getBooleanDetails(string $flagKey, bool $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface { - return $this->evaluateFlag(FlagValueType::BOOLEAN, $flagKey, $defaultValue, $context, $options); + return $this->evaluateFlag(FlagValueType::Boolean, $flagKey, $defaultValue, $context, $options); } /** @@ -175,7 +165,7 @@ public function getStringValue(string $flagKey, string $defaultValue, ?Evaluatio */ public function getStringDetails(string $flagKey, string $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface { - return $this->evaluateFlag(FlagValueType::STRING, $flagKey, $defaultValue, $context, $options); + return $this->evaluateFlag(FlagValueType::String, $flagKey, $defaultValue, $context, $options); } /** @@ -209,7 +199,7 @@ public function getIntegerValue(string $flagKey, int $defaultValue, ?EvaluationC */ public function getIntegerDetails(string $flagKey, int $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface { - return $this->evaluateFlag(FlagValueType::INTEGER, $flagKey, $defaultValue, $context, $options); + return $this->evaluateFlag(FlagValueType::Integer, $flagKey, $defaultValue, $context, $options); } /** @@ -243,7 +233,7 @@ public function getFloatValue(string $flagKey, float $defaultValue, ?EvaluationC */ public function getFloatDetails(string $flagKey, float $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface { - return $this->evaluateFlag(FlagValueType::FLOAT, $flagKey, $defaultValue, $context, $options); + return $this->evaluateFlag(FlagValueType::Float, $flagKey, $defaultValue, $context, $options); } /** @@ -278,7 +268,7 @@ public function getObjectValue(string $flagKey, $defaultValue, ?EvaluationContex */ public function getObjectDetails(string $flagKey, $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptionsInterface $options = null): EvaluationDetailsInterface { - return $this->evaluateFlag(FlagValueType::OBJECT, $flagKey, $defaultValue, $context, $options); + return $this->evaluateFlag(FlagValueType::Object, $flagKey, $defaultValue, $context, $options); } /** @@ -287,17 +277,17 @@ public function getObjectDetails(string $flagKey, $defaultValue, ?EvaluationCont * ----------------- * Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the default value in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup. * - * @param bool|string|int|float|DateTime|mixed[]|null $defaultValue + * @param bool|string|int|float|mixed[]|null $defaultValue */ private function evaluateFlag( - string $flagValueType, + FlagValueType $flagValueType, string $flagKey, - bool | string | int | float | DateTime | array | null $defaultValue, + bool | string | int | float | array | null $defaultValue, ?EvaluationContextInterface $invocationContext = null, ?EvaluationOptionsInterface $options = null, ): EvaluationDetailsInterface { $api = $this->api; - $provider = $api->getProvider(); + $provider = $this->determineProvider(); /** @var EvaluationOptionsInterface $options */ $options = $options ?? new EvaluationOptions(); $hookHints = $options->getHookHints() ?? new HookHints(); @@ -326,14 +316,14 @@ private function evaluateFlag( // after: Provider, Invocation, Client, API // error (if applicable): Provider, Invocation, Client, API // finally: Provider, Invocation, Client, API - $mergedBeforeHooks = array_merge( - $api->getHooks(), - $this->getHooks(), - $options->getHooks(), - $provider->getHooks(), - ); + $mergedBeforeHooks = [ + ...$api->getHooks(), + ...$this->getHooks(), + ...$options->getHooks(), + ...$provider->getHooks(), + ]; - $mergedRemainingHooks = array_reverse(array_merge([], $mergedBeforeHooks)); + $mergedRemainingHooks = array_reverse([...$mergedBeforeHooks]); try { $contextFromBeforeHook = $hookExecutor->beforeHooks($flagValueType, $hookContext, $mergedBeforeHooks, $hookHints); @@ -357,7 +347,7 @@ private function evaluateFlag( ); if (!$resolutionDetails->getError() && !ValueTypeValidator::is($flagValueType, $resolutionDetails->getValue())) { - throw new InvalidResolutionValueError($flagValueType); + throw new InvalidResolutionValueError($flagValueType->value); } $details = EvaluationDetailsFactory::fromResolution($flagKey, $resolutionDetails); @@ -388,41 +378,47 @@ private function evaluateFlag( return $details; } + /** + * @param bool|string|int|float|mixed[]|null $defaultValue + */ private function createProviderEvaluation( - string $type, + FlagValueType $type, string $key, - mixed $defaultValue, + bool | string | int | float | array | null $defaultValue, Provider $provider, EvaluationContextInterface $context, ): ResolutionDetails { - switch ($type) { - case FlagValueType::BOOLEAN: - /** @var bool $defaultValue */ + switch ($type->value) { + case FlagValueType::Boolean->value: + /** @var bool $defaultValue */; $defaultValue = $defaultValue; return $provider->resolveBooleanValue($key, $defaultValue, $context); - case FlagValueType::STRING: - /** @var string $defaultValue */ + case FlagValueType::String->value: + /** @var string $defaultValue */; $defaultValue = $defaultValue; return $provider->resolveStringValue($key, $defaultValue, $context); - case FlagValueType::INTEGER: - /** @var int $defaultValue */ + case FlagValueType::Integer->value: + /** @var int $defaultValue */; $defaultValue = $defaultValue; return $provider->resolveIntegerValue($key, $defaultValue, $context); - case FlagValueType::FLOAT: - /** @var float $defaultValue */ + case FlagValueType::Float->value: + /** @var float $defaultValue */; $defaultValue = $defaultValue; return $provider->resolveFloatValue($key, $defaultValue, $context); - case FlagValueType::OBJECT: - /** @var mixed[] $defaultValue */ + case FlagValueType::Object->value: + /** @var mixed[] $defaultValue */; $defaultValue = $defaultValue; return $provider->resolveObjectValue($key, $defaultValue, $context); - default: - throw new FlagValueTypeError($type); } } + + private function determineProvider(): Provider + { + return $this->provider ?? $this->api->getProvider(); + } } diff --git a/src/implementation/common/ArrayHelper.php b/src/implementation/common/ArrayHelper.php index 21ef3f6..4521cfa 100644 --- a/src/implementation/common/ArrayHelper.php +++ b/src/implementation/common/ArrayHelper.php @@ -11,7 +11,9 @@ class ArrayHelper { /** - * Determines whether the array is indexed or associative + * Returns a list of the keys from an associative array + * + * Non-associative arrays will return empty lists * * @param mixed[] $array * diff --git a/src/implementation/common/Metadata.php b/src/implementation/common/Metadata.php index 6759b3a..2af4711 100644 --- a/src/implementation/common/Metadata.php +++ b/src/implementation/common/Metadata.php @@ -8,20 +8,30 @@ class Metadata implements MetadataInterface { - private string $name; + public function __construct(private string $domain) + { + } + + public function getDomain(): string + { + return $this->domain; + } - public function __construct(string $name) + public function setDomain(string $domain): void { - $this->name = $name; + $this->domain = $domain; } + /** + * @deprecated Use getDomain + */ public function getName(): string { - return $this->name; + return $this->domain; } public function setName(string $name): void { - $this->name = $name; + $this->domain = $name; } } diff --git a/src/implementation/common/StringHelper.php b/src/implementation/common/StringHelper.php index 55a4ebd..447a72d 100644 --- a/src/implementation/common/StringHelper.php +++ b/src/implementation/common/StringHelper.php @@ -13,25 +13,19 @@ class StringHelper { public static function capitalize(string $input): string { - switch (strlen($input)) { - case 0: - return ''; - case 1: - return strtoupper($input); - default: - return strtoupper(substr($input, 0, 1)) . substr($input, 1); - } + return match (strlen($input)) { + 0 => '', + 1 => strtoupper($input), + default => strtoupper(substr($input, 0, 1)) . substr($input, 1), + }; } public static function decapitalize(string $input): string { - switch (strlen($input)) { - case 0: - return ''; - case 1: - return strtolower($input); - default: - return strtolower(substr($input, 0, 1)) . substr($input, 1); - } + return match (strlen($input)) { + 0 => '', + 1 => strtolower($input), + default => strtolower(substr($input, 0, 1)) . substr($input, 1), + }; } } diff --git a/src/implementation/common/ValueTypeValidator.php b/src/implementation/common/ValueTypeValidator.php index 955f468..3fd12e7 100644 --- a/src/implementation/common/ValueTypeValidator.php +++ b/src/implementation/common/ValueTypeValidator.php @@ -74,21 +74,14 @@ public static function isDateTime(mixed $value): bool /** * Validates whether the value is valid for the given type */ - public static function is(string $type, mixed $value): bool + public static function is(FlagValueType $type, mixed $value): bool { - switch ($type) { - case FlagValueType::BOOLEAN: - return self::isBoolean($value); - case FlagValueType::FLOAT: - return self::isFloat($value); - case FlagValueType::INTEGER: - return self::isInteger($value); - case FlagValueType::STRING: - return self::isString($value); - case FlagValueType::OBJECT: - return self::isStructure($value) || self::isArray($value); - default: - return false; - } + return match ($type) { + FlagValueType::Boolean => self::isBoolean($value), + FlagValueType::Float => self::isFloat($value), + FlagValueType::Integer => self::isInteger($value), + FlagValueType::String => self::isString($value), + FlagValueType::Object => self::isStructure($value) || self::isArray($value), + }; } } diff --git a/src/implementation/events/Event.php b/src/implementation/events/Event.php new file mode 100644 index 0000000..722f635 --- /dev/null +++ b/src/implementation/events/Event.php @@ -0,0 +1,25 @@ +eventName; + } + + public function getDetails(): EventDetails + { + return $this->details; + } +} diff --git a/src/implementation/events/EventMetadata.php b/src/implementation/events/EventMetadata.php new file mode 100644 index 0000000..80bde7a --- /dev/null +++ b/src/implementation/events/EventMetadata.php @@ -0,0 +1,53 @@ + $eventMetadataMap + */ + public function __construct(protected array $eventMetadataMap = []) + { + } + + public function has(string $key): bool + { + return key_exists($key, $this->eventMetadataMap); + } + + /** + * Return key-type pairs of the EventMetadata + * + * @return Array + */ + public function keys(): array + { + return ArrayHelper::getStringKeys($this->eventMetadataMap); + } + + public function get(string $key): bool | string | int | float | null + { + if ($this->has($key)) { + return $this->eventMetadataMap[$key]; + } + + return null; + } + + /** + * @return Array + */ + public function toArray(): array + { + return array_merge([], $this->eventMetadataMap); + } +} diff --git a/src/implementation/events/HandlerAdderTrait.php b/src/implementation/events/HandlerAdderTrait.php new file mode 100644 index 0000000..0882983 --- /dev/null +++ b/src/implementation/events/HandlerAdderTrait.php @@ -0,0 +1,13 @@ + $handlers + */ + private array $handlers = []; +} diff --git a/src/implementation/flags/Attributes.php b/src/implementation/flags/Attributes.php index 9676c4e..34837f3 100644 --- a/src/implementation/flags/Attributes.php +++ b/src/implementation/flags/Attributes.php @@ -8,19 +8,13 @@ use OpenFeature\implementation\common\ArrayHelper; use OpenFeature\interfaces\flags\Attributes as AttributesInterface; -use function array_merge; - class Attributes implements AttributesInterface { - /** @var Array $attributesMap */ - protected array $attributesMap; - /** * @param Array $attributesMap */ - public function __construct(array $attributesMap = []) + public function __construct(protected array $attributesMap = []) { - $this->attributesMap = $attributesMap; } /** @@ -38,11 +32,7 @@ public function keys(): array */ public function get(string $key): bool | string | int | float | DateTime | array | null { - if (isset($this->attributesMap[$key])) { - return $this->attributesMap[$key]; - } - - return null; + return $this->attributesMap[$key] ?? null; } /** @@ -50,6 +40,6 @@ public function get(string $key): bool | string | int | float | DateTime | array */ public function toArray(): array { - return array_merge([], $this->attributesMap); + return [...$this->attributesMap]; } } diff --git a/src/implementation/flags/EvaluationContext.php b/src/implementation/flags/EvaluationContext.php index 5af458f..20eaaee 100644 --- a/src/implementation/flags/EvaluationContext.php +++ b/src/implementation/flags/EvaluationContext.php @@ -11,13 +11,10 @@ class EvaluationContext implements EvaluationContextInterface { use EvaluationContextMerger; - private ?string $targetingKey; - protected AttributesInterface $attributes; - - public function __construct(?string $targetingKey = null, ?AttributesInterface $attributes = null) - { - $this->targetingKey = $targetingKey; - $this->attributes = $attributes ?? new Attributes(); + public function __construct( + private ?string $targetingKey = null, + protected readonly AttributesInterface $attributes = new Attributes(), + ) { } public function getTargetingKey(): ?string diff --git a/src/implementation/flags/EvaluationDetails.php b/src/implementation/flags/EvaluationDetails.php index d96c46b..31e66eb 100644 --- a/src/implementation/flags/EvaluationDetails.php +++ b/src/implementation/flags/EvaluationDetails.php @@ -4,7 +4,6 @@ namespace OpenFeature\implementation\flags; -use DateTime; use OpenFeature\interfaces\flags\EvaluationDetails as EvaluationDetailsInterface; use OpenFeature\interfaces\provider\ResolutionError; @@ -12,15 +11,14 @@ class EvaluationDetails implements EvaluationDetailsInterface { private string $flagKey = ''; - /** @var bool|string|int|float|DateTime|mixed[]|null $value */ - private bool | string | int | float | DateTime | array | null $value = null; + /** @var bool|string|int|float|mixed[]|null $value */ + private bool | string | int | float | array | null $value = null; private ?ResolutionError $error = null; private ?string $reason = null; private ?string $variant = null; public function __construct() { - $this->value = null; } public function getFlagKey(): string @@ -39,17 +37,17 @@ public function setFlagKey(string $flagKey): void * ----------------- * The evaluation details structure's value field MUST contain the evaluated flag value. * - * @return bool|string|int|float|DateTime|mixed[]|null + * @return bool|string|int|float|mixed[]|null */ - public function getValue(): bool | string | int | float | DateTime | array | null + public function getValue(): bool | string | int | float | array | null { return $this->value; } /** - * @param bool|string|int|float|DateTime|mixed[]|null $value + * @param bool|string|int|float|mixed[]|null $value */ - public function setValue(bool | string | int | float | DateTime | array | null $value): void + public function setValue(bool | string | int | float | array | null $value): void { $this->value = $value; } diff --git a/src/implementation/flags/EvaluationDetailsBuilder.php b/src/implementation/flags/EvaluationDetailsBuilder.php index 7cb4dc6..61e8cf6 100644 --- a/src/implementation/flags/EvaluationDetailsBuilder.php +++ b/src/implementation/flags/EvaluationDetailsBuilder.php @@ -4,7 +4,6 @@ namespace OpenFeature\implementation\flags; -use DateTime; use OpenFeature\interfaces\flags\EvaluationDetails as EvaluationDetailsInterface; use OpenFeature\interfaces\provider\ResolutionError; @@ -25,9 +24,9 @@ public function withFlagKey(string $flagKey): EvaluationDetailsBuilder } /** - * @param bool|string|int|float|DateTime|mixed[]|null $value + * @param bool|string|int|float|mixed[]|null $value */ - public function withValue(bool | string | int | float | DateTime | array | null $value): EvaluationDetailsBuilder + public function withValue(bool | string | int | float | array | null $value): EvaluationDetailsBuilder { $this->details->setValue($value); diff --git a/src/implementation/flags/EvaluationDetailsFactory.php b/src/implementation/flags/EvaluationDetailsFactory.php index 6115799..b491a15 100644 --- a/src/implementation/flags/EvaluationDetailsFactory.php +++ b/src/implementation/flags/EvaluationDetailsFactory.php @@ -4,7 +4,6 @@ namespace OpenFeature\implementation\flags; -use DateTime; use OpenFeature\interfaces\flags\EvaluationDetails; use OpenFeature\interfaces\provider\ResolutionDetails; @@ -13,9 +12,9 @@ class EvaluationDetailsFactory /** * Provides a simple method for building EvaluationDetails from a given value\ * - * @param bool|string|int|float|DateTime|mixed[]|null $value + * @param bool|string|int|float|mixed[]|null $value */ - public static function from(string $flagKey, bool | string | int | float | DateTime | array | null $value): EvaluationDetails + public static function from(string $flagKey, bool | string | int | float | array | null $value): EvaluationDetails { return (new EvaluationDetailsBuilder()) ->withFlagKey($flagKey) diff --git a/src/implementation/flags/EvaluationOptions.php b/src/implementation/flags/EvaluationOptions.php index 72a8c89..c018108 100644 --- a/src/implementation/flags/EvaluationOptions.php +++ b/src/implementation/flags/EvaluationOptions.php @@ -13,15 +13,12 @@ class EvaluationOptions implements EvaluationOptionsInterface { use HooksAwareTrait; - private ?HookHints $hookHints; - /** * @param Hook[] $hooks */ - public function __construct(array $hooks = [], ?HookHints $hookHints = null) + public function __construct(array $hooks = [], private ?HookHints $hookHints = null) { $this->setHooks($hooks); - $this->hookHints = $hookHints; } public function getHookHints(): ?HookHints diff --git a/src/implementation/flags/FlagMetadata.php b/src/implementation/flags/FlagMetadata.php new file mode 100644 index 0000000..01a26f3 --- /dev/null +++ b/src/implementation/flags/FlagMetadata.php @@ -0,0 +1,41 @@ + $metadata + */ + public function __construct(protected array $metadata = []) + { + } + + /** + * Return key-type pairs of the attributes + * + * @return Array + */ + public function keys(): array + { + return ArrayHelper::getStringKeys($this->metadata); + } + + public function get(string $key): bool | string | int | float | null + { + return $this->metadata[$key] ?? null; + } + + /** + * @return Array + */ + public function toArray(): array + { + return [...$this->metadata]; + } +} diff --git a/src/implementation/flags/NoOpClient.php b/src/implementation/flags/NoOpClient.php index 792e262..8eb6b3b 100644 --- a/src/implementation/flags/NoOpClient.php +++ b/src/implementation/flags/NoOpClient.php @@ -4,15 +4,18 @@ namespace OpenFeature\implementation\flags; -use DateTime; use OpenFeature\implementation\common\Metadata; +use OpenFeature\implementation\provider\ProviderAwareTrait; use OpenFeature\interfaces\flags\Client; use OpenFeature\interfaces\flags\EvaluationContext as EvaluationContextInterface; use OpenFeature\interfaces\flags\EvaluationDetails; use OpenFeature\interfaces\flags\EvaluationOptions; +use OpenFeature\interfaces\provider\ProviderAware; -class NoOpClient implements Client +class NoOpClient implements Client, ProviderAware { + use ProviderAwareTrait; + private const CLIENT_NAME = 'no-op-client'; public function getBooleanValue(string $flagKey, bool $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptions $options = null): ?bool @@ -69,7 +72,7 @@ public function getObjectValue( return $defaultValue; } - public function getObjectDetails(string $flagKey, bool | string | int | float | DateTime | array | null $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptions $options = null): EvaluationDetails + public function getObjectDetails(string $flagKey, bool | string | int | float | array | null $defaultValue, ?EvaluationContextInterface $context = null, ?EvaluationOptions $options = null): EvaluationDetails { return EvaluationDetailsFactory::from($flagKey, $defaultValue); } diff --git a/src/implementation/hooks/AbstractHook.php b/src/implementation/hooks/AbstractHook.php index 4303d9c..ead670d 100644 --- a/src/implementation/hooks/AbstractHook.php +++ b/src/implementation/hooks/AbstractHook.php @@ -5,6 +5,7 @@ namespace OpenFeature\implementation\hooks; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\Hook; use OpenFeature\interfaces\hooks\HookContext; use OpenFeature\interfaces\hooks\HookHints; @@ -21,5 +22,5 @@ abstract public function error(HookContext $context, Throwable $error, HookHints abstract public function finally(HookContext $context, HookHints $hints): void; - abstract public function supportsFlagValueType(string $flagValueType): bool; + abstract public function supportsFlagValueType(FlagValueType $flagValueType): bool; } diff --git a/src/implementation/hooks/AbstractHookContext.php b/src/implementation/hooks/AbstractHookContext.php index 39fbeba..678238c 100644 --- a/src/implementation/hooks/AbstractHookContext.php +++ b/src/implementation/hooks/AbstractHookContext.php @@ -4,11 +4,12 @@ namespace OpenFeature\implementation\hooks; -use DateTime; +use Exception; use OpenFeature\implementation\common\Metadata; use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\interfaces\common\Metadata as MetadataInterface; use OpenFeature\interfaces\flags\EvaluationContext as EvaluationContextInterface; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\HookContext; use function is_array; @@ -16,13 +17,15 @@ abstract class AbstractHookContext { protected string $flagKey = ''; - protected string $type = ''; - /** @var bool|string|int|float|DateTime|mixed[]|null $defaultValue */ - protected bool | string | int | float | DateTime | array | null $defaultValue = null; + protected FlagValueType $type = FlagValueType::Boolean; + /** @var bool|string|int|float|mixed[]|null $defaultValue */ + protected bool | string | int | float | array | null $defaultValue = null; protected EvaluationContextInterface $evaluationContext; protected MetadataInterface $clientMetadata; protected MetadataInterface $providerMetadata; + private const REQUIRED_PROPERTIES = ['flagKey', 'type']; + /** * @param HookContext|mixed[]|null $hookContext */ @@ -43,6 +46,12 @@ public function __construct(HookContext | array | null $hookContext = null) $this->clientMetadata = $hookContext->getClientMetadata(); $this->providerMetadata = $hookContext->getProviderMetadata(); } elseif (is_array($hookContext)) { + foreach (self::REQUIRED_PROPERTIES as $requiredProperty) { + if (!isset($hookContext[$requiredProperty])) { + throw new Exception('Required property missing from hook context'); + } + } + /** @var string $property */ /** @var mixed $value */ foreach ($hookContext as $property => $value) { diff --git a/src/implementation/hooks/BooleanHook.php b/src/implementation/hooks/BooleanHook.php index 500c176..45c931e 100644 --- a/src/implementation/hooks/BooleanHook.php +++ b/src/implementation/hooks/BooleanHook.php @@ -8,8 +8,8 @@ abstract class BooleanHook extends AbstractHook { - public function supportsFlagValueType(string $flagValueType): bool + public function supportsFlagValueType(FlagValueType $flagValueType): bool { - return $flagValueType === FlagValueType::BOOLEAN; + return $flagValueType === FlagValueType::Boolean; } } diff --git a/src/implementation/hooks/FloatHook.php b/src/implementation/hooks/FloatHook.php index 5e80e5b..f7e49e4 100644 --- a/src/implementation/hooks/FloatHook.php +++ b/src/implementation/hooks/FloatHook.php @@ -8,8 +8,8 @@ abstract class FloatHook extends AbstractHook { - public function supportsFlagValueType(string $flagValueType): bool + public function supportsFlagValueType(FlagValueType $flagValueType): bool { - return $flagValueType === FlagValueType::FLOAT; + return $flagValueType === FlagValueType::Float; } } diff --git a/src/implementation/hooks/HookContextBuilder.php b/src/implementation/hooks/HookContextBuilder.php index 5e94f37..88805dd 100644 --- a/src/implementation/hooks/HookContextBuilder.php +++ b/src/implementation/hooks/HookContextBuilder.php @@ -4,9 +4,9 @@ namespace OpenFeature\implementation\hooks; -use DateTime; use OpenFeature\interfaces\common\Metadata; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\HookContext as HookContextInterface; class HookContextBuilder @@ -26,7 +26,7 @@ public function withFlagKey(string $flagKey): self return $this; } - public function withType(string $type): self + public function withType(FlagValueType $type): self { $this->hookContext->setType($type); @@ -34,9 +34,9 @@ public function withType(string $type): self } /** - * @param bool|string|int|float|DateTime|mixed[]|null $defaultValue + * @param bool|string|int|float|mixed[]|null $defaultValue */ - public function withDefaultValue(bool | string | int | float | DateTime | array | null $defaultValue): self + public function withDefaultValue(bool | string | int | float | array | null $defaultValue): self { $this->hookContext->setDefaultValue($defaultValue); diff --git a/src/implementation/hooks/HookContextFactory.php b/src/implementation/hooks/HookContextFactory.php index 1d22e69..d633661 100644 --- a/src/implementation/hooks/HookContextFactory.php +++ b/src/implementation/hooks/HookContextFactory.php @@ -4,21 +4,21 @@ namespace OpenFeature\implementation\hooks; -use DateTime; use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\interfaces\common\Metadata; use OpenFeature\interfaces\flags\EvaluationContext as EvaluationContextInterface; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\HookContext; class HookContextFactory { /** - * @param bool|string|int|float|DateTime|mixed[]|null $defaultValue + * @param bool|string|int|float|mixed[]|null $defaultValue */ public static function from( string $flagKey, - string $type, - bool | string | int | float | DateTime | array | null $defaultValue, + FlagValueType $type, + bool | string | int | float | array | null $defaultValue, ?EvaluationContextInterface $evaluationContext, Metadata $clientMetadata, Metadata $providerMetadata, diff --git a/src/implementation/hooks/HookExecutor.php b/src/implementation/hooks/HookExecutor.php index b92374c..b218340 100644 --- a/src/implementation/hooks/HookExecutor.php +++ b/src/implementation/hooks/HookExecutor.php @@ -8,6 +8,7 @@ use OpenFeature\implementation\flags\MutableEvaluationContext; use OpenFeature\interfaces\common\LoggerAwareTrait; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\Hook; use OpenFeature\interfaces\hooks\HookContext; use OpenFeature\interfaces\hooks\HookHints as HookHintsInterface; @@ -35,7 +36,7 @@ public function __construct(?LoggerInterface $logger = null) * * @param Hook[] $mergedHooks */ - public function beforeHooks(string $type, HookContext $hookContext, array $mergedHooks, HookHintsInterface $hints): ?EvaluationContext + public function beforeHooks(FlagValueType $type, HookContext $hookContext, array $mergedHooks, HookHintsInterface $hints): ?EvaluationContext { $additionalContext = new MutableEvaluationContext(); @@ -54,7 +55,7 @@ public function beforeHooks(string $type, HookContext $hookContext, array $merge /** * @param Hook[] $mergedHooks */ - public function afterHooks(string $type, HookContext $hookContext, ResolutionDetails $details, array $mergedHooks, HookHintsInterface $hints): void + public function afterHooks(FlagValueType $type, HookContext $hookContext, ResolutionDetails $details, array $mergedHooks, HookHintsInterface $hints): void { foreach ($mergedHooks as $hook) { if ($hook->supportsFlagValueType($type)) { @@ -66,7 +67,7 @@ public function afterHooks(string $type, HookContext $hookContext, ResolutionDet /** * @param Hook[] $mergedHooks */ - public function errorHooks(string $type, HookContext $hookContext, Throwable $err, array $mergedHooks, HookHintsInterface $hints): void + public function errorHooks(FlagValueType $type, HookContext $hookContext, Throwable $err, array $mergedHooks, HookHintsInterface $hints): void { foreach ($mergedHooks as $hook) { if ($hook->supportsFlagValueType($type)) { @@ -78,7 +79,7 @@ public function errorHooks(string $type, HookContext $hookContext, Throwable $er /** * @param Hook[] $mergedHooks */ - public function finallyHooks(string $type, HookContext $hookContext, array $mergedHooks, HookHintsInterface $hints): void + public function finallyHooks(FlagValueType $type, HookContext $hookContext, array $mergedHooks, HookHintsInterface $hints): void { foreach ($mergedHooks as $hook) { if ($hook->supportsFlagValueType($type)) { diff --git a/src/implementation/hooks/HookHints.php b/src/implementation/hooks/HookHints.php index 2385d57..a237777 100644 --- a/src/implementation/hooks/HookHints.php +++ b/src/implementation/hooks/HookHints.php @@ -12,8 +12,12 @@ class HookHints implements HookHintsInterface { - /** @var Array $hints */ - private array $hints = []; + /** + * @param Array $hints + */ + public function __construct(private readonly array $hints = []) + { + } /** * @return bool|string|int|float|DateTime|mixed[]|null @@ -34,12 +38,4 @@ public function keys(): array { return array_keys($this->hints); } - - /** - * @param Array $hints - */ - public function __construct(array $hints = []) - { - $this->hints = $hints; - } } diff --git a/src/implementation/hooks/ImmutableHookContext.php b/src/implementation/hooks/ImmutableHookContext.php index e0ed360..2cb6756 100644 --- a/src/implementation/hooks/ImmutableHookContext.php +++ b/src/implementation/hooks/ImmutableHookContext.php @@ -4,9 +4,9 @@ namespace OpenFeature\implementation\hooks; -use DateTime; use OpenFeature\interfaces\common\Metadata; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\HookContext as HookContextInterface; class ImmutableHookContext extends AbstractHookContext implements HookContextInterface @@ -16,15 +16,15 @@ public function getFlagKey(): string return $this->flagKey; } - public function getType(): string + public function getType(): FlagValueType { return $this->type; } /** - * @return bool|string|int|float|DateTime|mixed[]|null + * @return bool|string|int|float|mixed[]|null */ - public function getDefaultValue(): bool | string | int | float | DateTime | array | null + public function getDefaultValue(): bool | string | int | float | array | null { return $this->defaultValue; } diff --git a/src/implementation/hooks/IntegerHook.php b/src/implementation/hooks/IntegerHook.php index 86eebf3..4c9d730 100644 --- a/src/implementation/hooks/IntegerHook.php +++ b/src/implementation/hooks/IntegerHook.php @@ -8,8 +8,8 @@ abstract class IntegerHook extends AbstractHook { - public function supportsFlagValueType(string $flagValueType): bool + public function supportsFlagValueType(FlagValueType $flagValueType): bool { - return $flagValueType === FlagValueType::INTEGER; + return $flagValueType === FlagValueType::Integer; } } diff --git a/src/implementation/hooks/MutableHookContext.php b/src/implementation/hooks/MutableHookContext.php index a8a1b77..348e0b5 100644 --- a/src/implementation/hooks/MutableHookContext.php +++ b/src/implementation/hooks/MutableHookContext.php @@ -4,9 +4,9 @@ namespace OpenFeature\implementation\hooks; -use DateTime; use OpenFeature\interfaces\common\Metadata; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\HookContext as HookContextInterface; use OpenFeature\interfaces\hooks\MutableHookContext as MutableHookContextInterface; @@ -17,12 +17,12 @@ public function setFlagKey(string $flagKey): void $this->flagKey = $flagKey; } - public function setType(string $type): void + public function setType(FlagValueType $type): void { $this->type = $type; } - public function setDefaultValue(bool | string | int | float | DateTime | array | null $defaultValue): void + public function setDefaultValue(bool | string | int | float | array | null $defaultValue): void { $this->defaultValue = $defaultValue; } diff --git a/src/implementation/hooks/ObjectHook.php b/src/implementation/hooks/ObjectHook.php index 45dcfc7..7fd0b5d 100644 --- a/src/implementation/hooks/ObjectHook.php +++ b/src/implementation/hooks/ObjectHook.php @@ -8,8 +8,8 @@ abstract class ObjectHook extends AbstractHook { - public function supportsFlagValueType(string $flagValueType): bool + public function supportsFlagValueType(FlagValueType $flagValueType): bool { - return $flagValueType === FlagValueType::OBJECT; + return $flagValueType === FlagValueType::Object; } } diff --git a/src/implementation/hooks/StringHook.php b/src/implementation/hooks/StringHook.php index 3f8e2cd..04d8974 100644 --- a/src/implementation/hooks/StringHook.php +++ b/src/implementation/hooks/StringHook.php @@ -8,8 +8,8 @@ abstract class StringHook extends AbstractHook { - public function supportsFlagValueType(string $flagValueType): bool + public function supportsFlagValueType(FlagValueType $flagValueType): bool { - return $flagValueType === FlagValueType::STRING; + return $flagValueType === FlagValueType::String; } } diff --git a/src/implementation/provider/AbstractProvider.php b/src/implementation/provider/AbstractProvider.php index 3598d3f..9975091 100644 --- a/src/implementation/provider/AbstractProvider.php +++ b/src/implementation/provider/AbstractProvider.php @@ -7,19 +7,21 @@ use OpenFeature\implementation\common\Metadata; use OpenFeature\interfaces\common\Metadata as MetadataInterface; use OpenFeature\interfaces\flags\EvaluationContext; -use OpenFeature\interfaces\hooks\Hook; +use OpenFeature\interfaces\hooks\HooksAware; +use OpenFeature\interfaces\hooks\HooksAwareTrait; use OpenFeature\interfaces\provider\Provider; +use OpenFeature\interfaces\provider\ProviderState; use OpenFeature\interfaces\provider\ResolutionDetails as ResolutionDetailsInterface; use Psr\Log\LoggerAwareTrait; -abstract class AbstractProvider implements Provider +abstract class AbstractProvider implements HooksAware, Provider { + use HooksAwareTrait; use LoggerAwareTrait; protected static string $NAME = 'AbstractProvider'; - /** @var Hook[] $hooks */ - private array $hooks = []; + protected ProviderState $status = ProviderState::NOT_READY; public function getMetadata(): MetadataInterface { @@ -40,18 +42,23 @@ abstract public function resolveFloatValue(string $flagKey, float $defaultValue, abstract public function resolveObjectValue(string $flagKey, array $defaultValue, ?EvaluationContext $context = null): ResolutionDetailsInterface; /** - * @return Hook[] + * The default is that this is not required, and must be overridden by the implementing provider */ - public function getHooks(): array + public function dispose(): void { - return $this->hooks; + $this->status = ProviderState::NOT_READY; } /** - * @param Hook[] $hooks + * The default is that this is not required, and must be overridden by the implementing provider */ - public function setHooks(array $hooks): void + public function initialize(EvaluationContext $evaluationContext): void { - $this->hooks = $hooks; + $this->status = ProviderState::READY; + } + + public function getStatus(): ProviderState + { + return $this->status; } } diff --git a/src/implementation/provider/ProviderAwareTrait.php b/src/implementation/provider/ProviderAwareTrait.php new file mode 100644 index 0000000..6368f05 --- /dev/null +++ b/src/implementation/provider/ProviderAwareTrait.php @@ -0,0 +1,22 @@ +provider ?? new NoOpProvider(); + } + + public function setProvider(Provider $provider): void + { + $this->provider = $provider; + } +} diff --git a/src/implementation/provider/Reason.php b/src/implementation/provider/Reason.php deleted file mode 100644 index a867e7b..0000000 --- a/src/implementation/provider/Reason.php +++ /dev/null @@ -1,16 +0,0 @@ -flagMetadata = new FlagMetadata(); + } /** - * @return bool|string|int|float|DateTime|mixed[]|null + * @return bool|string|int|float|mixed[]|null */ - public function getValue(): bool | string | int | float | DateTime | array | null + public function getValue(): bool | string | int | float | array | null { return $this->value; } /** - * @param bool|string|int|float|DateTime|mixed[]|null $value + * @param bool|string|int|float|mixed[]|null $value */ - public function setValue(bool | string | int | float | DateTime | array | null $value): void + public function setValue(bool | string | int | float | array | null $value): void { $this->value = $value; } @@ -61,4 +68,14 @@ public function setVariant(?string $variant): void { $this->variant = $variant; } + + public function getFlagMetadata(): FlagMetadataInterface + { + return $this->flagMetadata; + } + + public function setFlagMetadata(FlagMetadataInterface $flagMetadata): void + { + $this->flagMetadata = $flagMetadata; + } } diff --git a/src/implementation/provider/ResolutionDetailsBuilder.php b/src/implementation/provider/ResolutionDetailsBuilder.php index 73ae2c7..0a645e1 100644 --- a/src/implementation/provider/ResolutionDetailsBuilder.php +++ b/src/implementation/provider/ResolutionDetailsBuilder.php @@ -4,7 +4,7 @@ namespace OpenFeature\implementation\provider; -use DateTime; +use OpenFeature\interfaces\flags\FlagMetadata; use OpenFeature\interfaces\provider\ResolutionDetails as ResolutionDetailsInterface; use OpenFeature\interfaces\provider\ResolutionError; @@ -18,9 +18,9 @@ public function __construct() } /** - * @param bool|string|int|float|DateTime|mixed[]|null $value + * @param bool|string|int|float|mixed[]|null $value */ - public function withValue(bool | string | int | float | DateTime | array | null $value): ResolutionDetailsBuilder + public function withValue(bool | string | int | float | array | null $value): ResolutionDetailsBuilder { $this->details->setValue($value); @@ -48,6 +48,13 @@ public function withVariant(string $variant): ResolutionDetailsBuilder return $this; } + public function withFlagMetadata(FlagMetadata $flagMetadata): ResolutionDetailsBuilder + { + $this->details->setFlagMetadata($flagMetadata); + + return $this; + } + public function build(): ResolutionDetailsInterface { return $this->details; diff --git a/src/implementation/provider/ResolutionDetailsFactory.php b/src/implementation/provider/ResolutionDetailsFactory.php index 79d2e1b..42289ea 100644 --- a/src/implementation/provider/ResolutionDetailsFactory.php +++ b/src/implementation/provider/ResolutionDetailsFactory.php @@ -4,15 +4,14 @@ namespace OpenFeature\implementation\provider; -use DateTime; use OpenFeature\interfaces\provider\ResolutionDetails as ResolutionDetailsInterface; class ResolutionDetailsFactory { /** - * @param bool|string|int|float|DateTime|mixed[]|null $value + * @param bool|string|int|float|mixed[]|null $value */ - public static function fromSuccess(bool | string | int | float | DateTime | array | null $value): ResolutionDetailsInterface + public static function fromSuccess(bool | string | int | float | array | null $value): ResolutionDetailsInterface { return (new ResolutionDetailsBuilder()) ->withValue($value) diff --git a/src/interfaces/common/Disposable.php b/src/interfaces/common/Disposable.php new file mode 100644 index 0000000..a9a91b5 --- /dev/null +++ b/src/interfaces/common/Disposable.php @@ -0,0 +1,10 @@ + + */ + public function getChangedFlags(): array; + + public function getMessage(): ?string; + + public function getEventMetadata(): EventMetadata; +} diff --git a/src/interfaces/events/EventMetadata.php b/src/interfaces/events/EventMetadata.php new file mode 100644 index 0000000..25cff73 --- /dev/null +++ b/src/interfaces/events/EventMetadata.php @@ -0,0 +1,22 @@ + + */ + public function toArray(): array; +} diff --git a/src/interfaces/events/HandlerAdder.php b/src/interfaces/events/HandlerAdder.php new file mode 100644 index 0000000..293227b --- /dev/null +++ b/src/interfaces/events/HandlerAdder.php @@ -0,0 +1,10 @@ + + */ + public function keys(): array; + + public function get(string $key): bool | string | int | float | null; + + /** + * @return Array + */ + public function toArray(): array; +} diff --git a/src/interfaces/flags/FlagValueType.php b/src/interfaces/flags/FlagValueType.php index a5eca98..9108d77 100644 --- a/src/interfaces/flags/FlagValueType.php +++ b/src/interfaces/flags/FlagValueType.php @@ -4,17 +4,32 @@ namespace OpenFeature\interfaces\flags; -/** - * A pseudo-enumerator to support PHP 7.x - * - * TODO: Bump to PHP 8.x + support after EOL with - * native enum implementation - */ -class FlagValueType +enum FlagValueType: string { + case String = 'STRING'; + case Integer = 'INTEGER'; + case Float = 'FLOAT'; + case Object = 'OBJECT'; + case Boolean = 'BOOLEAN'; + + /** + * @deprecated prefer enum value over const + */ public const STRING = 'STRING'; + /** + * @deprecated prefer enum value over const + */ public const INTEGER = 'INTEGER'; + /** + * @deprecated prefer enum value over const + */ public const FLOAT = 'FLOAT'; + /** + * @deprecated prefer enum value over const + */ public const OBJECT = 'OBJECT'; + /** + * @deprecated prefer enum value over const + */ public const BOOLEAN = 'BOOLEAN'; } diff --git a/src/interfaces/hooks/Hook.php b/src/interfaces/hooks/Hook.php index 25bc807..5f7db60 100644 --- a/src/interfaces/hooks/Hook.php +++ b/src/interfaces/hooks/Hook.php @@ -5,6 +5,7 @@ namespace OpenFeature\interfaces\hooks; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\provider\ResolutionDetails; use Throwable; @@ -54,7 +55,7 @@ public function finally(HookContext $context, HookHints $hints): void; /** * Determines whether the hook should be run for the flag value type returned. * - * @param string $flagValueType The type of flag value + * @param FlagValueType $flagValueType The type of flag value */ - public function supportsFlagValueType(string $flagValueType): bool; + public function supportsFlagValueType(FlagValueType $flagValueType): bool; } diff --git a/src/interfaces/hooks/HookContext.php b/src/interfaces/hooks/HookContext.php index 6ae0599..5c82c49 100644 --- a/src/interfaces/hooks/HookContext.php +++ b/src/interfaces/hooks/HookContext.php @@ -4,9 +4,9 @@ namespace OpenFeature\interfaces\hooks; -use DateTime; use OpenFeature\interfaces\common\Metadata; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; interface HookContext { @@ -36,7 +36,7 @@ public function getFlagKey(): string; * The flag key, flag type, and default value properties MUST be immutable. If * the language does not support immutability, the hook MUST NOT modify these properties. */ - public function getType(): string; + public function getType(): FlagValueType; /** * ----------------- @@ -50,9 +50,9 @@ public function getType(): string; * The flag key, flag type, and default value properties MUST be immutable. If * the language does not support immutability, the hook MUST NOT modify these properties. * - * @return bool|string|int|float|DateTime|mixed[]|null + * @return bool|string|int|float|mixed[]|null */ - public function getDefaultValue(): bool | string | int | float | DateTime | array | null; + public function getDefaultValue(): bool | string | int | float | array | null; /** * ----------------- diff --git a/src/interfaces/hooks/HooksAwareTrait.php b/src/interfaces/hooks/HooksAwareTrait.php index 6d59e53..566de73 100644 --- a/src/interfaces/hooks/HooksAwareTrait.php +++ b/src/interfaces/hooks/HooksAwareTrait.php @@ -6,7 +6,7 @@ trait HooksAwareTrait { - /** @var Hook[] $hooks */ + /** @var Array $hooks */ private array $hooks = []; /** diff --git a/src/interfaces/hooks/MutableHookContext.php b/src/interfaces/hooks/MutableHookContext.php index b6c144f..c40fa99 100644 --- a/src/interfaces/hooks/MutableHookContext.php +++ b/src/interfaces/hooks/MutableHookContext.php @@ -4,20 +4,20 @@ namespace OpenFeature\interfaces\hooks; -use DateTime; use OpenFeature\interfaces\common\Metadata; use OpenFeature\interfaces\flags\EvaluationContext; +use OpenFeature\interfaces\flags\FlagValueType; interface MutableHookContext { public function setFlagKey(string $flagKey): void; - public function setType(string $type): void; + public function setType(FlagValueType $type): void; /** - * @param bool|string|int|float|DateTime|mixed[]|null $defaultValue + * @param bool|string|int|float|mixed[]|null $defaultValue */ - public function setDefaultValue(bool | string | int | float | DateTime | array | null $defaultValue): void; + public function setDefaultValue(bool | string | int | float | array | null $defaultValue): void; public function setEvaluationContext(EvaluationContext $evaluationContext): void; diff --git a/src/interfaces/provider/Initializable.php b/src/interfaces/provider/Initializable.php new file mode 100644 index 0000000..f1ee9a8 --- /dev/null +++ b/src/interfaces/provider/Initializable.php @@ -0,0 +1,12 @@ +expectException(Throwable::class); + + new MutableHookContext(['flagKey' => 'test-key']); + } + public function testAsMutable(): void { - $expectedValue = new MutableHookContext(['flagKey' => 'test-key']); + $expectedValue = new MutableHookContext(['flagKey' => 'test-key', 'type' => FlagValueType::Boolean]); $actualValue = (new HookContextBuilder())->withFlagKey('test-key')->asMutable()->build(); @@ -24,9 +33,9 @@ public function testAsMutable(): void public function testAsImmutable(): void { - $expectedValue = new ImmutableHookContext(['flagKey' => 'test-key']); + $expectedValue = new ImmutableHookContext(['flagKey' => 'test-key', 'type' => FlagValueType::Boolean]); - $actualValue = (new HookContextBuilder())->withFlagKey('test-key')->asImmutable()->build(); + $actualValue = (new HookContextBuilder())->withFlagKey('test-key')->withType(FlagValueType::Boolean)->asImmutable()->build(); $this->assertInstanceOf(HookContext::class, $actualValue); $this->assertEqualsCanonicalizing($expectedValue, $actualValue); diff --git a/tests/unit/HooksTest.php b/tests/unit/HooksTest.php index 51c7370..b655112 100644 --- a/tests/unit/HooksTest.php +++ b/tests/unit/HooksTest.php @@ -38,7 +38,7 @@ class HooksTest extends TestCase public function testHookContextMustProvideArguments(): void { $flagKey = 'test-key'; - $flagValueType = FlagValueType::BOOLEAN; + $flagValueType = FlagValueType::Boolean; $evaluationContext = new EvaluationContext(); $defaultValue = false; @@ -60,7 +60,7 @@ public function testHookContextShouldProvideAccessToMetadataFields(): void $clientMetadata = new Metadata('client'); $providerMetadata = new Metadata('provider'); - $hookContext = HookContextFactory::from('key', FlagValueType::BOOLEAN, false, new EvaluationContext(), $clientMetadata, $providerMetadata); + $hookContext = HookContextFactory::from('key', FlagValueType::Boolean, false, new EvaluationContext(), $clientMetadata, $providerMetadata); $this->assertEquals($clientMetadata, $hookContext->getClientMetadata()); $this->assertEquals($providerMetadata, $hookContext->getProviderMetadata()); @@ -73,7 +73,7 @@ public function testHookContextShouldProvideAccessToMetadataFields(): void */ public function testValuePropertiesMustBeImmutable(): void { - $expectedHookContext = HookContextFactory::from('key', FlagValueType::BOOLEAN, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')); + $expectedHookContext = HookContextFactory::from('key', FlagValueType::Boolean, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')); $testRunner = $this; $hook = $this->mockery(TestHook::class)->makePartial(); @@ -83,7 +83,7 @@ public function testValuePropertiesMustBeImmutable(): void $testRunner->assertEquals($expectedHookContext, $context); }); - (new HookExecutor(null))->beforeHooks(FlagValueType::BOOLEAN, $expectedHookContext, [$hook], new HookHints()); + (new HookExecutor(null))->beforeHooks(FlagValueType::Boolean, $expectedHookContext, [$hook], new HookHints()); } /** @@ -106,8 +106,8 @@ public function testEvaluationContextIsMutableWithinBeforeHook(): void $additionalEvaluationContext = (new HookExecutor()) ->beforeHooks( - FlagValueType::BOOLEAN, - HookContextFactory::from('flagKey', FlagValueType::BOOLEAN, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), + FlagValueType::Boolean, + HookContextFactory::from('flagKey', FlagValueType::Boolean, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), [$mutationHook], new HookHints(), ); @@ -137,13 +137,12 @@ public function testEvaluationContextIsImmutableInAfterHooks(): void // @phpstan-ignore-next-line $additionalEvaluationContext = (new HookExecutor()) ->afterHooks( - FlagValueType::BOOLEAN, - HookContextFactory::from('flagKey', FlagValueType::BOOLEAN, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), + FlagValueType::Boolean, + HookContextFactory::from('flagKey', FlagValueType::Boolean, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), ResolutionDetailsFactory::fromSuccess(true), [$mutationHook], new HookHints(), ); - // @phpstan-ignore-next-line $this->assertNull($additionalEvaluationContext); } @@ -168,14 +167,13 @@ public function testEvaluationContextIsImmutableInErrorHooks(): void //@phpstan-ignore-next-line $additionalEvaluationContext = (new HookExecutor()) ->errorHooks( - FlagValueType::BOOLEAN, - HookContextFactory::from('flagKey', FlagValueType::BOOLEAN, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), + FlagValueType::Boolean, + HookContextFactory::from('flagKey', FlagValueType::Boolean, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), new Exception('Error'), [$mutationHook], new HookHints(), ); - //@phpstan-ignore-next-line $this->assertNull($additionalEvaluationContext); } @@ -200,13 +198,12 @@ public function testEvaluationContextIsImmutableInFinallyHooks(): void // @phpstan-ignore-next-line $additionalEvaluationContext = (new HookExecutor()) ->finallyHooks( - FlagValueType::BOOLEAN, - HookContextFactory::from('flagKey', FlagValueType::BOOLEAN, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), + FlagValueType::Boolean, + HookContextFactory::from('flagKey', FlagValueType::Boolean, false, new EvaluationContext(), new Metadata('client'), new Metadata('provider')), [$mutationHook], new HookHints(), ); - // @phpstan-ignore-next-line $this->assertNull($additionalEvaluationContext); } @@ -289,7 +286,7 @@ public function testClientMetadataInHookContextIsImmutable(): void $hookContext = (new HookContextBuilder())->withClientMetadata($clientMetadata)->build(); - (new HookExecutor())->beforeHooks(FlagValueType::BOOLEAN, $hookContext, [], new HookHints()); + (new HookExecutor())->beforeHooks(FlagValueType::Boolean, $hookContext, [], new HookHints()); $this->assertEquals($originalName, $hookContext->getClientMetadata()->getName()); } @@ -326,7 +323,7 @@ public function testProviderMetadataInHookContextIsImmutable(): void $hookContext = (new HookContextBuilder())->withProviderMetadata($providerMetadata)->build(); - (new HookExecutor())->beforeHooks(FlagValueType::BOOLEAN, $hookContext, [], new HookHints()); + (new HookExecutor())->beforeHooks(FlagValueType::Boolean, $hookContext, [], new HookHints()); $this->assertEquals($originalName, $hookContext->getProviderMetadata()->getName()); } @@ -398,7 +395,7 @@ public function testHookHintsMustBePassedToEachHook(): void $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $client->getBooleanValue('flagKey', false, null, new EvaluationOptions([$hook], $expectedHookHints)); } @@ -436,7 +433,7 @@ public function testHooksMustNotAlterHookHints(): void $hookContext = new ImmutableHookContext(); - (new HookExecutor())->beforeHooks(FlagValueType::BOOLEAN, $hookContext, [], $expectedHookHints); + (new HookExecutor())->beforeHooks(FlagValueType::Boolean, $hookContext, [], $expectedHookHints); $this->assertEquals('value', $expectedHookHints->get('key')); } diff --git a/tests/unit/NoOpClientTest.php b/tests/unit/NoOpClientTest.php index 093bb77..82b0bce 100644 --- a/tests/unit/NoOpClientTest.php +++ b/tests/unit/NoOpClientTest.php @@ -9,6 +9,7 @@ use OpenFeature\implementation\flags\EvaluationContext; use OpenFeature\implementation\flags\EvaluationDetailsBuilder; use OpenFeature\implementation\flags\NoOpClient; +use OpenFeature\interfaces\flags\FlagValueType; use OpenFeature\interfaces\hooks\Hook; use OpenFeature\interfaces\hooks\HookContext; use OpenFeature\interfaces\hooks\HookHints; @@ -198,7 +199,7 @@ public function finally(HookContext $context, HookHints $hints): void // no-op } - public function supportsFlagValueType(string $flagValueType): bool + public function supportsFlagValueType(FlagValueType $flagValueType): bool { return true; } diff --git a/tests/unit/OpenFeatureAPITest.php b/tests/unit/OpenFeatureAPITest.php index d3fecf6..261d234 100644 --- a/tests/unit/OpenFeatureAPITest.php +++ b/tests/unit/OpenFeatureAPITest.php @@ -128,6 +128,8 @@ public function testApiCanProvideProviderMetadata(): void } /** + * @runInSeparateProcess + * * Requirement 1.1.5 * * The API MUST provide a function for creating a client which accepts the following options: @@ -136,14 +138,13 @@ public function testApiCanProvideProviderMetadata(): void */ public function testApiCanCreateClient(): void { - $name = 'test-name'; - $version = 'test-version'; + $name = 'ApiCanCreateClient.test'; $api = APITestHelper::new(); - $client = $api->getClient($name, $version); + $client = $api->getClient($name); - $this->assertEquals($name, $client->getMetadata()->getName()); + $this->assertEquals($name, $client->getMetadata()->getDomain()); $this->assertInstanceOf(NoOpProvider::class, $api->getProvider()); } @@ -194,30 +195,34 @@ public function testApiWillDefaultToNoOpProvider(): void $this->assertInstanceOf(NoOpProvider::class, $actualProvider); } + /** + * @runInSeparateProcess + */ public function testApiWillCreateClientWithoutProvider(): void { - $name = 'test-name'; - $version = 'test-version'; + $domain = 'ApiWillCreateClientWithoutProvider.test'; $api = APITestHelper::new(); - $client = $api->getClient($name, $version); + $client = $api->getClient($domain); - $this->assertEquals($name, $client->getMetadata()->getName()); + $this->assertEquals($domain, $client->getMetadata()->getDomain()); $this->assertInstanceOf(NoOpProvider::class, $api->getProvider()); } + /** + * @runInSeparateProcess + */ public function testApiWillCreateClientWithProvider(): void { - $name = 'test-name'; - $version = 'test-version'; + $domain = 'ApiWillCreateClientWithProvider.test'; $api = APITestHelper::new(); $api->setProvider(new TestProvider()); - $client = $api->getClient($name, $version); + $client = $api->getClient($domain); - $this->assertEquals($name, $client->getMetadata()->getName()); + $this->assertEquals($domain, $client->getMetadata()->getDomain()); $this->assertInstanceOf(TestProvider::class, $api->getProvider()); } } diff --git a/tests/unit/OpenFeatureClientTest.php b/tests/unit/OpenFeatureClientTest.php index 1b5cd46..d9c6a44 100644 --- a/tests/unit/OpenFeatureClientTest.php +++ b/tests/unit/OpenFeatureClientTest.php @@ -41,7 +41,7 @@ class OpenFeatureClientTest extends TestCase public function testClientCanAddHooks(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); /** @var Hook|MockInterface $firstHook */ $firstHook = $this->mockery(TestHook::class)->makePartial(); @@ -67,11 +67,12 @@ public function testClientHasMetadataAccessor(): void { $api = APITestHelper::new(); $clientName = 'test-name'; - $client = new OpenFeatureClient($api, $clientName, 'test-version'); + $client = new OpenFeatureClient($api, $clientName); $metadata = $client->getMetadata(); $this->assertEquals($clientName, $metadata->getName()); + $this->assertEquals($clientName, $metadata->getDomain()); } /** @@ -83,7 +84,7 @@ public function testClientHasMetadataAccessor(): void public function testClientHasMethodForTypedFlagEvaluationValueForBoolean(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = true; @@ -105,7 +106,7 @@ public function testClientHasMethodForTypedFlagEvaluationValueForBoolean(): void public function testClientHasMethodForTypedFlagEvaluationValueForFloat(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 3.14; @@ -127,7 +128,7 @@ public function testClientHasMethodForTypedFlagEvaluationValueForFloat(): void public function testClientHasMethodForTypedFlagEvaluationValueForInteger(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 42; @@ -145,7 +146,7 @@ public function testClientHasMethodForTypedFlagEvaluationValueForInteger(): void public function testClientHasMethodForTypedFlagEvaluationValueForString(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 'STRING VALUE'; @@ -163,7 +164,7 @@ public function testClientHasMethodForTypedFlagEvaluationValueForString(): void public function testClientHasMethodForTypedFlagEvaluationValueForStructure(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = []; @@ -181,7 +182,7 @@ public function testClientHasMethodForTypedFlagEvaluationValueForStructure(): vo public function testClientValidatesReturnTypeForRetrievalOfBoolean(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $invalidValue = 'not a bool'; $expectedValue = true; @@ -205,7 +206,7 @@ public function testClientValidatesReturnTypeForRetrievalOfBoolean(): void public function testClientValidatesReturnTypeForRetrievalOfFloat(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $invalidValue = 'not a float'; $expectedValue = 3.14; @@ -229,7 +230,7 @@ public function testClientValidatesReturnTypeForRetrievalOfFloat(): void public function testClientValidatesReturnTypeForRetrievalOfInteger(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $invalidValue = 'not an integer'; $expectedValue = 42; @@ -253,7 +254,7 @@ public function testClientValidatesReturnTypeForRetrievalOfInteger(): void public function testClientValidatesReturnTypeForRetrievalOfString(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $invalidValue = 42; $expectedValue = 'a string'; @@ -277,7 +278,7 @@ public function testClientValidatesReturnTypeForRetrievalOfString(): void public function testClientValidatesReturnTypeForRetrievalOfStructure(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $invalidValue = 42; $expectedValue = ['key' => 'value']; @@ -301,7 +302,7 @@ public function testClientValidatesReturnTypeForRetrievalOfStructure(): void public function testClientHasMethodForTypedFlagEvaluationDetailsForBoolean(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = true; @@ -319,7 +320,7 @@ public function testClientHasMethodForTypedFlagEvaluationDetailsForBoolean(): vo public function testClientHasMethodForTypedFlagEvaluationDetailsForFloat(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 3.14; @@ -337,7 +338,7 @@ public function testClientHasMethodForTypedFlagEvaluationDetailsForFloat(): void public function testClientHasMethodForTypedFlagEvaluationDetailsForInteger(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 42; @@ -355,7 +356,7 @@ public function testClientHasMethodForTypedFlagEvaluationDetailsForInteger(): vo public function testClientHasMethodForTypedFlagEvaluationDetailsForString(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 'STRING VALUE'; @@ -373,7 +374,7 @@ public function testClientHasMethodForTypedFlagEvaluationDetailsForString(): voi public function testClientHasMethodForTypedFlagEvaluationDetailsForStructure(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = []; @@ -391,7 +392,7 @@ public function testClientHasMethodForTypedFlagEvaluationDetailsForStructure(): public function testClientEvaluationDetailsHasFlagValue(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = ['key' => 'value']; @@ -421,7 +422,7 @@ public function testClientEvaluationDetailsHasFlagValue(): void public function testClientEvaluationDetailsHasFlagKey(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = 'flagKey'; $actualDetails = $client->getBooleanDetails($expectedValue, false); @@ -438,7 +439,7 @@ public function testClientEvaluationDetailsHasFlagKey(): void public function testClientEvaluationDetailsHasVariantField(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedVariant = 'VARIANT VALUE'; @@ -461,7 +462,7 @@ public function testClientEvaluationDetailsHasVariantField(): void public function testClientEvaluationDetailsHasReasonField(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedReason = 'REASON VALUE'; @@ -484,7 +485,7 @@ public function testClientEvaluationDetailsHasReasonField(): void public function testClientEvaluationDetailsAbnormalExecutionHasErrorCodeField(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedErrorCode = ErrorCode::FLAG_NOT_FOUND(); @@ -510,7 +511,7 @@ public function testClientEvaluationDetailsAbnormalExecutionHasErrorCodeField(): public function testClientEvaluationDetailsAbnormalExecutionHasReasonField(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedErrorCode = ErrorCode::FLAG_NOT_FOUND(); $expectedReason = 'Failed to reach target server'; @@ -560,7 +561,7 @@ public function testClientShouldLogInformativeErrorDuringAbnormalExecution(): vo ->andThrows(new Exception('NETWORK_ERROR')); $api->setProvider($mockProvider); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $client->setLogger($mockLogger); $value = $client->getBooleanValue('flagKey', false); @@ -577,7 +578,7 @@ public function testClientShouldLogInformativeErrorDuringAbnormalExecution(): vo public function testEvaluationOptionsHooksAreCalled(): void { $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $expectedValue = true; @@ -664,7 +665,7 @@ public function testMustHaveMethodForSupplyingEvaluationContexts(): void $api->setProvider($mockProvider); $api->setEvaluationContext(new EvaluationContext(null, new Attributes(['api' => 'api']))); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $client->setEvaluationContext(new EvaluationContext(null, new Attributes(['client' => 'client']))); $testRunner = $this; @@ -737,7 +738,7 @@ public function testMustMergeEvaluationContextsInExpectedOrder(): void $api->setProvider($mockProvider); $api->setEvaluationContext(new EvaluationContext(null, new Attributes(['api' => 'api']))); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $client->setEvaluationContext(new EvaluationContext(null, new Attributes(['client' => 'client']))); $testRunner = $this; @@ -783,7 +784,7 @@ public function testHooksAndExecutionMustRunInOrder(): void $api = APITestHelper::new(); $api->setProvider($mockProvider); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $mockHook->shouldReceive('before')->andReturnUsing(function () use ($mockHook, $mockProvider) { $mockHook->shouldNotHaveReceived('after'); @@ -864,7 +865,7 @@ public function testBeforeHooksReturnedEvaluationContextMustBeMergedWithExisting $api = APITestHelper::new(); $api->setProvider($mockProvider); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $evaluationContext = new EvaluationContext(null, new Attributes([ 'oldKey' => 'oldValue', @@ -923,7 +924,7 @@ public function testBeforeHookReturnedEvaluationContextMustBePassedToSubsequentH }); $api = APITestHelper::new(); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $evaluationContext = new EvaluationContext(null, new Attributes([ 'initialKey' => 'initialValue', @@ -962,7 +963,7 @@ public function testErrorHookMustRunWhenErrorsAreEncounteredInTheBeforeStage(): $api->setProvider($mockProvider); $api->addHooks($mockHook); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $actualValue = $client->getBooleanValue('key', false); @@ -999,7 +1000,7 @@ public function testErrorHookMustRunWhenErrorsAreEncounteredInTheAfterStage(): v $api->setProvider($mockProvider); $api->addHooks($mockHook); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $actualValue = $client->getBooleanValue('key', false); @@ -1036,7 +1037,7 @@ public function testErrorHookMustRunWhenErrorsAreEncounteredInFlagResolution(): $api->setProvider($mockProvider); $api->addHooks($mockHook); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $actualValue = $client->getBooleanValue('key', false); @@ -1062,7 +1063,7 @@ public function testXMustHaveMethodForRegisteringHooks(): void $api->setProvider($mockProvider); $api->setEvaluationContext(new EvaluationContext(null, new Attributes(['api' => 'api']))); - $client = new OpenFeatureClient($api, 'name', 'version'); + $client = new OpenFeatureClient($api, 'name'); $client->setEvaluationContext(new EvaluationContext(null, new Attributes(['client' => 'client']))); $testRunner = $this; @@ -1128,7 +1129,7 @@ public function testHooksAreEvaluatedInTheCorrectOrderInBeforeHook(): void $api->setProvider($mockProvider); $api->addHooks($apiHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $client->addHooks($clientHook); $actualValue = $client->getBooleanValue('key', false, new EvaluationContext(null, new Attributes(['invocation' => 'invocation'])), new EvaluationOptions([$invocationHook])); @@ -1183,7 +1184,7 @@ public function testHooksAreEvaluatedInTheCorrectOrderInAfterHook(): void $api->setProvider($testProvider); $api->addHooks($apiHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $client->addHooks($clientHook); $actualValue = $client->getBooleanValue('key', false, new EvaluationContext(null, new Attributes(['invocation' => 'invocation'])), new EvaluationOptions([$invocationHook])); @@ -1240,7 +1241,7 @@ public function testHooksAreEvaluatedInTheCorrectOrderInErrorHook(): void $api->setProvider($mockProvider); $api->addHooks($apiHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $client->addHooks($clientHook); $actualValue = $client->getBooleanValue('key', false, new EvaluationContext(null, new Attributes(['invocation' => 'invocation'])), new EvaluationOptions([$invocationHook])); @@ -1295,7 +1296,7 @@ public function testHooksAreEvaluatedInTheCorrectOrderInFinallyHook(): void $api->setProvider($testProvider); $api->addHooks($apiHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $client->addHooks($clientHook); $actualValue = $client->getBooleanValue('key', false, new EvaluationContext(null, new Attributes(['invocation' => 'invocation'])), new EvaluationOptions([$invocationHook])); @@ -1326,7 +1327,7 @@ public function testFinallyHookAbnormalExecutionMustContinueRemainingFinallyHook $api->setProvider($mockProvider); $api->addHooks($erroringHook, $subsequentHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); @@ -1356,7 +1357,7 @@ public function testErrorHookAbnormalExecutionMustContinueRemainingErrorHooks(): $api->setProvider($mockProvider); $api->addHooks($erroringHook, $subsequentHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); @@ -1386,7 +1387,7 @@ public function testErrorsInBeforeHooksMustInvokeErrorHooks(): void $api->setProvider($mockProvider); $api->addHooks($errorHook, $failingBeforeHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); @@ -1416,7 +1417,7 @@ public function testErrorsInAfterHooksMustInvokeErrorHooks(): void $api->setProvider($mockProvider); $api->addHooks($errorHook, $failingAfterHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); @@ -1446,7 +1447,7 @@ public function testErrorsInBeforeHooksStopsFurtherExecutionOfRemainingSaidHooks $api->setProvider($mockProvider); $api->addHooks($erroringHook, $subsequentHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); @@ -1477,7 +1478,7 @@ public function testErrorsInAfterHooksStopsFurtherExecutionOfRemainingSaidHooks( // error hooks run in reverse order $api->addHooks($subsequentHook, $erroringHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); @@ -1504,24 +1505,13 @@ public function testErrorInBeforeHooksMustReturnDefaultValue(): void $api->setProvider($mockProvider); $api->addHooks($erroringBeforeHook); - $client = new OpenFeatureClient($api, 'test-name', 'test-version'); + $client = new OpenFeatureClient($api, 'test-name'); $actualValue = $client->getBooleanValue('key', false); $this->assertEquals($actualValue, false); } - public function testCanGetVersion(): void - { - $expectedVersion = 'a.b.c'; - - $client = new OpenFeatureClient(APITestHelper::new(), 'name', $expectedVersion); - - $actualVersion = $client->getVersion(); - - $this->assertEquals($expectedVersion, $actualVersion); - } - /** * @return Provider|MockInterface */ diff --git a/tests/unit/ProviderTest.php b/tests/unit/ProviderTest.php index 18a203a..e4acd8f 100644 --- a/tests/unit/ProviderTest.php +++ b/tests/unit/ProviderTest.php @@ -8,6 +8,7 @@ use OpenFeature\Test\TestCase; use OpenFeature\Test\TestHook; use OpenFeature\Test\TestProvider; +use OpenFeature\implementation\flags\FlagMetadata; use OpenFeature\implementation\provider\ResolutionDetailsBuilder; use OpenFeature\implementation\provider\ResolutionDetailsFactory; use OpenFeature\implementation\provider\ResolutionError; @@ -202,6 +203,47 @@ public function testShouldPopulateReasonField(): void $this->assertEquals($expectedReason, $actualResolution->getReason()); } + /** + * Requirement 2.6 + * + * The provider SHOULD populate the flag resolution structure's reason field with "DEFAULT", "TARGETING_MATCH", "SPLIT", "DISABLED", "UNKNOWN", "ERROR" or some other string indicating the semantic reason for the returned flag value. + */ + public function testShouldPopulateFlagMetadataField(): void + { + $expectedFlagMetadata = new FlagMetadata(['str' => 'value', 'num' => 42, 'bool' => true]); + + /** @var Mockery\MockInterface|Provider $mockProvider */ + $mockProvider = $this->mockery(TestProvider::class)->makePartial(); + $mockProvider->shouldReceive('resolveBooleanValue') + ->andReturn((new ResolutionDetailsBuilder()) + ->withValue(true) + ->withFlagMetadata($expectedFlagMetadata) + ->build()); + + $actualResolution = $mockProvider->resolveBooleanValue('flagKey', false, null); + + $this->assertEquals($expectedFlagMetadata, $actualResolution->getFlagMetadata()); + } + + /** + * Requirement 2.6 + * + * The provider SHOULD populate the flag resolution structure's reason field with "DEFAULT", "TARGETING_MATCH", "SPLIT", "DISABLED", "UNKNOWN", "ERROR" or some other string indicating the semantic reason for the returned flag value. + */ + public function testShouldDefaultAnEmptyFlagMetadataField(): void + { + $expectedFlagMetadata = new FlagMetadata(); + + /** @var Mockery\MockInterface|Provider $mockProvider */ + $mockProvider = $this->mockery(TestProvider::class)->makePartial(); + $mockProvider->shouldReceive('resolveBooleanValue') + ->andReturn(ResolutionDetailsFactory::fromSuccess(true)); + + $actualResolution = $mockProvider->resolveBooleanValue('flagKey', false, null); + + $this->assertEquals($expectedFlagMetadata, $actualResolution->getFlagMetadata()); + } + /** * Requirement 2.7 *