em411/feature-flag-bundle
A Symfony bundle for feature flags: enable or disable code paths based on context-aware callables. This is a Symfony 4.4 / PHP 7.4 backport of ajgarlag/feature-flag-bundle, keeping the same API and namespace for a drop-in upgrade from the upstream package.
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 https://github.com/symfony/symfony/pull/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.
ajgarlag/feature-flag-bundle — it targets modern Symfony and uses native PHP 8 attributes.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.
Follow these steps to install and use the bundle in your Symfony application.
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
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],
];
You can declare features using the @AsFeature annotation. This allows you to autoconfigure your features as services.
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';
}
}
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.
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 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.
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.
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);
};
}
}
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);
};
}
}
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.
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 %}
How can I help you explore Laravel packages today?