Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Jean-Beru
Copy link
Contributor

@Jean-Beru Jean-Beru commented Dec 26, 2023

Q A
Branch? 8.0
Bug fix? no
New feature? yes
Deprecations? no
Issues
License MIT

Simple FeatureFlag component

Introduction

Co-authored with @Neirda24 who is also at the origin of this component.

Based on the initial pull request #51649:

Currently, there is no straightforward method to conditionally execute parts of the source code based on specific
contexts. This PR tries to solve this issue by integrating some easy way to check is this or that feature should be
enabled.

First check out Martin Fowler's article about different use cases for feature flag (a.k.a. feature toggling).
He categorizes feature flag like this:

  • Experiment: show a beta version of your website to users who subscribed to.
  • Release: deploy a new version of your code but keep the old one to compare them easily and rollback quickly if needed
    or control when a feature is released.
  • Permission: grant access to a feature for paid accounts using the Security component.
  • Ops: remove access to a consuming feature if server ressources are low (a k.a. kill switch). During Black Friday for
    example it is common to deactivate certain features for Ops because the load will be on other pages.

There are already some libraries / bundles out there but they either lack extensibility or are locked in to a SAAS
tool (Unleash, Gitlab (which uses Unleash)).

More reading

Proposal

The FeatureFlag component provides a service that checks if a feature is enabled. A feature is a callable which returns
a value compared to the expected value to determine is the feature is enabled (mostly a boolean but not limited to).

Since every declared feature is a callable, they are only resolved when needed.

The component

A service implementing the ProviderInterface is responsible for giving a callable from a feature name. This callable,
which does not accept any argument (for now), can be called to get the feature value.

The FeatureChecker is responsible for checking if a feature is enabled from its name. It uses a ProviderInterface to
retrieve the corresponding feature, resolves it and compare it to the expected value (true by default).

For now, two providers are currently implemented:

  • InMemoryProvider which contains the features passed in its constructor
  • ChainProvider which aggregates multiple providers and iterates through them to retrieve a feature. It allows
    combining different sources of feature definitions, such as in-memory, database, or external services.

Usage

Declare your features using a provider and pass it to the checker:

use Symfony\Component\FeatureFlag\FeatureChecker;
use Symfony\Component\FeatureFlag\Provider\InMemoryProvider;

$provider = new InMemoryProvider([
    'weekend_feature' => fn() => date('N') >= 6, // will only be activated on weekends
    'feature_version' => fn() => random_int(1, 3), // will return a random version to use
]);
$featureChecker = new FeatureChecker($provider);

Of course, invokable classes can be used:

use Symfony\Component\FeatureFlag\FeatureChecker;
use Symfony\Component\FeatureFlag\Provider\InMemoryProvider;

final class XmasFeature
{
    public function __invoke(): bool
    {
        return date('m-d') === '12-25';
    }
}

$features = new InMemoryProvider([
    'xmas-feature' => new XmasFeature(),
]);
$featureChecker = new FeatureChecker($features);

Check the feature's value:

$featureChecker->isEnabled('weekend_feature'); // returns true on weekend
$featureChecker->isEnabled('not_a_feature'); // returns false

Once the feature's value is resolved, it is stored to make it immutable:

$featureChecker->getValue('feature_version'); // returns a random version between 1 and 3
$featureChecker->getValue('feature_version'); // returns the previous value

FrameworkBundle integration

In addition, the #[AsFeature] attribute can be used to make a feature from a service:

// Will create a feature named "UserFeature"
use App\Security\User;
use Symfony\Bundle\SecurityBundle\Security;

#[AsFeature]
final class UserFeature
{
    public function __construct(private readonly Security $security) {}

    public function __invoke(?User $user): bool
    {
        if (!($user = $this->security->getUser()) instanceof User) {
            return false;
        } 
    
        return $user->hasGroup(1);
    }
}

In fact, any callable can be declared as a feature using this attribute.

// Will create three features named "AppFeatures::alpha", "AppFeatures::beta", "gamma" and "delta"
#[AsFeature(name: 'delta', method: 'delta')]
final class AppFeatures
{
    #[AsFeature]
    public function alpha(): bool
    {
        // ...
    }
    
