em411 / feature-flag-bundle
Provides a feature flag mechanism (Symfony 4.4 / PHP 7.4 backport)
Package info
github.com/em411/feature-flag-bundle
Type:symfony-bundle
pkg:composer/em411/feature-flag-bundle
Requires
- php: >=7.4
- doctrine/annotations: ^1.13
- psr/container: ^1.0
- symfony/config: ^4.4
- symfony/dependency-injection: ^4.4
- symfony/http-kernel: ^4.4
- symfony/service-contracts: ^1.1|^2.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- symfony/framework-bundle: ^4.4
- symfony/phpunit-bridge: ^4.4
- symfony/twig-bundle: ^4.4
- symfony/var-dumper: ^4.4
- symfony/yaml: ^4.4
- twig/twig: ^2.12 | >=3.0,<3.7
This package is auto-updated.
Last update: 2026-05-26 17:00:43 UTC
README
The FeatureFlag Bundle allows you to split the code execution flow by enabling features depending on context.
It provides a service that checks if a feature is enabled. Each feature is defined by a callable function that returns a value. The feature is enabled if the value matches the expected one (mostly a boolean but not limited to).
This bundle code has been borrowed from symfony/symfony#53213.
Symfony 4.4 / PHP 7.4 backport. This is a fork of ajgarlag/feature-flag-bundle refactored to run on Symfony 4.4 and PHP 7.4.
Which package should I use?
- Running Symfony 6+ / PHP 8.1+? Use the upstream
ajgarlag/feature-flag-bundle— it targets modern Symfony and uses native PHP 8 attributes. - Stuck on Symfony 4.4 / PHP 7.4? Use this fork. It exists because the upstream cannot run on legacy stacks. The public API is intentionally kept compatible with the upstream so migration is a namespace-free drop-in.
Note on the namespace. The Composer package is
em411/feature-flag-bundlebut the PHP namespace remainsAjgarlag\FeatureFlagBundle\on purpose, so code written against the upstream works here unchanged. Imports and the bundle class reference (Ajgarlag\FeatureFlagBundle\FeatureFlagBundle::class) stay the same.
🚀 Getting Started
Follow these steps to install and use the bundle in your Symfony application.
Step 1: Download the bundle
Open a command console, enter your project directory and execute the following command to download the latest stable version of this bundle:
composer require em411/feature-flag-bundle
Step 2: Enable the bundle (for non-Flex applications)
Then, enable the bundle by adding it to the list of registered bundles in the config/bundles.php file of your project:
// config/bundles.php return [ // ... Ajgarlag\FeatureFlagBundle\FeatureFlagBundle::class => ['all' => true], ];
✨ Declaring features with annotations
You can declare features using the @AsFeature annotation. This allows you to autoconfigure your features as services.
On a class
You can use the annotation on an invokable class:
namespace App\Feature; use Ajgarlag\FeatureFlagBundle\Attribute\AsFeature; /** * @AsFeature("xmas") */ final class XmasFeature { public function __invoke(): bool { return date('m-d') === '12-25'; } }
The feature will be named xmas. If you don't provide a name, the FQCN of the class will be used.
You can also use the method property of the annotation to specify a method to call on the service.
namespace App\Feature; use Ajgarlag\FeatureFlagBundle\Attribute\AsFeature; /** * @AsFeature(name="xmas", method="isXmas") */ final class XmasFeature { public function isXmas(): bool { return date('m-d') === '12-25'; } }
On a method
You can also use the annotation on a method of a service. The method must be public.
namespace App\Feature; use Ajgarlag\FeatureFlagBundle\Attribute\AsFeature; final class FeatureService { /** * @AsFeature(name="weekend") */ public function isWeekend(): bool { return date('N') >= 6; } /** * @AsFeature */ // The feature will be named "App\Feature\FeatureService::isAnotherFeature" public function isAnotherFeature(): bool { return true; } }
Note: Unlike PHP 8 attributes, a Doctrine annotation cannot be repeated on the same class or method. To declare multiple features on one class, use the
ajgarlag.feature_flag.featureservice tag.
Gating routes by a feature
Symfony 4.4 cannot evaluate services inside route condition expressions, so
route-level feature gating is done in the controller (or a kernel.controller
listener). Inject FeatureCheckerInterface and throw a 404 when the feature is
off:
use Ajgarlag\FeatureFlagBundle\FeatureCheckerInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class CheckoutController { private $featureChecker; public function __construct(FeatureCheckerInterface $featureChecker) { $this->featureChecker = $featureChecker; } public function index(): Response { if (!$this->featureChecker->isEnabled('new_checkout')) { throw new NotFoundHttpException(); } // ... } }
🧩 Providers
Providers are responsible for returning the feature callables. You can create your own provider by implementing the
ProviderInterface.
Any service that implements ProviderInterface is automatically registered as a provider. The bundle comes with a
ChainProvider that allows you to combine multiple providers. The first provider that returns a feature wins.
The ProviderInterface
To create your own provider, you need to implement the ProviderInterface.
namespace App\FeatureProvider; use Ajgarlag\FeatureFlagBundle\Provider\ProviderInterface; class MyProvider implements ProviderInterface { public function get(string $featureName): ?callable { // ... } }
The get method must return a callable if the provider has the feature, or null otherwise.
Doctrine example
namespace App\FeatureProvider; use Ajgarlag\FeatureFlagBundle\Provider\ProviderInterface; use App\Repository\FeatureAssignmentRepository; final class DoctrineProvider implements ProviderInterface { private $featureAssignmentRepository; public function __construct(FeatureAssignmentRepository $featureAssignmentRepository) { $this->featureAssignmentRepository = $featureAssignmentRepository; } public function get(string $featureName): ?callable { // Set context. Example: user identifier, IP, hostname, etc. $context = []; return function () use ($featureName, $context) { return $this->featureAssignmentRepository->featureIsEnabled($featureName, $context); }; } }
Gitlab example
First, declare a service to interact with the Unleash API.
# config/services.yaml services: # A PSR-16 cache wrapping the "cache.unleash" pool, recommended to limit API calls. gitlab.unleash_cache: class: Symfony\Component\Cache\Psr16Cache arguments: ['@cache.unleash'] gitlab.client_factory: class: Unleash\Client\UnleashBuilder factory: ['Unleash\Client\UnleashBuilder', 'createForGitlab'] calls: - { method: withGitlabEnvironment, arguments: ['%env(GITLAB_ENVIRONMENT)%'], returns_clone: true } - { method: withAppUrl, arguments: ['%env(GITLAB_URL)%'], returns_clone: true } - { method: withInstanceId, arguments: ['%env(GITLAB_INSTANCE_ID)%'], returns_clone: true } - { method: withHttpClient, arguments: ['@psr18.http_client'], returns_clone: true } - { method: withCacheHandler, arguments: ['@gitlab.unleash_cache'], returns_clone: true } gitlab.client: class: Unleash\Client\Unleash factory: ['@gitlab.client_factory', 'build'] App\FeatureProvider\GitlabProvider: arguments: $unleash: '@gitlab.client'
namespace App\FeatureProvider; use Ajgarlag\FeatureFlagBundle\Provider\ProviderInterface; use Symfony\Component\Security\Core\Security; use Unleash\Client\Configuration\UnleashContext; use Unleash\Client\Unleash; class GitlabProvider implements ProviderInterface { private $unleash; private $security; public function __construct(Unleash $unleash, Security $security) { $this->unleash = $unleash; $this->security = $security; } public function get(string $featureName): ?callable { $user = $this->security->getUser(); // Set context. Example: user identifier, IP, hostname, etc. $context = new UnleashContext($user ? $user->getUsername() : null); return function () use ($featureName, $context) { return $this->unleash->isEnabled($featureName, $context); }; } }
Priority
You can control the order of the providers in the chain using the priority attribute of the
ajgarlag.feature_flag.provider tag.
Set the priority on the service tag in your configuration:
# config/services.yaml services: App\FeatureProvider\MyProvider: tags: - { name: 'ajgarlag.feature_flag.provider', priority: 10 }
Providers with a higher priority will be checked first.
🎨 Twig extension
The bundle provides two functions to use in Twig templates: feature_is_enabled and feature_get_value.
{% if feature_is_enabled('some_feature') %}
{# ... #}
{% endif %}
{% if feature_get_value('some_feature') == 'some_value' %}
{# ... #}
{% endif %}