Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Comment on lines +67 to +70

@owenvoke owenvoke Sep 2, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these maybe be nested per OpenFeature provider. 🤔 (i.e. options.flagd.*)

],
],
```

## Changelog

Please see the [Releases](https://github.com/worksome/feature-flags/releases) for more information on what has changed recently.
Expand Down
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions config/feature-flags.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
*/
'access-token' => env('FEATURE_FLAGS_API_ACCESS_TOKEN', null),
],
'open_feature' => [
'resolver' => env('OPEN_FEATURE_RESOLVER'),
'options' => [
// ...
],
],
],

/**
Expand Down
15 changes: 15 additions & 0 deletions src/FeatureFlagsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -38,6 +40,19 @@ public function createLaunchDarklyDriver(): LaunchDarklyProvider
);
}

public function createOpenFeatureDriver(): OpenFeatureProvider
{
/** @var array{resolver: class-string<OpenFeatureResolver>|null, options: array<string, mixed>} $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();
Expand Down
12 changes: 12 additions & 0 deletions src/Providers/OpenFeature/Contracts/OpenFeatureResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags\Providers\OpenFeature\Contracts;

use OpenFeature\interfaces\provider\Provider;

interface OpenFeatureResolver
{
public function __invoke(array $options): Provider;
}
76 changes: 76 additions & 0 deletions src/Providers/OpenFeature/OpenFeatureProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags\Providers\OpenFeature;

use OpenFeature\implementation\flags\Attributes;
use OpenFeature\implementation\flags\EvaluationContext;
use OpenFeature\interfaces\flags\EvaluationContext as EvaluationContextContract;
use OpenFeature\OpenFeatureAPI;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Worksome\FeatureFlags\Contracts\FeatureFlagEnum;
use Worksome\FeatureFlags\Contracts\FeatureFlagsProvider;
use Worksome\FeatureFlags\FeatureFlagUser;
use Worksome\FeatureFlags\Providers\OpenFeature\Contracts\OpenFeatureResolver;

class OpenFeatureProvider implements FeatureFlagsProvider
{
private EvaluationContextContract|null $context = null;

/** @param array{resolver: class-string<OpenFeatureResolver>|null, options: array<string, mixed>} $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);
}
}
45 changes: 45 additions & 0 deletions src/Providers/OpenFeature/Resolvers/FlagdOpenFeatureResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Worksome\FeatureFlags\Providers\OpenFeature\Resolvers;

use Illuminate\Support\Arr;
use OpenFeature\interfaces\provider\Provider;
use OpenFeature\Providers\Flagd\FlagdProvider;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use RuntimeException;
use Worksome\FeatureFlags\Providers\OpenFeature\Contracts\OpenFeatureResolver;

/** @link https://packagist.org/packages/open-feature/flagd-provider */
readonly class FlagdOpenFeatureResolver implements OpenFeatureResolver
{
public function __construct(
private ClientInterface $client,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
) {
}

/** @param array{protocol?: string, host?: string, port?: int|numeric-string, secure?: bool} $options */
public function __invoke(array $options): Provider
{
if (! class_exists(FlagdProvider::class)) {
throw new RuntimeException('The `open-feature/flagd-provider` package is required.');
}

return new FlagdProvider([
'protocol' => 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,
],
]);
}
}