    #[AsFeature]
    public function beta(): bool
    {
        // ...
    }
    
    #[AsFeature(name: 'gamma')]
    public function gamma(): bool
    {
        // ...
    }
    
    public function delta(): bool
    {
        // ...
    }
}

Thanks to the ExpressionLanguage component, feature flags can be used in Route definitions.

class MyController
{
    #[Route(path: '/', name: 'homepage_new', condition: 'feature_is_enabled("beta")', priority: 50)]
    public function homepageNew(FeatureCheckerInterface $featureChecker): Response
    {
        // Will be called if the "beta" feature flag is enabled
    }
   
    #[Route(path: '/', name: 'homepage')]
    public function homepage(FeatureCheckerInterface $featureChecker): Response
    {
        // Will be called if the "beta" feature flag is not enabled
    }

WebProfilerBundle integration

The Feature Flag component integrates with the WebProfilerBundle to provide insights into feature evaluations:

Resolved features in the toolbar:

Resolved features in the toolbar

Resolved features in the panel:

Resolved features in the panel

Unresolved features in the panel:

Unresolved features in the panel:

Disabled FeatureFlag menu in the panel:

Resolved feature details in the panel

TwigBridge integration

Feature flags can be checked in Twig template if needed.

Beta features enabled: {{ feature_is_enabled('beta')  ? '' : '' }}

App version used: {{ feature_get_value('app_version')  ? '' : '' }}

Going further (to discuss in dedicated PRs)

  • Add documentation
  • Integrate popular SaaS tool as bridges (Gitlab,
    LaunchDarkly, etc.) by adding a dedicated provider
  •  Propose a Doctrine-based provider
  • Propose pre-implemented strategies like opening a feature to a percentage of visitors
  • Resolve feature's arguments like it is done for controllers
  • Add a #[FeatureEnabled] to restrict access to a controller
  • Declare features from the semantic configuration using the ExpressionLanguage component
  • Add a debug:feature to make feature debugging easier
  • Trace feature resolution
  • Dispatch events

About caching

Immutability

The resolved value of a feature must be immutable during the same request to avoid invalid behaviors. That's why the
FeatureChecker keeps values internally.

Heavy computing

Even if features are lazy loaded, they can be resolved in every request to determine if a controller can be accessed. It
could be problematic when the provider retrieves this value from a database or an external service. Depending on the
solution chosen to store features, the logic to cache value may differ. That's why the value caching between requests
should be handled by providers.

For instance, Unleashed can define some strategy configuration that can be cached and used to resolve feature values.
See https://github.com/Unleash/unleash-client-php/tree/main/src/Strategy.

Try it

Note

Want to test it on an existing project? This component is now available as a bundle, thanks to the work of @ajgarlag! 🎉

Important

The purpose of this bundle is to allow you to test the code proposed in the PR.

@OskarStark
Copy link
Contributor

I like this simplified version, which is all, I would need in the first step ❤️ Thank you!

@noahlvb
Copy link

noahlvb commented Jan 2, 2024

In the going further section Gitlab is mentioned. The Gitlab feature flag is based on and compatible with the Unleash api spec. So maybe it is worth to implement that api since a few services are compatible with it.

@Neirda24
Copy link
Contributor

Neirda24 commented Jan 2, 2024

@noahlvb : yes we have this in mind and plan on implementing :

