Symfony bundle for the OpenFeature PHP SDK -- the CNCF standard for feature flags.
Switch between providers (Flagd, Flagsmith, Unleash, LaunchDarkly...) without touching your application code.
- PHP 8.2+
- Symfony 6.4, 7.x or 8.x
open-feature/sdk^2.0 (implements OpenFeature spec v0.5.1)
composer require aubes/openfeature-bundleNote: Without a Symfony Flex recipe, register the bundle manually in
config/bundles.php:Aubes\OpenFeatureBundle\OpenFeatureBundle::class => ['all' => true],
# config/packages/open_feature.yaml
open_feature:
# Service ID of the OpenFeature provider (default: built-in InMemoryProvider)
provider: Aubes\OpenFeatureBundle\Provider\InMemoryProvider
# Flags for the InMemoryProvider (dev/test use)
flags:
new_checkout: true
dark_mode: false
max_items: 10
# EvaluationContext: populate targeting key from the authenticated Symfony user
evaluation_context:
user_provider: auto # auto | true | false
# Exception behavior when a #[FeatureGate] flag is disabled
feature_flag:
on_disabled: auto # auto | access_denied | http_exception
status_code: 403The bundle works with any OpenFeature provider. Point to a service that implements the Provider interface:
open_feature:
provider: App\OpenFeature\MyProviderUse a provider from open-feature/php-sdk-contrib or a vendor SDK (ConfigCat, LaunchDarkly, Kameleoon...):
use OpenFeature\implementation\provider\AbstractProvider;
use OpenFeature\interfaces\provider\Provider;
class MyProvider extends AbstractProvider implements Provider
{
// implement resolveBooleanValue, resolveStringValue, etc.
}The bundle also ships with three simple providers for quick starts and basic use cases (kill switches, on/off toggles). These are static flag stores: they ignore the EvaluationContext (no targeting, no rollout rules). For user targeting, percentage rollouts, or A/B testing, use a dedicated provider like Flagd, Unleash, or LaunchDarkly.
Flags defined statically in configuration. Ideal for local development and tests.
open_feature:
flags:
my_feature: trueResolves flags from environment variables. Ideal for staging/production without a dedicated backend.
open_feature:
provider: Aubes\OpenFeatureBundle\Provider\EnvVarProviderFEATURE_NEW_CHECKOUT=true
FEATURE_DARK_MODE=falseFlag keys are uppercased and prefixed: new_checkout -> FEATURE_NEW_CHECKOUT. Hyphens and dots are replaced by underscores. The prefix is configurable via the constructor.
Resolves flags from Redis string keys. Ideal when you already have Redis in your stack and want dynamic flags without a dedicated backend.
open_feature:
provider: Aubes\OpenFeatureBundle\Provider\RedisProvider
redis:
client: App\OpenFeature\MyRedisClient # service implementing RedisClientInterface
prefix: 'feature:' # key prefix (default: "feature:")Each flag is stored as a plain Redis string:
feature:new_checkout -> "true"
feature:max_items -> "10"
feature:config -> '{"color":"blue"}'
Boolean truthy values: true, 1, yes, on. Object values must be JSON-encoded. The key prefix defaults to feature: and is configurable.
If Redis is unavailable, the provider returns the default value with an error reason instead of throwing an exception.
The RedisProvider depends on a RedisClientInterface service. Implement it to wrap your Redis connection (\Redis, Predis\Client, etc.):
use Aubes\OpenFeatureBundle\Provider\Redis\RedisClientInterface;
class MyRedisClient implements RedisClientInterface
{
public function __construct(private readonly \Redis $redis) {}
public function get(string $key): string|false|null
{
return $this->redis->get($key);
}
}use OpenFeature\interfaces\flags\Client;
class MyService
{
public function __construct(private readonly Client $client) {}
public function checkout(): void
{
if ($this->client->getBooleanValue('new_checkout', false)) {
// new flow
}
}
}All flag types are supported: getBooleanValue, getStringValue, getIntegerValue, getFloatValue, getObjectValue.
{% if feature('new_checkout') %}
{# new checkout #}
{% endif %}
{# Non-boolean values #}
{{ feature_value('theme', 'light') }}
{{ feature_value('max_items', 10) }}Inject resolved flag values directly into controller parameters:
use Aubes\OpenFeatureBundle\Attribute\FeatureFlag;
public function index(
#[FeatureFlag('dark_mode')] bool $darkMode,
#[FeatureFlag('max_items')] int $maxItems,
#[FeatureFlag('theme')] string $theme,
): Response {
// values resolved from the active provider
}Supported types: bool, string, int, float, array. Without a type hint (or mixed), defaults to bool.
Restrict access to a controller action based on a flag:
use Aubes\OpenFeatureBundle\Attribute\FeatureGate;
#[FeatureGate('new_checkout')]
public function checkout(): Response
{
// throws AccessDeniedException (or HttpException 403) if flag is disabled
}Multiple gates can be stacked on the same method. The exception message includes the flag name for easier debugging.
The exception type is auto-detected: AccessDeniedException if symfony/security-core is available, HttpException otherwise. Override via configuration:
open_feature:
feature_flag:
on_disabled: http_exception
status_code: 404The EvaluationContext carries targeting information (user ID, attributes) used by providers for segmentation, A/B testing, and percentage rollouts.
When symfony/security-core is available, the authenticated user's identifier is automatically set as the targeting_key:
open_feature:
evaluation_context:
user_provider: true # or "auto" (default)Implement EvaluationContextProviderInterface to contribute additional attributes:
use Aubes\OpenFeatureBundle\EvaluationContext\EvaluationContextProviderInterface;
use OpenFeature\implementation\flags\MutableAttributes;
use OpenFeature\implementation\flags\MutableEvaluationContext;
use OpenFeature\interfaces\flags\EvaluationContext;
use Symfony\Component\HttpFoundation\Request;
class TenantContextProvider implements EvaluationContextProviderInterface
{
public function getContext(Request $request): ?EvaluationContext
{
return new MutableEvaluationContext(null, new MutableAttributes([
'tenant' => $request->attributes->get('tenant'),
'plan' => 'premium',
]));
}
}The interface is autoconfigured: if your service implements EvaluationContextProviderInterface, the openfeature.evaluation_context_provider tag is added automatically. No manual tagging required.
Multiple providers are supported and their contexts are merged. The global context is reset between requests (FrankenPHP worker mode safe).
Hooks run around every flag evaluation. Register a global hook with the openfeature.hook tag:
use OpenFeature\interfaces\hooks\Hook;
use OpenFeature\interfaces\hooks\HookContext;
use OpenFeature\interfaces\hooks\HookHints;
use OpenFeature\interfaces\provider\ResolutionDetails;
class LoggerHook implements Hook
{
public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void
{
// log, trace, metrics...
}
// also: before(), error(), finally(), supportsFlagValueType()
}services:
App\OpenFeature\LoggerHook:
tags: [openfeature.hook]Pre-built hooks (OpenTelemetry, Datadog) are available in open-feature/php-sdk-contrib.
List all feature flags detected in your controllers and their current values:
php bin/console debug:feature-flagsThe command scans routes for #[FeatureFlag] and #[FeatureGate] attributes and evaluates them against the active provider. Note that flags are evaluated without HTTP request context (no authenticated user).
The bundle registers an OpenFeature panel in the Symfony Web Debug Toolbar showing:
- Active provider name
- All flags evaluated during the request (key, type, resolved value, reason, error)
- Global EvaluationContext (targeting key and attributes)
The global EvaluationContext is automatically reset between requests via Symfony's kernel.reset mechanism. Hooks and the provider are preserved (set once at boot). No configuration required.