-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[FeatureFlag] Introduce a simple version #53213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 8.0
Are you sure you want to change the base?
Conversation
|
I like this simplified version, which is all, I would need in the first step ❤️ Thank you! |
src/Symfony/Component/FeatureFlag/DataCollector/FeatureCheckerDataCollector.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/FeatureFlag/DataCollector/FeatureCheckerDataCollector.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/FeatureFlag/DataCollector/FeatureCheckerDataCollector.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/FeatureFlag/DataCollector/FeatureCheckerDataCollector.php
Outdated
Show resolved
Hide resolved
|
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. |
|
@noahlvb : yes we have this in mind and plan on implementing :
and more if further contributions. |
|
Bridges to providers should be proposed in a follow-up PR |
04de789 to
0c4aec4
Compare
|
About the proposed API, I have some suggestions:
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
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 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 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. |
|
On my side I think it's important to support non-boolean values from day 1. |
src/Symfony/Component/FeatureFlag/Exception/FeatureNotFoundException.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/FeatureFlag/Debug/TraceableFeatureChecker.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/FeatureFlag/Debug/TraceableFeatureChecker.php
Outdated
Show resolved
Hide resolved
welcoMattic
left a comment
There was a problem hiding this 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 👏
47d16ba to
2f490f8
Compare
66c5c73 to
802cd2c
Compare
@Jean-Beru What do you think of the approach used in |
|
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
802cd2c to
3c50e47
Compare
src/Symfony/Component/FeatureFlag/Provider/InMemoryProvider.php
Outdated
Show resolved
Hide resolved
189847d to
5d14915
Compare
Co-authored-by: Adrien Roches <[email protected]>
eec0e8c to
aa13f4a
Compare
|
@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. |
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:
or control when a feature is released.
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
ProviderInterfaceis 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
FeatureCheckeris responsible for checking if a feature is enabled from its name. It uses aProviderInterfacetoretrieve the corresponding feature, resolves it and compare it to the expected value (
trueby default).For now, two providers are currently implemented:
InMemoryProviderwhich contains the features passed in its constructorChainProviderwhich aggregates multiple providers and iterates through them to retrieve a feature. It allowscombining 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:
Of course, invokable classes can be used:
Check the feature's value:
Once the feature's value is resolved, it is stored to make it immutable:
FrameworkBundle integration
In addition, the
#[AsFeature]attribute can be used to make a feature from a service:In fact, any callable can be declared as a feature using this attribute.
Thanks to the ExpressionLanguage component, feature flags can be used in Route definitions.
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 panel:
Unresolved features in the panel:
Disabled FeatureFlag menu 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)
LaunchDarkly, etc.) by adding a dedicated provider
#[FeatureEnabled]to restrict access to a controllerdebug:featureto make feature debugging easierAbout caching
Immutability
The resolved value of a feature must be immutable during the same request to avoid invalid behaviors. That's why the
FeatureCheckerkeeps 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.