  • unleash
  • gitlab
  • DarkLaunchy
  • Maybe Jira

and more if further contributions.

@OskarStark
Copy link
Contributor

Bridges to providers should be proposed in a follow-up PR

@Jean-Beru Jean-Beru force-pushed the rfc/simple-feature-flag branch from 04de789 to 0c4aec4 Compare January 2, 2024 12:43
@yceruto
Copy link
Member

yceruto commented Jan 2, 2024

About the proposed API, I have some suggestions:

$featureChecker->getValue('feature_version'); // returns a random version between 1 and 3

If we take the stricter path of the concept, a feature flag should deal only with a boolean result at the end. So, I don't think the getValue() method should be public. Otherwise, it can be used as a configuration fetcher rather than a toggle checker, defeating its purpose. I would make this method private or protected instead, in charge of resolving the boolean toggle value.

$features = new FeatureRegistry(['feature_version' => fn() => random_int(1, 3)]);
$featureChecker->isEnabled('feature_version', 1); // returns true if the random version is 1

Following the same principle, the toggle callable should always return a boolean value, receiving the arguments necessary to compute it. Thus, we can pass all arguments from isEnabled('feature', ...args) to the toggle decider rather than a hardcoded equal condition, which may not be sufficient.

Example:

$features = new FeatureRegistry([
    'feature_version' => fn (int $v): bool => random_int(1, 3) === $v,
]);
$checker = new FeatureChecker($features);

if ($checker->isEnabled('feature_version', 1)) {
   // ...
}

We could also add isDisabled(...) method to improve the semantic of negative conditions.

This way feature deciders (the callables here) have full control over the computation of the feature value, and the feature checker will be responsible for checking/caching the value only.

@nicolas-grekas
Copy link
Member

On my side I think it's important to support non-boolean values from day 1.
Maybe it can be abused as a config fetcher, but A/B/C testing needs this for example.
(and L has it ;) )

Copy link
Member

@welcoMattic welcoMattic left a comment

Choose a reason for hiding this comment

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

Thanks for the huge work @Jean-Beru @Neirda24 👏

@welcoMattic welcoMattic added ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" and removed ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" labels May 2, 2025
@Jean-Beru Jean-Beru force-pushed the rfc/simple-feature-flag branch from 47d16ba to 2f490f8 Compare May 2, 2025 20:31
@Jean-Beru Jean-Beru force-pushed the rfc/simple-feature-flag branch from 66c5c73 to 802cd2c Compare May 5, 2025 08:56
@ajgarlag
Copy link
Contributor

ajgarlag commented May 5, 2025

A tricky way is to use a repository in your composer.json to download the component:

    "require": {
        "symfony/feature-flags": "dev-rfc/simple-feature-flags"
    }
    "repositories": [
        {
            "type": "package",
            "package": {
                "name": "symfony/feature-flags",
                "version": "dev-rfc/simple-feature-flags",
                "dist": {
                    "url": "https://github.com/Jean-Beru/symfony/archive/refs/heads/rfc/simple-feature-flag.zip",
                    "type": "zip"
                },
                "autoload": {
                    "psr-4": { "Symfony\\Component\\FeatureFlag\\": "src/Symfony/Component/FeatureFlags" }
                }
            }
        }
    ]

@Jean-Beru What do you think of the approach used in jwage/phpamqplib-messenger? You could release it as a third-party bundle that could be subsequently integrated it into the Symfony Core.

@ajgarlag
Copy link
Contributor

I've released ajgarlag/feature-flag-bundle, a bundle for Symfony inspired by this PR.

The goal of developing the bundle is to provide an easy way to test the proposed features in the PR. Feel free to check it out, and I'd appreciate any feedback!

A huge thank you to @Jean-Beru for helping me with its development and release!


