From 9347eabd052be85127f340a2853478cc44703be6 Mon Sep 17 00:00:00 2001 From: Owen Voke Date: Wed, 19 Feb 2025 15:13:05 +0000 Subject: [PATCH] feat: add support for OpenFeature driver --- README.md | 24 ++++++ composer.json | 5 ++ config/feature-flags.php | 6 ++ src/FeatureFlagsManager.php | 15 ++++ .../Contracts/OpenFeatureResolver.php | 12 +++ .../OpenFeature/OpenFeatureProvider.php | 76 +++++++++++++++++++ .../Resolvers/FlagdOpenFeatureResolver.php | 45 +++++++++++ 7 files changed, 183 insertions(+) create mode 100644 src/Providers/OpenFeature/Contracts/OpenFeatureResolver.php create mode 100644 src/Providers/OpenFeature/OpenFeatureProvider.php create mode 100644 src/Providers/OpenFeature/Resolvers/FlagdOpenFeatureResolver.php diff --git a/README.md b/README.md index c502574..aab661c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,30 @@ enum FeatureFlag: string implements \Worksome\FeatureFlags\Contracts\FeatureFlag @endfeature ``` +### OpenFeature + +The `open_feature` driver adds support for [OpenFeature](https://openfeature.dev), a vendor-agnostic ecosystem for feature flagging. + +To use this, follow the steps below: + +1. Install an [OpenFeature provider](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5BallTechnologies%5D%5B0%5D=PHP) + ```shell + composer require open-feature/flagd-provider + ``` +2. Create or use [an existing OpenFeature resolver](src/Providers/OpenFeature/Resolvers) +3. Add the required configuration to the configuration file + ```php + 'open_feature' => [ + 'resolver' => \Worksome\FeatureFlags\Providers\OpenFeature\Resolvers\FlagdOpenFeatureResolver::class, + 'options' => [ + 'protocol' => env('FLAGD_PROTOCOL'), + 'host' => env('FLAGD_HOST'), + 'port' => env('FLAGD_PORT'), + 'secure' => env('FLAGD_SECURE'), + ], + ], + ``` + ## Changelog Please see the [Releases](https://github.com/worksome/feature-flags/releases) for more information on what has changed recently. diff --git a/composer.json b/composer.json index 44aa442..985bc6c 100644 --- a/composer.json +++ b/composer.json @@ -13,15 +13,20 @@ "guzzlehttp/guzzle": "^7.8", "illuminate/contracts": "^11.0 || ^12.0", "launchdarkly/server-sdk": "^6.4", + "open-feature/sdk": "^2.0", "spatie/laravel-package-tools": "^1.19" }, "require-dev": { "nunomaduro/collision": "^8.6", + "open-feature/flagd-provider": "^1.0", "orchestra/testbench": "^9.12 || ^10.1", "pestphp/pest": "^3.7", "pestphp/pest-plugin-laravel": "^3.1", "worksome/coding-style": "^3.2" }, + "suggest": { + "open-feature/flagd-provider": "Required to use the OpenFeature Flagd provider." + }, "autoload": { "psr-4": { "Worksome\\FeatureFlags\\": "src" diff --git a/config/feature-flags.php b/config/feature-flags.php index 616b043..79fa72f 100644 --- a/config/feature-flags.php +++ b/config/feature-flags.php @@ -37,6 +37,12 @@ */ 'access-token' => env('FEATURE_FLAGS_API_ACCESS_TOKEN', null), ], + 'open_feature' => [ + 'resolver' => env('OPEN_FEATURE_RESOLVER'), + 'options' => [ + // ... + ], + ], ], /** diff --git a/src/FeatureFlagsManager.php b/src/FeatureFlagsManager.php index 7fcbbf1..f6eb9ce 100644 --- a/src/FeatureFlagsManager.php +++ b/src/FeatureFlagsManager.php @@ -9,6 +9,8 @@ use Worksome\FeatureFlags\Providers\Bucket\BucketProvider; use Worksome\FeatureFlags\Providers\FakeProvider; use Worksome\FeatureFlags\Providers\LaunchDarkly\LaunchDarklyProvider; +use Worksome\FeatureFlags\Providers\OpenFeature\Contracts\OpenFeatureResolver; +use Worksome\FeatureFlags\Providers\OpenFeature\OpenFeatureProvider; class FeatureFlagsManager extends Manager { @@ -38,6 +40,19 @@ public function createLaunchDarklyDriver(): LaunchDarklyProvider ); } + public function createOpenFeatureDriver(): OpenFeatureProvider + { + /** @var array{resolver: class-string|null, options: array} $config */ + $config = $this->config->get('feature-flags.providers.open_feature'); + /** @var LoggerInterface $logger */ + $logger = $this->getContainer()->get(LoggerInterface::class); + + return new OpenFeatureProvider( + $config, + $logger, + ); + } + public function createFakeDriver(): FakeProvider { return new FakeProvider(); diff --git a/src/Providers/OpenFeature/Contracts/OpenFeatureResolver.php b/src/Providers/OpenFeature/Contracts/OpenFeatureResolver.php new file mode 100644 index 0000000..09ff099 --- /dev/null +++ b/src/Providers/OpenFeature/Contracts/OpenFeatureResolver.php @@ -0,0 +1,12 @@ +|null, options: array} $config */ + public function __construct(public readonly array $config, public readonly LoggerInterface $logger) + { + } + + public function setUser(FeatureFlagUser $user): void + { + $id = (string) $user->id; + + $this->context = new EvaluationContext( + targetingKey: $id, + attributes: new Attributes(['email' => $user->email, ... $user->custom]), // @phpstan-ignore argument.type + ); + } + + public function setAnonymousUser(): void + { + $this->context = new EvaluationContext( + targetingKey: 'anonymous', + ); + } + + public function flag(FeatureFlagEnum $flag): bool + { + assert(is_string($flag->value)); // @phpstan-ignore function.alreadyNarrowedType, function.alreadyNarrowedType + + $resolver = $this->config['resolver'] ?? null; + + if ($resolver === null) { + throw new RuntimeException('The `open_feature` driver requires a resolver.'); + } + + // @phpstan-ignore larastanStrictRules.noGlobalLaravelFunction + $provider = app($resolver); + + if (! $provider instanceof OpenFeatureResolver) { + return false; + } + + if ($this->context === null) { + $this->setAnonymousUser(); + } + + assert($this->context !== null); + + $openFeature = OpenFeatureAPI::getInstance(); + + $openFeature->setProvider($provider($this->config['options'])); + + $openFeature->setLogger($this->logger); + + // @phpstan-ignore arguments.count + return (bool) $openFeature->getClient()->getBooleanValue($flag->value, false, context: $this->context); + } +} diff --git a/src/Providers/OpenFeature/Resolvers/FlagdOpenFeatureResolver.php b/src/Providers/OpenFeature/Resolvers/FlagdOpenFeatureResolver.php new file mode 100644 index 0000000..f510dc1 --- /dev/null +++ b/src/Providers/OpenFeature/Resolvers/FlagdOpenFeatureResolver.php @@ -0,0 +1,45 @@ + Arr::get($options, 'protocol', 'tcp'), + 'host' => Arr::get($options, 'host', 'localhost'), + 'port' => Arr::get($options, 'port', 8013), + 'secure' => Arr::get($options, 'secure', true), + 'http' => [ + 'client' => $this->client, + 'requestFactory' => $this->requestFactory, + 'streamFactory' => $this->streamFactory, + ], + ]); + } +}