Skip to content
Merged
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
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-06-15

### Added

- Multi-provider support through the SDK `MultiProvider`. The new `providers` configuration key declares several providers (map of provider name => service ID, evaluated in declaration order, mutually exclusive with `provider`), and the `strategy` key selects the evaluation strategy: `first_match` (default), `first_successful`, or `comparison` with a required `fallback` provider name. Configuration is validated at compile time (exclusivity, fallback consistency, case-insensitive provider name uniqueness).
- The profiler panel and toolbar now display the evaluation strategy and the sub-providers in evaluation order (with a `fallback` badge for the `comparison` strategy) when multi-provider is configured.

### Changed

- `open-feature/sdk` requirement raised from `^2.0` to `^2.2` (the bundle relies on the `MultiProvider` classes introduced in SDK 2.2.0).
- The `provider` configuration node now defaults to `null` instead of `InMemoryProvider`. When neither `provider` nor `providers` is set, the bundle still falls back to the built-in `InMemoryProvider`: behavior is unchanged, only the output of `config:dump-reference open_feature` differs.

### Upgrade notes

- Run `composer update open-feature/sdk` if your lock file pins a version below 2.2.0.

## [0.2.0] - 2026-04-24

### Changed
Expand All @@ -29,7 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Initial release.

[Unreleased]: https://github.com/aubes/openfeature-bundle/compare/v0.2.0...HEAD
[Unreleased]: https://github.com/aubes/openfeature-bundle/compare/v0.3.0...HEAD
[0.3.0]: https://github.com/aubes/openfeature-bundle/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/aubes/openfeature-bundle/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/aubes/openfeature-bundle/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/aubes/openfeature-bundle/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Aurélian Bes
Copyright (c) Aurélian Bes

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

Feature flags, the Symfony way.