public function isEnabled(string $featureName): bool
{
return true === $this->getValue($featureName);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't it check if it's enabled from the provider? Not every feature flag is a Boolean unless that's all this component intends to support.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The isEnabled method is a helper to improve DX on boolean flags. For other flag value types, we can use the getValue method.

Copy link
Contributor

Choose a reason for hiding this comment

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

But having the method there implies it should be useable on all flags. I would remove the helper method, or require specifying the type of the flag. If the flag isn't a Boolean, then throw an error. Another option would be to specify what the value should look like to be considered enabled. Then it would support all flags. Or even better, accept a callback as an optional argument to "isEnabled". The default handling of this method can check against true, and if you use a non book flag, you can use the callback.

For example, we often have json flags that have an enabled property in it, but we also have other configuration there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But having the method there implies it should be useable on all flags. I would remove the helper method, or require specifying the type of the flag.

Another option would be to specify what the value should look like to be considered enabled. Then it would support all flags.

We previously discussed the $expectedValue argument (e.g., $featureChecker->isEnabled('expected_value');) and decided to remove it for now to keep the component as simple as possible. Of course, a future PR could reintroduce this functionality.

Or even better, accept a callback as an optional argument to "isEnabled". The default handling of this method can check against true, and if you use a non book flag, you can use the callback.

However, while a callback would solve the issue, it might degrade the developer experience (DX) of isEnabled. The getValue method already offers similar flexibility:

// Using a callback
$featureChecker->isEnabled(fn($v) $v->enable_property === 'expected value');

// Using the getValue method
$featureChecker->getValue()?->enable_property === 'expected value';

If the flag isn't a Boolean, then throw an error.

As for throwing an error when a flag isn’t boolean, I’d advise against it. Since the checker might be called on every request, an exception could render the application unusable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just remove the isEnabled then? It's not doing much but implies it should work with all flags which it won't. It will lead to bugs if someone doesn't realize it only works with bools because a non bool flag will never be seen as enabled. If simplicity is the key, why have a method that doesn't always work?

Copy link
Contributor

Choose a reason for hiding this comment

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

@Jean-Beru I didn't get a response here and am worried this is going to make it into the final version of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the late reply.

I agree that the isEnabled does not fit well with non-boolean feature flags. IMO, we should keep it to improve the DX for the frequent use case of boolean flags.

The alternatives are less than ideal:

  • The current implementation only handle boolean flags.
  • Removing the method would degrade the user experience for a primary use case.
  • Throwing an exception could crash applications if a flag's type is modified.

Maybe isEnabled should return false and simultaneously dispatch a warning log or an event. This provides a safe, non-breaking default while ensuring developers are notified of the type mismatch.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious, how does it degrade the user experience?

if ($flagChecker->getValue('flag1')) {
 // ...
}

vs

if ($flagChecker->isEnabled('flag1')) {
 // ...
}

If you use non-boolean comparisons from phpstan, then yes you would need to do

if ($flagChecker->getValue('flag1') === true) {
 // ...
}

vs just calling isEnabled, but it's really not that much code. I don't see the benefit of adding a warning and doing all of the other work to ensure that isEnabled isn't incorrectly used vs just removing the method. If a helper is really needed, then at minimum it should be changed (IMO) to isBooleanFlagEnabled to ensure that people are aware it only works for boolean flags, and then throw an exception if the value isn't actually boolean. A log or event (which is only really going to be seen in logs unless someone is using the profiler), isn't guaranteed to be noticed.

While boolean flags are probably the predominant type, we have a ton of non boolean flags ourselves. We had a similar system to this, without all of the Symfony integration, and got rid of isEnabled for precisely this use case. It requires more review of code to ensure someone doesn't call it on a non boolean flag.

Also what happens if a flag is defined as one type in one provider, and another type in another? For example, for local development someone defines it as a boolean, but in production using LaunchDarkly it's a json? This leads me to feel that this feature needs a registered flag system where you define the available flags and their types, as well as the default values. This would prevent this type of scenario.

@Jean-Beru Jean-Beru force-pushed the rfc/simple-feature-flag branch from 802cd2c to 3c50e47 Compare August 19, 2025 15:35
@Jean-Beru Jean-Beru changed the base branch from 7.4 to 8.0 August 19, 2025 15:35
@Jean-Beru Jean-Beru force-pushed the rfc/simple-feature-flag branch from 189847d to 5d14915 Compare August 25, 2025 08:45
@welcoMattic welcoMattic changed the title [FeatureFlag] Propose a simple version [FeatureFlag] Introduce a simple version Sep 29, 2025
@welcoMattic welcoMattic added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Sep 29, 2025
@Jean-Beru Jean-Beru force-pushed the rfc/simple-feature-flag branch from eec0e8c to aa13f4a Compare September 29, 2025 12:14
@daFish
Copy link

daFish commented Oct 6, 2025

@Jean-Beru @ajgarlag Thanks to the bundle I was able to integrate this feature into my application. I have added a basic doctrine provider which is based on the example from the bundle. It just... works. Thanks for all the hard work. This will be a great new component.

@nicolas-grekas nicolas-grekas removed the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Oct 24, 2025
@nicolas-grekas nicolas-grekas modified the milestones: 7.4, 8.1 Nov 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.