Symfony bundle for the [OpenFeature PHP SDK](https://github.com/open-feature/php-sdk) the [CNCF standard](https://openfeature.dev) for feature flags.
Symfony bundle for the [OpenFeature PHP SDK](https://github.com/open-feature/php-sdk), the [CNCF standard](https://openfeature.dev) for feature flags.

```php
class CheckoutController
Expand Down Expand Up @@ -35,7 +35,7 @@ class CheckoutController

- PHP 8.2+
- Symfony 6.4, 7.x or 8.x
- `open-feature/sdk` ^2.0 (implements [OpenFeature spec v0.5.1](https://github.com/open-feature/spec/releases/tag/v0.5.1))
- `open-feature/sdk` ^2.2 (implements [OpenFeature spec v0.5.1](https://github.com/open-feature/spec/releases/tag/v0.5.1))

## Quick start

Expand Down Expand Up @@ -105,6 +105,20 @@ For anything beyond a quick demo (user targeting, percentage rollouts, A/B testi
| EnvVarProvider | Kill switches via env vars | `provider` |
| RedisProvider | Shared on/off toggles via Redis | `provider` + `redis` |

### Multiple providers

Combine several providers through the SDK `MultiProvider` with the `providers` key (evaluated in declaration order):

```yaml
open_feature:
providers:
remote: App\OpenFeature\MyProvider
local: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
strategy: first_match # first_match | first_successful | comparison
```

See [Configuration reference](docs/configuration.md#multiple-providers) for strategy details.

## Documentation

Full documentation lives in the [`docs/`](docs/index.md) folder:
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"require": {
"php": ">=8.2",
"open-feature/sdk": "^2.0",
"open-feature/sdk": "^2.2",
"symfony/config": "^6.4|^7.4|^8.0",
"symfony/dependency-injection": "^6.4|^7.4|^8.0",
"symfony/http-kernel": "^6.4|^7.4|^8.0"
Expand Down
51 changes: 51 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,25 @@ open_feature:

# Service ID of the OpenFeature provider
# Default: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
# Mutually exclusive with "providers"
provider: Aubes\OpenFeatureBundle\Provider\InMemoryProvider

# Multiple providers combined through the SDK MultiProvider
# Keys are provider names, values are service IDs
# Evaluation follows declaration order
# Mutually exclusive with "provider"
providers:
remote: App\OpenFeature\MyProvider
local: Aubes\OpenFeatureBundle\Provider\InMemoryProvider

# Evaluation strategy for the MultiProvider (only when using "providers")
# Shorthand: strategy: first_match
strategy:
type: first_match # first_match | first_successful | comparison
# Provider name used as fallback on mismatch
# Required (and only allowed) when type is "comparison"
fallback: ~

# Flags for the InMemoryProvider (dev/test use)
flags:
new_checkout: true
Expand Down Expand Up @@ -53,6 +70,40 @@ open_feature:

See [Providers](providers/index.md) for available options.

## Multiple providers

Declare several providers under `providers` to combine them through the SDK `MultiProvider` (requires `open-feature/sdk` >= 2.2). Each key is a provider name, each value a service ID. Providers are evaluated in declaration order:

```yaml
open_feature:
providers:
remote: App\OpenFeature\MyProvider
local: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
strategy: first_match
```

`provider` and `providers` are mutually exclusive.

### Strategies

| Strategy | Behavior |
|---|---|
| `first_match` (default) | Sequential. Returns the first provider that knows the flag; `FLAG_NOT_FOUND` moves to the next provider, any other error stops the evaluation. |
| `first_successful` | Sequential. Returns the first result without error; errors do not stop the chain. |
| `comparison` | Evaluates all providers and compares results. If they disagree, the result of the `fallback` provider is used. |

The `comparison` strategy requires a `fallback` pointing to one of the declared provider names:

```yaml
open_feature:
providers:
remote: App\OpenFeature\MyProvider
local: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
strategy:
type: comparison
fallback: local
```

## Flags

The `flags` key is only used by the `InMemoryProvider`. Values can be booleans, integers, floats, strings, or arrays:
Expand Down
1 change: 1 addition & 0 deletions docs/profiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
The bundle registers an **OpenFeature panel** in the Symfony Web Debug Toolbar showing:

- Active provider name
- In multi-provider mode: the evaluation strategy, the fallback marker, and the sub-providers in evaluation order
- All flags evaluated during the request (key, type, resolved value, reason, error)
- Global EvaluationContext (targeting key and attributes)

Expand Down
4 changes: 4 additions & 0 deletions docs/providers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Any other provider from [open-feature/php-sdk-contrib](https://github.com/open-f
| [EnvVarProvider](env-var.md) | Kill switches via env vars, no extra infra |
| [RedisProvider](redis.md) | Shared on/off toggles when Redis is already in the stack |

## Multiple providers

Several providers can be combined through the SDK `MultiProvider` using the `providers` key, with a configurable evaluation strategy (`first_match`, `first_successful`, `comparison`). See [Configuration reference](../configuration.md#multiple-providers).

## Writing a custom provider

```php
Expand Down
71 changes: 65 additions & 6 deletions src/DependencyInjection/Compiler/SetProviderPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,92 @@

namespace Aubes\OpenFeatureBundle\DependencyInjection\Compiler;

use OpenFeature\implementation\multiprovider\MultiProvider;
use OpenFeature\implementation\multiprovider\strategy\ComparisonStrategy;
use OpenFeature\implementation\multiprovider\strategy\FirstMatchStrategy;
use OpenFeature\implementation\multiprovider\strategy\FirstSuccessfulStrategy;
use OpenFeature\interfaces\flags\API;
use OpenFeature\interfaces\provider\Provider;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

class SetProviderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$providerId = $container->getParameter('open_feature.provider');
/** @var array<string, string> $providers */
$providers = $container->getParameter('open_feature.providers');

if ($providers !== []) {
$this->registerMultiProvider($container, $providers);

if ($providerId === null || !\is_string($providerId)) {
return;
}

$definition = $container->findDefinition($providerId);
$class = $definition->getClass();
$providerId = $container->getParameter('open_feature.provider');

if ($class !== null && !\is_subclass_of($class, \OpenFeature\interfaces\provider\Provider::class)) {
throw new \InvalidArgumentException(\sprintf('The service "%s" (class "%s") configured as OpenFeature provider must implement "%s".', $providerId, $class, \OpenFeature\interfaces\provider\Provider::class));
if (!\is_string($providerId)) {
return;
}

$definition = $this->validateProvider($container, $providerId);

if ($container->has('logger')) {
$definition->addMethodCall('setLogger', [new Reference('logger')]);
}

$container->getDefinition(API::class)
->addMethodCall('setProvider', [new Reference($providerId)]);
}

/**
* @param array<string, string> $providers
*/
private function registerMultiProvider(ContainerBuilder $container, array $providers): void
{
$hasLogger = $container->has('logger');
$providerData = [];

foreach ($providers as $name => $providerId) {
$definition = $this->validateProvider($container, $providerId);

if ($hasLogger) {
$definition->addMethodCall('setLogger', [new Reference('logger')]);
}

$providerData[] = ['name' => (string) $name, 'provider' => new Reference($providerId)];
}

/** @var array{type: string, fallback: null|string} $strategy */
$strategy = $container->getParameter('open_feature.strategy');

$strategyDefinition = match ($strategy['type']) {
'first_successful' => new Definition(FirstSuccessfulStrategy::class),
'comparison' => new Definition(ComparisonStrategy::class, [new Reference($providers[(string) $strategy['fallback']])]),
default => new Definition(FirstMatchStrategy::class),
};

$definition = new Definition(MultiProvider::class, [$providerData, $strategyDefinition]);

if ($hasLogger) {
$definition->addMethodCall('setLogger', [new Reference('logger')]);
}

$container->getDefinition(API::class)
->addMethodCall('setProvider', [$definition]);
}

private function validateProvider(ContainerBuilder $container, string $providerId): Definition
{
$definition = $container->findDefinition($providerId);
$class = $definition->getClass();

if ($class !== null && !\is_subclass_of($class, Provider::class)) {
throw new \InvalidArgumentException(\sprintf('The service "%s" (class "%s") configured as OpenFeature provider must implement "%s".', $providerId, $class, Provider::class));
}

return $definition;
}
}
8 changes: 6 additions & 2 deletions src/EvaluationContext/EvaluationContextProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
* Implement this interface to contribute attributes to the global OpenFeature
* EvaluationContext on each request.
*
* Multiple providers are supported and their contexts are merged in priority order
* (highest priority first). Use the "priority" attribute on the tag to control ordering.
* Multiple providers are supported. They are iterated highest priority first
* (set the "priority" attribute on the tag), then their contexts are merged by the
* OpenFeature SDK, which gives precedence to the last context on conflicts.
* As a result, a lower-priority provider overrides a higher-priority one for the
* targeting key and any shared attribute. Register a fallback at high priority so a
* real identity contributed at lower priority wins.
*/
interface EvaluationContextProviderInterface
{
Expand Down
Loading
Loading