From 129043f4e2fb97506ba21a1353d16daf4e4d785c Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 28 Jul 2023 18:56:36 +0200 Subject: [PATCH 01/25] [FeatureToggle] Import component from private sources --- composer.json | 1 + .../FeatureCheckerDataCollector.php | 157 ++++++++++ .../CompilerPass/DebugPass.php | 47 +++ .../CompilerPass/FeatureCollectionPass.php | 42 +++ .../DependencyInjection/Configuration.php | 171 +++++++++++ .../FeatureToggleExtension.php | 135 +++++++++ .../FeatureToggleBundle.php | 26 ++ .../Resources/config/debug.php | 16 ++ .../Resources/config/feature.php | 25 ++ .../Resources/config/providers.php | 13 + .../Resources/config/routing.php | 19 ++ .../Resources/config/strategies.php | 49 ++++ .../Resources/config/twig.php | 16 ++ .../views/Collector/profiler.html.twig | 158 ++++++++++ .../Strategy/CustomStrategy.php | 28 ++ .../RequestStackAttributeStrategy.php | 44 +++ .../DependencyInjection/ConfigurationTest.php | 154 ++++++++++ .../FeatureToggleExtensionTest.php | 144 ++++++++++ .../Twig/FeatureEnabledExtension.php | 31 ++ .../Bundle/FeatureToggleBundle/composer.json | 38 +++ .../FeatureToggleBundle/phpunit.xml.dist | 30 ++ .../Component/FeatureToggle/.gitattributes | 4 + .../Component/FeatureToggle/.gitignore | 3 + .../Debug/TraceableFeatureChecker.php | 35 +++ .../FeatureToggle/Debug/TraceableStrategy.php | 43 +++ .../Component/FeatureToggle/Feature.php | 40 +++ .../FeatureToggle/FeatureChecker.php | 30 ++ .../FeatureToggle/FeatureCheckerInterface.php | 17 ++ .../FeatureToggle/FeatureCollection.php | 103 +++++++ .../FeatureNotFoundException.php | 22 ++ .../Provider/InMemoryProvider.php | 31 ++ .../Provider/ProviderInterface.php | 19 ++ .../FeatureToggle/Provider/RandomProvider.php | 39 +++ .../Strategy/AffirmativeStrategy.php | 48 ++++ .../FeatureToggle/Strategy/DateStrategy.php | 58 ++++ .../FeatureToggle/Strategy/DenyStrategy.php | 22 ++ .../FeatureToggle/Strategy/EnvStrategy.php | 35 +++ .../FeatureToggle/Strategy/GrantStrategy.php | 22 ++ .../FeatureToggle/Strategy/NotStrategy.php | 38 +++ .../Strategy/OuterStrategiesInterface.php | 20 ++ .../Strategy/OuterStrategyInterface.php | 17 ++ .../Strategy/PriorityStrategy.php | 44 +++ .../FeatureToggle/Strategy/RandomStrategy.php | 26 ++ .../Strategy/RequestHeaderStrategy.php | 34 +++ .../Strategy/RequestQueryStrategy.php | 34 +++ .../Strategy/StrategyInterface.php | 19 ++ .../FeatureToggle/StrategyResult.php | 28 ++ .../Tests/FeatureCheckerTest.php | 76 +++++ .../Tests/FeatureCollectionTest.php | 129 +++++++++ .../FeatureToggle/Tests/FeatureTest.php | 108 +++++++ .../AbstractOuterStrategiesTestCase.php | 33 +++ .../Strategy/AffirmativeStrategyTest.php | 95 ++++++ .../Tests/Strategy/DateStrategyTest.php | 272 ++++++++++++++++++ .../Tests/Strategy/PriorityStrategyTest.php | 95 ++++++ .../Tests/StrategyResultTest.php | 76 +++++ .../Component/FeatureToggle/composer.json | 32 +++ .../Component/FeatureToggle/phpunit.xml.dist | 30 ++ 57 files changed, 3121 insertions(+) create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Strategy/CustomStrategy.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/composer.json create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/phpunit.xml.dist create mode 100644 src/Symfony/Component/FeatureToggle/.gitattributes create mode 100644 src/Symfony/Component/FeatureToggle/.gitignore create mode 100644 src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php create mode 100644 src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Feature.php create mode 100644 src/Symfony/Component/FeatureToggle/FeatureChecker.php create mode 100644 src/Symfony/Component/FeatureToggle/FeatureCheckerInterface.php create mode 100644 src/Symfony/Component/FeatureToggle/FeatureCollection.php create mode 100644 src/Symfony/Component/FeatureToggle/FeatureNotFoundException.php create mode 100644 src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php create mode 100644 src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php create mode 100644 src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/DenyStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/GrantStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/StrategyInterface.php create mode 100644 src/Symfony/Component/FeatureToggle/StrategyResult.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/Strategy/AbstractOuterStrategiesTestCase.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php create mode 100644 src/Symfony/Component/FeatureToggle/composer.json create mode 100644 src/Symfony/Component/FeatureToggle/phpunit.xml.dist diff --git a/composer.json b/composer.json index 3d0aa2f4f87af..cb084d5558867 100644 --- a/composer.json +++ b/composer.json @@ -73,6 +73,7 @@ "symfony/error-handler": "self.version", "symfony/event-dispatcher": "self.version", "symfony/expression-language": "self.version", + "symfony/feature-toggle": "self.version", "symfony/filesystem": "self.version", "symfony/finder": "self.version", "symfony/form": "self.version", diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php new file mode 100644 index 0000000000000..4111ea6f1b812 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\DataCollector; + +use Closure; +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\FeatureCollection; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\VarDumper\Caster\ClassStub; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * @phpstan-type FeatureType array{ + * default: bool, + * description: string, + * strategy: StrategyInterface, + * } + * @phpstan-type ToggleType array{ + * feature: string, + * result: bool|null, + * computes: array, + * } + * @phpstan-type ComputeType array{ + * strategyId: string, + * strategyClass: string, + * level: int, + * result: StrategyResult|null, + * } + * + * @property Data|array{ + * features: array, + * toggles: array, + * } $data + */ +final class FeatureCheckerDataCollector extends DataCollector implements LateDataCollectorInterface +{ + /** @var \SplStack */ + private \SplStack $currentToggle; + + /** @var \SplStack */ + private \SplStack $currentCompute; + + public function __construct( + private readonly FeatureCollection $featureCollection, + ) { + $this->data = ['features' => [], 'toggles' => []]; + $this->currentToggle = new \SplStack(); + $this->currentCompute = new \SplStack(); + } + + public function collect(Request $request, Response $response, \Throwable $exception = null): void + { + foreach ($this->featureCollection as $feature) { + $strategy = (Closure::bind(fn(): StrategyInterface => $this->strategy, $feature, Feature::class))(); + $default = (Closure::bind(fn(): bool => $this->default, $feature, Feature::class))(); + + $this->data['features'][$feature->getName()] = [ + 'default' => $default, + 'description' => $feature->getDescription(), + 'strategy' => $strategy, + ]; + } + } + + public function collectIsEnabledStart(string $featureName): void + { + $toggleId = uniqid(); + + $this->data['toggles'][$toggleId] = [ + 'feature' => $featureName, + 'computes' => [], + 'result' => null, + ]; + $this->currentToggle->push($toggleId); + } + + /** + * @param class-string $strategyClass + */ + public function collectComputeStart(string $strategyId, string $strategyClass): void + { + $toggleId = $this->currentToggle->top(); + $computeId = uniqid(); + $level = $this->currentCompute->count(); + + $this->data['toggles'][$toggleId]['computes'][$computeId] = [ + 'strategyId' => $strategyId, + 'strategyClass' => new ClassStub($strategyClass), + 'level' => $level, + 'result' => null, + ]; + $this->currentCompute->push($computeId); + } + + public function collectComputeStop(StrategyResult $result): void + { + $toggleId = $this->currentToggle->top(); + $computeId = $this->currentCompute->pop(); + + $this->data['toggles'][$toggleId]['computes'][$computeId]['result'] = $result; + } + + public function collectIsEnabledStop(bool $result): void + { + $toggleId = $this->currentToggle->pop(); + + $this->data['toggles'][$toggleId]['result'] = $result; + } + + public function getName(): string + { + return 'feature_toggle'; + } + + public function reset(): void + { + $this->data = [ + 'features' => [], + 'toggles' => [], + ]; + } + + public function lateCollect(): void + { + $this->data = $this->cloneVar($this->data); + } + + /** + * @return list|Data + */ + public function getFeatures(): array|Data + { + return $this->data['features']; + } + + /** + * @return list|Data + */ + public function getToggles(): array|Data + { + return $this->data['toggles']; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php new file mode 100644 index 0000000000000..2a4b2dc6acda5 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\FeatureToggle\Debug\TraceableFeatureChecker; +use Symfony\Component\FeatureToggle\Debug\TraceableStrategy; + +final class DebugPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('feature_toggle.data_collector')) { + return; + } + + $container->register('debug.toggle_feature.feature_checker', TraceableFeatureChecker::class) + ->setDecoratedService('toggle_feature.feature_checker') + ->setArguments([ + '$featureChecker' => new Reference('.inner'), + '$dataCollector' => new Reference('feature_toggle.data_collector'), + ]) + ; + + foreach ($container->findTaggedServiceIds('feature_toggle.feature_strategy') as $serviceId => $tags) { + $container->register('debug.'.$serviceId, TraceableStrategy::class) + ->setDecoratedService($serviceId) + ->setArguments([ + '$strategy' => new Reference('.inner'), + '$strategyId' => $serviceId, + '$dataCollector' => new Reference('feature_toggle.data_collector'), + ]) + ; + } + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php new file mode 100644 index 0000000000000..f227e1c7d74b8 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass; + +use Closure; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\FeatureToggle\Provider\ProviderInterface; + +final class FeatureCollectionPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + public function process(ContainerBuilder $container): void + { + $container->registerForAutoconfiguration(ProviderInterface::class)->addTag('feature_toggle.feature_provider'); + + $collection = $container->getDefinition('toggle_feature.feature_collection'); + + foreach ($this->findAndSortTaggedServices('feature_toggle.feature_provider', $container) as $provider) { + $collectionDefinition = (new Definition(Closure::class)) + ->setFactory([Closure::class, 'fromCallable']) + ->setArguments([[$provider, 'provide']]) + ; + + $collection + ->addMethodCall('withFeatures', [$collectionDefinition]) + ; + } + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000000000..27f5612b4fcd9 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -0,0 +1,171 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection; + +use InvalidArgumentException; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use function implode; +use function sprintf; +use function trim; + +/** + * @phpstan-type ConfigurationType array{ + * strategies: array, + * features: array + * } + * + * @phpstan-type ConfigurationStrategy array{ + * name: string, + * type: string, + * with: array + * } + * + * @phpstan-type ConfigurationFeature array{ + * name: string, + * description: string, + * default: bool, + * strategy: string, + * } + */ +final class Configuration implements ConfigurationInterface +{ + private const KNOWN_STRATEGY_TYPES = ['grant', 'deny', 'not', 'date', 'env', 'native_request_header', 'native_request_query', 'request_attribute', 'priority', 'affirmative']; + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('feature_toggle'); + $treeBuilder->getRootNode() // @phpstan-ignore-line + ->children() + // strategies + ->arrayNode('strategies') + ->useAttributeAsKey('name', false) + ->arrayPrototype() + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->info('Will become the service ID in the container.') + ->example('header.feature-strategy') + ->end() + ->scalarNode('type') + ->isRequired() + ->cannotBeEmpty() + ->info(sprintf('Can be one of : %s. Or a service ID.', implode(', ', self::KNOWN_STRATEGY_TYPES))) + ->example('native_request_header') + ->end() + ->variableNode('with') + ->defaultValue([]) + ->example(['name' => 'Some-Header']) + ->info('Additional information required. Depends on type.') + ->end() + ->end() + ->beforeNormalization() + ->always() + ->then(static function (array $strategy): array { + $defaultWith = match($strategy['type']) { + 'date' => ['from' => null, 'until' => null, 'includeFrom' => false, 'includeUntil' => false], + 'not' => ['strategy' => null], + 'env', 'native_request_header', 'native_request_query', 'request_attribute' => ['name' => null], + 'priority', 'affirmative' => ['strategies' => null], + default => [], + }; + + $strategy['with'] ??= []; + $strategy['with'] += $defaultWith; + + return $strategy; + }) + ->end() + ->validate() + ->always() + ->then(static function (array $strategy): array { + /** @var ConfigurationStrategy $strategy */ + $validator = match ($strategy['type']) { + 'date' => static function (array $with): void { + if ('' === trim((string)$with['from'] . (string)$with['until'])) { + throw new InvalidArgumentException('Either "from" or "until" must be provided.'); + } + }, + 'not' => static function (array $with): void { + if ('' === (string)$with['strategy']) { + throw new InvalidArgumentException('"strategy" must be provided.'); + } + }, + 'env' => static function (array $with): void { + if ('' === (string)$with['name']) { + throw new InvalidArgumentException('"name" must be provided.'); + } + }, + 'native_request_header' => static function (array $with): void { + if ('' === (string)$with['name']) { + throw new InvalidArgumentException('"name" must be provided.'); + } + }, + 'native_request_query' => static function (array $with): void { + if ('' === (string)$with['name']) { + throw new InvalidArgumentException('"name" must be provided.'); + } + }, + 'request_attribute' => static function (array $with): void { + if ('' === (string)$with['name']) { + throw new InvalidArgumentException('"name" must be provided.'); + } + }, + 'priority', 'affirmative' => static function (array $with): void { + if ([] === (array)$with['strategies']) { + throw new InvalidArgumentException('"strategies" must be provided.'); + } + }, + default => static fn(): bool => true, + }; + + $validator($strategy['with']); + + return $strategy; + }) + ->end() + ->end() + ->end() + // features + ->arrayNode('features') + ->useAttributeAsKey('name', false) + ->arrayPrototype() + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->info('Name to be used for checking.') + ->example('my-feature') + ->end() + ->scalarNode('description')->defaultValue('')->end() + ->booleanNode('default') + ->defaultFalse() + ->treatNullLike(false) + ->isRequired() + ->info('Will be used as a fallback mechanism if the strategy return StrategyResult::Abstain.') + ->end() + ->scalarNode('strategy') + ->isRequired() + ->cannotBeEmpty() + ->example('header.feature-strategy') + ->info('Strategy to be used for this feature. Can be one of "feature_toggle.strategies[].name" or a valid service id that implements StrategyInterface::class.') + ->end() + ->end() + ->end() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php new file mode 100644 index 0000000000000..c3af597236867 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection; + +use DateTimeImmutable; +use Symfony\Bundle\FeatureToggleBundle\Strategy\CustomStrategy; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\Routing\Router; +use Twig\Environment; +use function array_map; + +/** + * @phpstan-import-type ConfigurationType from Configuration + */ +final class FeatureToggleExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container): void + { + /** @var ConfigurationType $config */ + $config = $this->processConfiguration(new Configuration(), $configs); + + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__) . '/Resources/config')); + $loader->load('feature.php'); + $loader->load('providers.php'); + $loader->load('strategies.php'); + + // Configuration + $this->loadStrategies($container, $config); + $this->loadFeatures($container, $config); + + // Third party + if ($container::willBeAvailable('twig/twig', Environment::class, ['symfony/twig-bundle'])) { + $loader->load('twig.php'); + } + if ($container::willBeAvailable('symfony/expression-language', Router::class, ['symfony/framework-bundle', 'symfony/routing'])) { + $loader->load('routing.php'); + } + + // Debug + if ($container->getParameter('kernel.debug')) { + $loader->load('debug.php'); + } + } + + /** + * @param ConfigurationType $config + */ + private function loadFeatures(ContainerBuilder $container, array $config): void + { + $features = []; + foreach ($config['features'] as $featureName => $featureConfig) { + $definition = new Definition(Feature::class, [ + '$name' => $featureName, + '$description' => $featureConfig['description'], + '$default' => $featureConfig['default'], + '$strategy' => new Reference($featureConfig['strategy']), + ]); + $container->setDefinition($featureName, $definition); + + $features[] = new Reference($featureName); + } + + $container->getDefinition('toggle_feature.provider.in_memory') + ->setArguments([ + '$features' => $features, + ]) + ; + } + + /** + * @param ConfigurationType $config + */ + private function loadStrategies(ContainerBuilder $container, array $config): void + { + $container->registerForAutoconfiguration(StrategyInterface::class) + ->addTag('feature_toggle.feature_strategy') + ; + + foreach ($config['strategies'] as $strategyName => $strategyConfig) { + $container->setDefinition($strategyName, $this->generateStrategy($strategyConfig['type'], $strategyConfig['with'])) + ->addTag('feature_toggle.feature_strategy'); + } + } + + /** + * @param array $with + */ + private function generateStrategy(string $type, array $with): Definition + { + $definition = new ChildDefinition("toggle_feature.abstract_strategy.{$type}"); + + return match ($type) { + 'date' => $definition->setArguments([ + '$from' => new Definition(DateTimeImmutable::class, [$with['from']]), + '$until' => new Definition(DateTimeImmutable::class, [$with['until']]), + '$includeFrom' => $with['includeFrom'], + '$includeUntil' => $with['includeUntil'], + ]), + 'env' => $definition->setArguments(['$envName' => $with['name']]), + 'native_request_header' => $definition->setArguments(['$headerName' => $with['name']]), + 'native_request_query' => $definition->setArguments(['$queryParameterName' => $with['name']]), + 'request_attribute' => $definition->setArguments(['$attributeName' => $with['name']]), // Check if RequestStack class exists + 'priority', 'affirmative' => $definition->setArguments([ + '$strategies' => array_map( + static fn (string $referencedStrategyName): Reference => new Reference($referencedStrategyName), // @phpstan-ignore-line + (array) $with['strategies'], + ), + ]), + 'not' => $definition->setArguments([ + '$inner' => new Reference($with['strategy']), // @phpstan-ignore-line + ]), + 'grant', 'deny', => $definition, + default => (new Definition(CustomStrategy::class))->setDecoratedService($type)->setArguments([ + '$inner' => new Reference('.inner'), + ]), + }; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php b/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php new file mode 100644 index 0000000000000..1aa245f08ab68 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle; + +use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass\DebugPass; +use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass\FeatureCollectionPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +final class FeatureToggleBundle extends Bundle +{ + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new FeatureCollectionPass()); + $container->addCompilerPass(new DebugPass()); + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php new file mode 100644 index 0000000000000..24ab2e8cdd46a --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php @@ -0,0 +1,16 @@ +services(); + + $services->set('feature_toggle.data_collector', FeatureCheckerDataCollector::class) + ->args([ + '$featureCollection' => service('toggle_feature.feature_collection') + ]) + ->tag('data_collector', ['template' => '@FeatureToggle/Collector/profiler.html.twig', 'id' => 'feature_toggle']) + ; +}; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php new file mode 100644 index 0000000000000..b87e03c6d7724 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php @@ -0,0 +1,25 @@ +services(); + + $services->set('toggle_feature.feature_collection', FeatureCollection::class) + ->args([ + '$features' => [], + ]) + ; + + $services->set('toggle_feature.feature_checker', FeatureChecker::class) + ->args([ + '$features' => service('toggle_feature.feature_collection'), + '$whenNotFound' => false, + ]) + ; + $services->alias(FeatureCheckerInterface::class, service('toggle_feature.feature_checker')); +}; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php new file mode 100644 index 0000000000000..9737bf7ccf440 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php @@ -0,0 +1,13 @@ +services(); + + $services->set('toggle_feature.provider.in_memory', InMemoryProvider::class) + ->tag('feature_toggle.feature_provider', ['priority' => 16]) + ; +}; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php new file mode 100644 index 0000000000000..1340a4d4b77bf --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php @@ -0,0 +1,19 @@ +services(); + + $services->set('toggle_feature.routing.provider', \Closure::class) + ->factory([\Closure::class, 'fromCallable']) + ->args([ + [service('toggle_feature.feature_checker'), 'isEnabled'], + ]) + ->tag('routing.expression_language_function', ['function' => 'isFeatureEnabled']) + ; + + $services->get('toggle_feature.feature_checker') + ->tag('routing.condition_service', ['alias' => 'feature']) + ; +}; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php new file mode 100644 index 0000000000000..0ade373ec9b4f --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -0,0 +1,49 @@ +services(); + $services->set($prefix.'grant', GrantStrategy::class)->abstract(); + $services->set($prefix.'not', NotStrategy::class)->abstract()->args([ + '$inner' => abstract_arg('Defined in FeatureToggleExtension') + ]); + $services->set($prefix.'env', EnvStrategy::class)->abstract()->args([ + '$envName' => abstract_arg('Defined in FeatureToggleExtension'), + ]); + $services->set($prefix.'date', DateStrategy::class)->abstract()->args([ + '$from' => abstract_arg('Defined in FeatureToggleExtension'), + '$until' => abstract_arg('Defined in FeatureToggleExtension'), + '$includeFrom' => abstract_arg('Defined in FeatureToggleExtension'), + '$includeUntil' => abstract_arg('Defined in FeatureToggleExtension'), + '$clock' => service('clock')->nullOnInvalid(), + ]); + $services->set($prefix.'request_attribute', RequestStackAttributeStrategy::class)->abstract()->args([ + '$attributeName' => abstract_arg('Defined in FeatureToggleExtension'), + '$requestStack' => service('request_stack')->nullOnInvalid(), + ]); + $services->set($prefix.'native_request_header', RequestHeaderStrategy::class)->abstract()->args([ + '$headerName' => abstract_arg('Defined in FeatureToggleExtension'), + ]); + $services->set($prefix.'native_request_query', RequestQueryStrategy::class)->abstract()->args([ + '$queryParameterName' => abstract_arg('Defined in FeatureToggleExtension'), + ]); + $services->set($prefix.'priority', PriorityStrategy::class)->abstract()->args([ + '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), + ]); + $services->set($prefix.'affirmative', AffirmativeStrategy::class)->abstract()->args([ + '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), + ]); +}; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php new file mode 100644 index 0000000000000..d6e0879c3e037 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php @@ -0,0 +1,16 @@ +services(); + + $services->set('toggle_feature.twig_extension', FeatureEnabledExtension::class) + ->args([ + service('toggle_feature.feature_checker'), + ]) + ->tag('twig.extension') + ; +}; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig b/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig new file mode 100644 index 0000000000000..a2a83d08fcce1 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig @@ -0,0 +1,158 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block page_title 'Feature Toggle' %} + +{% block toolbar %} + {% set icon %} + Feature Toggle + {{ collector.features|length }} + Features + {% endset %} + + {% set text %} +
+
+ Toggles + {% for toggle in collector.toggles %} + {% set unknown = collector.features[toggle['feature']] is not defined %} + {% set color = not unknown and toggle['result'] ? 'green' : 'red' %} + + {{ unknown ? '✗' : '' }}{{ toggle['feature'] }} + + {% endfor %} +
+
+ Features + {{ collector.features|length }} +
+
+ {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} +{% endblock %} + +{% block menu %} + + Feature Toggle + Feature toggle + +{% endblock %} + +{% block head %} + {{ parent() }} + +{% endblock %} + +{% block panel %} +

Feature toggles

+ {% if collector.toggles|length > 0 %} + + + + + + + + + {% for toggle in collector.toggles %} + {% set result = toggle['result'] ? 'grant' : 'deny' %} + + + + + {% endfor %} + +
FeatureStrategies
{{ toggle['feature'] }} + + {{ result|capitalize }} + {%- if collector.features[toggle['feature']] is not defined %} + (unknown) + {% elseif (toggle['computes']|first).result.name|lower is same as 'abstain' %} + (default) + {% endif %} + + {% for compute in toggle['computes'] %} +
+ {{ _self.indent(compute['level']) }} + {{ compute['result'].name }} + {{ compute['strategyId'] }} + {{ compute['strategyClass']|abbr_class }} +
+ {% endfor %} +
+ {% else %} +
+

No feature toggle checked

+
+ {% endif %} + +

Available features

+ {% if collector.features|length > 0 %} + + + + + + + + + + + {% for featureName, featureData in collector.features %} + + + + + + + {% endfor %} + +
NameDefaultDescriptionStrategy
{{ featureName }}{{ featureData.default|json_encode }}{{ featureData.description }}{{ profiler_dump(featureData.strategy) }}
+ {% else %} +
+

No features configured

+
+ {% endif %} +{% endblock %} + +{% macro indent(level) %} + {%- for i in 0..level -%} {% endfor %}L +{% endmacro %} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/CustomStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/CustomStrategy.php new file mode 100644 index 0000000000000..ec629025e990c --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/CustomStrategy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Strategy; + +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; + +final class CustomStrategy implements StrategyInterface +{ + public function __construct( + private readonly StrategyInterface $inner, + ) { + } + + public function compute(): StrategyResult + { + return $this->inner->compute(); + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php new file mode 100644 index 0000000000000..d42df6c82286f --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Strategy; + +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Component\HttpFoundation\RequestStack; + +final class RequestStackAttributeStrategy implements StrategyInterface +{ + public function __construct( + private readonly string $attributeName, + private readonly RequestStack|null $requestStack = null, + ) { + } + + public function compute(): StrategyResult + { + if (null === $this->requestStack) { + return StrategyResult::Abstain; + } + + $currentRequest = $this->requestStack->getCurrentRequest(); + + if (null === $currentRequest) { + return StrategyResult::Abstain; + } + + if ($currentRequest->attributes->has($this->attributeName) === false) { + return StrategyResult::Abstain; + } + + return $currentRequest->attributes->getBoolean($this->attributeName) ? StrategyResult::Grant : StrategyResult::Deny; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000000000..914e53a854313 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Tests\DependencyInjection; + +use Generator; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\Configuration; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Processor; + +/** + * @covers \Symfony\Bundle\FeatureToggleBundle\DependencyInjection\Configuration + * + * @uses \Symfony\Component\Config\Definition\Processor + * + * @phpstan-import-type ConfigurationType from Configuration + * @phpstan-import-type ConfigurationStrategy from Configuration + * @phpstan-import-type ConfigurationFeature from Configuration + */ +final class ConfigurationTest extends TestCase +{ + /** + * @return ConfigurationType + */ + public static function getBundleDefaultConfig(): array + { + return ['strategies' => [], 'features' => []]; + } + + public function testDefaultConfig(): void + { + $processor = new Processor(); + $config = $processor->processConfiguration( + new Configuration(), + [], + ); + + self::assertEquals(self::getBundleDefaultConfig(), $config); + } + + + public static function provideValidStrategyNameConfigurationTest(): Generator + { + yield 'simple name' => ['foobar']; + yield 'underscore name' => ['foo_bar']; + yield 'dashed name' => ['foo-bar']; + } + + /** + * @dataProvider provideValidStrategyNameConfigurationTest + */ + public function testValidStrategyNameConfiguration(string $strategyName): void + { + $processor = new Processor(); + $config = $processor->processConfiguration( + new Configuration(), + [ + [ + 'strategies' => [ + [ + 'name' => $strategyName, + 'type' => 'provider-type', + ], + ], + ], + ], + ); + + self::assertArrayHasKey($strategyName, $config['strategies']); + } + + public static function provideValidFeatureNameConfigurationTest(): Generator + { + yield 'simple name' => ['foobar']; + yield 'underscore name' => ['foo_bar']; + yield 'dashed name' => ['foo-bar']; + } + + /** + * @dataProvider provideValidFeatureNameConfigurationTest + */ + public function testValidFeatureNameConfiguration(string $featureName): void + { + $processor = new Processor(); + $config = $processor->processConfiguration( + new Configuration(), + [ + [ + 'features' => [ + [ + 'name' => $featureName, + 'description' => "This is the description of {$featureName}", + 'strategy' => 'fake-strategy', + 'default' => false, + ], + ], + ], + ], + ); + + self::assertArrayHasKey($featureName, $config['features']); + } + + public function testFeatureRequiresDescriptionKey(): void + { + self::expectException(InvalidConfigurationException::class); + self::expectExceptionMessage('The child config "default" under "feature_toggle.features.some-feature" must be configured: Will be used as a fallback mechanism if the strategy return StrategyResult::Abstain.'); + + $processor = new Processor(); + $processor->processConfiguration( + new Configuration(), + [ + [ + 'features' => [ + [ + 'name' => 'some-feature', + 'strategy' => 'fake-strategy', + ], + ], + ], + ], + ); + } + + public function testFeatureRequiresStrategyKey(): void + { + self::expectException(InvalidConfigurationException::class); + self::expectExceptionMessage('The child config "strategy" under "feature_toggle.features.some-feature" must be configured: Strategy to be used for this feature. Can be one of "feature_toggle.strategies[].name" or a valid service id that implements StrategyInterface::class.'); + + $processor = new Processor(); + $processor->processConfiguration( + new Configuration(), + [ + [ + 'features' => [ + [ + 'name' => 'some-feature', + 'default' => false, + ], + ], + ], + ], + ); + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php new file mode 100644 index 0000000000000..0ce016c9e49a0 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\Configuration; +use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\FeatureToggleExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use function array_column; +use function in_array; + +/** + * @covers \Symfony\Bundle\FeatureToggleBundle\DependencyInjection\FeatureToggleExtension + * + * @uses \Symfony\Component\DependencyInjection\ContainerBuilder + * @uses \Symfony\Component\DependencyInjection\ParameterBag\ParameterBag + * + * @phpstan-import-type ConfigurationType from Configuration + */ +final class FeatureToggleExtensionTest extends TestCase +{ + /** + * @return list + */ + public function getConfig(): array + { + return [ + [ + 'strategies' => [ + [ + 'name' => 'date.feature-strategy', + 'type' => 'date', + 'with' => ['from' => '-2 days'], + ], + [ + 'name' => 'env.feature-strategy', + 'type' => 'env', + 'with' => ['name' => 'SOME_ENV'], + ], + [ + 'name' => 'native_request_header.feature-strategy', + 'type' => 'native_request_header', + 'with' => ['name' => 'SOME-HEADER-NAME'], + ], + [ + 'name' => 'native_request_query.feature-strategy', + 'type' => 'native_request_query', + 'with' => ['name' => 'some_query_parameter'], + ], + [ + 'name' => 'request_attribute.feature-strategy', + 'type' => 'request_attribute', + 'with' => ['name' => 'some_request_attribute'], + ], + [ + 'name' => 'priority.feature-strategy', + 'type' => 'priority', + 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], + ], + [ + 'name' => 'affirmative.feature-strategy', + 'type' => 'affirmative', + 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], + ], + [ + 'name' => 'not.feature-strategy', + 'type' => 'not', + 'with' => ['strategy' => 'grant.feature-strategy'], + ], + [ + 'name' => 'grant.feature-strategy', + 'type' => 'grant', + ], + [ + 'name' => 'deny.feature-strategy', + 'type' => 'deny', + ], + ], + 'features' => [ + ], + ], + ]; + } + + public function getContainerBuilder(): ContainerBuilder + { + return new ContainerBuilder(new ParameterBag(['kernel.debug' => true])); + } + + public function testStrategiesAreDefinedAsServicesAndTagged(): void + { + $extension = new FeatureToggleExtension(); + + $containerBuilder = $this->getContainerBuilder(); + $extension->load($this->getConfig(), $containerBuilder); + + $expectedServiceIds = array_column($this->getConfig()[0]['strategies'], 'name', null); + $serviceIds = $containerBuilder->getServiceIds(); + + foreach ($expectedServiceIds as $expectedServiceId) { + self::assertContains($expectedServiceId, $serviceIds); + if (in_array($expectedServiceId, $serviceIds, true)) { + $serviceDefinition = $containerBuilder->getDefinition($expectedServiceId); + + self::assertTrue( + $serviceDefinition->hasTag('feature_toggle.feature_strategy'), + "'{$expectedServiceId}' does not have the tag.", + ); + + $tagConfigs = $serviceDefinition->getTag('feature_toggle.feature_strategy'); + self::assertCount(1, $tagConfigs); + } + } + } + + public function testAutoconfigurationForInterfaces(): void + { + $extension = new FeatureToggleExtension(); + + $containerBuilder = $this->getContainerBuilder(); + $extension->load($this->getConfig(), $containerBuilder); + + $registeredForAutoconfiguration = $containerBuilder->getAutoconfiguredInstanceof(); + + self::assertArrayHasKey( + StrategyInterface::class, + $registeredForAutoconfiguration, + ); + $node = $registeredForAutoconfiguration[StrategyInterface::class]; + $tags = $node->getTags(); + self::assertArrayHasKey('feature_toggle.feature_strategy', $tags); + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php new file mode 100644 index 0000000000000..e332fd74e4c01 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Twig; + +use Symfony\Component\FeatureToggle\FeatureCheckerInterface; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +final class FeatureEnabledExtension extends AbstractExtension +{ + public function __construct( + private readonly FeatureCheckerInterface $featureEnabledChecker, + ) { + } + + public function getFunctions(): array + { + return [ + new TwigFunction('isFeatureEnabled', $this->featureEnabledChecker->isEnabled(...)), + ]; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/composer.json b/src/Symfony/Bundle/FeatureToggleBundle/composer.json new file mode 100644 index 0000000000000..4b8d305746236 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/feature-toggle-bundle", + "type": "symfony-bundle", + "description": "Provides a tight integration of the Symfony FeatureToggle component into the Symfony full-stack framework", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/contracts": "^3.3", + "symfony/config": "^6.3", + "symfony/dependency-injection": "^6.3", + "symfony/options-resolver": "^6.3", + "symfony/http-kernel": "^6.3" + }, + "require-dev": { + "symfony/expression-language": "^6.3", + "symfony/http-foundation": "^6.3", + "symfony/twig-bridge": "^6.3" + }, + "autoload": { + "psr-4": { "Symfony\\Bundle\\FeatureToggle\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/phpunit.xml.dist b/src/Symfony/Bundle/FeatureToggleBundle/phpunit.xml.dist new file mode 100644 index 0000000000000..ecb615655d4ac --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/FeatureToggle/.gitattributes b/src/Symfony/Component/FeatureToggle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/FeatureToggle/.gitignore b/src/Symfony/Component/FeatureToggle/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php b/src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php new file mode 100644 index 0000000000000..da33190a60da2 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Debug; + +use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; +use Symfony\Component\FeatureToggle\FeatureCheckerInterface; + +final class TraceableFeatureChecker implements FeatureCheckerInterface +{ + public function __construct( + private readonly FeatureCheckerInterface $featureChecker, + private readonly FeatureCheckerDataCollector $dataCollector, + ) { + } + + public function isEnabled(string $featureName): bool + { + $this->dataCollector->collectIsEnabledStart($featureName); + + $result = $this->featureChecker->isEnabled($featureName); + + $this->dataCollector->collectIsEnabledStop($result); + + return $result; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php b/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php new file mode 100644 index 0000000000000..7bb2eed32c727 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Debug; + +use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; +use Symfony\Component\FeatureToggle\Strategy\OuterStrategyInterface; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; + +final class TraceableStrategy implements StrategyInterface, OuterStrategyInterface +{ + public function __construct( + private readonly StrategyInterface $strategy, + private readonly string $strategyId, + private readonly FeatureCheckerDataCollector $dataCollector, + ) { + } + + public function compute(): StrategyResult + { + $this->dataCollector->collectComputeStart($this->strategyId, $this->strategy::class); + + $result = $this->strategy->compute(); + + $this->dataCollector->collectComputeStop($result); + + return $result; + } + + public function getInnerStrategy(): StrategyInterface + { + return $this->strategy; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Feature.php b/src/Symfony/Component/FeatureToggle/Feature.php new file mode 100644 index 0000000000000..76ed5f435d9a3 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Feature.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle; + +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; + +final class Feature +{ + public function __construct( + private readonly string $name, + private readonly string $description, + private readonly bool $default, + private readonly StrategyInterface $strategy, + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isEnabled(): bool + { + return $this->strategy->compute()->isEnabled($this->default); + } +} diff --git a/src/Symfony/Component/FeatureToggle/FeatureChecker.php b/src/Symfony/Component/FeatureToggle/FeatureChecker.php new file mode 100644 index 0000000000000..087337c0eebf8 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/FeatureChecker.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle; + +final class FeatureChecker implements FeatureCheckerInterface +{ + public function __construct( + private readonly FeatureCollection $features, + private readonly bool $whenNotFound, + ) { + } + + public function isEnabled(string $featureName): bool + { + if (!$this->features->has($featureName)) { + return $this->whenNotFound; + } + + return $this->features->get($featureName)->isEnabled(); + } +} diff --git a/src/Symfony/Component/FeatureToggle/FeatureCheckerInterface.php b/src/Symfony/Component/FeatureToggle/FeatureCheckerInterface.php new file mode 100644 index 0000000000000..aa92b39bf327e --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/FeatureCheckerInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle; + +interface FeatureCheckerInterface +{ + public function isEnabled(string $featureName): bool; +} diff --git a/src/Symfony/Component/FeatureToggle/FeatureCollection.php b/src/Symfony/Component/FeatureToggle/FeatureCollection.php new file mode 100644 index 0000000000000..68851495e31c1 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/FeatureCollection.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle; + +use ArrayIterator; +use Closure; +use IteratorAggregate; +use Psr\Container\ContainerInterface; +use function array_values; +use function is_callable; + +/** @implements IteratorAggregate */ +final class FeatureCollection implements ContainerInterface, IteratorAggregate +{ + /** @var array|null */ + private array|null $features = null; + + /** @var array|(Closure(): iterable)> */ + private array $featureProviders = []; + + /** + * @param iterable $features + */ + public function __construct(iterable $features) + { + $this->append($features); + } + + /** + * @param iterable|(Closure(): iterable) $features + */ + private function append(iterable|Closure $features): void + { + $this->featureProviders[] = $features; + $this->features = null; + } + + /** + * @param iterable|(Closure(): iterable) $features + */ + public function withFeatures(iterable|Closure $features): self + { + $this->append($features); + + return $this; + } + + /** + * @phpstan-assert-if-true null $this->features + */ + private function compile(): void + { + if (null !== $this->features) { + return; + } + + $this->features = []; + + foreach ($this->featureProviders as $featureProvider) { + if (is_callable($featureProvider)) { + $featureProvider = $featureProvider(); + } + + foreach ($featureProvider as $feature) { + $this->features[$feature->getName()] = $feature; + } + } + } + + public function has(string $id): bool + { + $this->compile(); + return array_key_exists($id, $this->features); + } + + /** + * @throws FeatureNotFoundException If the feature is not registered in this provider. + */ + public function get(string $id): Feature + { + $this->compile(); + return $this->features[$id] ?? throw new FeatureNotFoundException($id); + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + $this->compile(); + + return new ArrayIterator(array_values($this->features)); + } +} diff --git a/src/Symfony/Component/FeatureToggle/FeatureNotFoundException.php b/src/Symfony/Component/FeatureToggle/FeatureNotFoundException.php new file mode 100644 index 0000000000000..0f7391a7f8ab7 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/FeatureNotFoundException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle; + +use Psr\Container\NotFoundExceptionInterface; + +class FeatureNotFoundException extends \Exception implements NotFoundExceptionInterface +{ + public function __construct(string $id) + { + parent::__construct("Feature \"$id\" not found."); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php b/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php new file mode 100644 index 0000000000000..dc6d385ccb2a2 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Provider; + +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\FeatureCollection; + +final class InMemoryProvider implements ProviderInterface +{ + /** + * @param list $features + */ + public function __construct( + private readonly array $features, + ) { + } + + public function provide(): FeatureCollection + { + return new FeatureCollection($this->features); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php b/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php new file mode 100644 index 0000000000000..67ad172b10d87 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Provider; + +use Symfony\Component\FeatureToggle\FeatureCollection; + +interface ProviderInterface +{ + public function provide(): FeatureCollection; +} diff --git a/src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php b/src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php new file mode 100644 index 0000000000000..cf31d2549caab --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Provider; + +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\FeatureCollection; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; + +final class RandomProvider implements ProviderInterface +{ + public function __construct( + private readonly StrategyInterface $defaultStrategy, + ) { + } + + public function provide(): FeatureCollection + { + $features = []; + for ($i = 1; $i <= random_int(2, 10); $i++) { + $features[] = new Feature( + name: "random-feature-$i", + description: "Random feature #$i", + default: (bool) random_int(0, 1), + strategy: $this->defaultStrategy, + ); + } + + return new FeatureCollection($features); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php new file mode 100644 index 0000000000000..c6a6407dac070 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class AffirmativeStrategy implements OuterStrategiesInterface +{ + /** + * @param iterable $strategies + */ + public function __construct( + private readonly iterable $strategies, + ) { + } + + public function compute(): StrategyResult + { + $result = StrategyResult::Abstain; + foreach ($this->strategies as $strategy) { + $innerResult = $strategy->compute(); + + if (StrategyResult::Grant === $innerResult) { + return StrategyResult::Grant; + } + + if (StrategyResult::Deny === $innerResult) { + $result = StrategyResult::Deny; + } + } + + return $result; + } + + public function getInnerStrategies(): iterable + { + return $this->strategies; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php new file mode 100644 index 0000000000000..dec0c921bd6cf --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; +use InvalidArgumentException; +use Psr\Clock\ClockInterface; + +final class DateStrategy implements StrategyInterface +{ + public function __construct( + private readonly ClockInterface $clock, + private readonly \DateTimeImmutable|null $from = null, + private readonly \DateTimeImmutable|null $until = null, + private readonly bool $includeFrom = true, + private readonly bool $includeUntil = true, + ) { + if (null === $this->from && null === $this->until) { + throw new InvalidArgumentException('Either from or until must be provided.'); + } + } + + public function compute(): StrategyResult + { + $now = $this->clock->now(); + + if (null !== $this->from) { + if ($this->includeFrom && $this->from > $now) { + return StrategyResult::Deny; + } + + if (!$this->includeFrom && $this->from >= $now) { + return StrategyResult::Deny; + } + } + + if (null !== $this->until) { + if ($this->includeUntil && $this->until < $now) { + return StrategyResult::Deny; + } + + if (!$this->includeUntil && $this->until <= $now) { + return StrategyResult::Deny; + } + } + + return StrategyResult::Grant; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/DenyStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/DenyStrategy.php new file mode 100644 index 0000000000000..6464a08759c79 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/DenyStrategy.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class DenyStrategy implements StrategyInterface +{ + public function compute(): StrategyResult + { + return StrategyResult::Deny; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php new file mode 100644 index 0000000000000..746f7c5358532 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; +use function filter_var; +use const FILTER_VALIDATE_BOOL; + +final class EnvStrategy implements StrategyInterface +{ + public function __construct( + private readonly string $envName, + ) { + } + + public function compute(): StrategyResult + { + $value = getenv($this->envName); + + if (false === $value) { + return StrategyResult::Abstain; + } + + return filter_var($value, FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/GrantStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/GrantStrategy.php new file mode 100644 index 0000000000000..b0d633107cfdf --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/GrantStrategy.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class GrantStrategy implements StrategyInterface +{ + public function compute(): StrategyResult + { + return StrategyResult::Grant; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php new file mode 100644 index 0000000000000..a6aafc3062f34 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class NotStrategy implements OuterStrategyInterface +{ + public function __construct( + private readonly StrategyInterface $inner, + ) { + } + + public function compute(): StrategyResult + { + $innerResult = $this->inner->compute(); + + return match ($innerResult) { + StrategyResult::Abstain => StrategyResult::Abstain, + StrategyResult::Grant => StrategyResult::Deny, + StrategyResult::Deny => StrategyResult::Grant, + }; + } + + public function getInnerStrategy(): StrategyInterface + { + return $this->inner; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php b/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php new file mode 100644 index 0000000000000..4c899c89baded --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +interface OuterStrategiesInterface extends StrategyInterface +{ + /** + * @return iterable + */ + public function getInnerStrategies(): iterable; +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php b/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php new file mode 100644 index 0000000000000..c3ef440e92cec --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +interface OuterStrategyInterface extends StrategyInterface +{ + public function getInnerStrategy(): StrategyInterface; +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php new file mode 100644 index 0000000000000..536f7e00ca141 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class PriorityStrategy implements OuterStrategiesInterface +{ + /** + * @param iterable $strategies + */ + public function __construct( + private readonly iterable $strategies, + ) { + } + + public function compute(): StrategyResult + { + $result = StrategyResult::Abstain; + foreach ($this->strategies as $strategy) { + $innerResult = $strategy->compute(); + + if (StrategyResult::Abstain !== $innerResult) { + return $innerResult; + } + } + + return $result; + } + + public function getInnerStrategies(): iterable + { + return $this->strategies; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php new file mode 100644 index 0000000000000..017cf3c41a363 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class RandomStrategy implements StrategyInterface +{ + public function compute(): StrategyResult + { + return match(random_int(0, 2)) { + 0 => StrategyResult::Grant, + 1 => StrategyResult::Abstain, + 2 => StrategyResult::Deny, + }; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php new file mode 100644 index 0000000000000..8fccceaf1e5f1 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; +use function filter_var; +use const FILTER_VALIDATE_BOOL; + +// TODO: make it case insensitive ? +final class RequestHeaderStrategy implements StrategyInterface +{ + public function __construct( + private readonly string $headerName, + ) { + } + + public function compute(): StrategyResult + { + if (!array_key_exists('HTTP_'.$this->headerName, $_SERVER)) { + return StrategyResult::Abstain; + } + + return filter_var($_SERVER['HTTP_'.$this->headerName], FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php new file mode 100644 index 0000000000000..7f26f24db9f70 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; +use function filter_var; +use const FILTER_VALIDATE_BOOL; + +// TODO: make it case insensitive ? +final class RequestQueryStrategy implements StrategyInterface +{ + public function __construct( + private readonly string $queryParameterName, + ) { + } + + public function compute(): StrategyResult + { + if (!array_key_exists($this->queryParameterName, $_GET)) { + return StrategyResult::Abstain; + } + + return filter_var($_GET[$this->queryParameterName], FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/StrategyInterface.php b/src/Symfony/Component/FeatureToggle/Strategy/StrategyInterface.php new file mode 100644 index 0000000000000..66ded40097400 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/StrategyInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +interface StrategyInterface +{ + public function compute(): StrategyResult; +} diff --git a/src/Symfony/Component/FeatureToggle/StrategyResult.php b/src/Symfony/Component/FeatureToggle/StrategyResult.php new file mode 100644 index 0000000000000..0d76f380808b2 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/StrategyResult.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle; + +enum StrategyResult +{ + case Grant; + case Deny; + case Abstain; + + public function isEnabled(bool $fallback): bool + { + return match($this) { + self::Grant => true, + self::Deny => false, + self::Abstain => $fallback, + }; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php new file mode 100644 index 0000000000000..7576985ec7813 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\FeatureChecker; +use Symfony\Component\FeatureToggle\FeatureCollection; +use Symfony\Component\FeatureToggle\Strategy\DenyStrategy; +use Symfony\Component\FeatureToggle\Strategy\GrantStrategy; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; + +/** + * @covers \Symfony\Component\FeatureToggle\FeatureChecker + * + * @uses \Symfony\Component\FeatureToggle\FeatureCollection + * @uses \Symfony\Component\FeatureToggle\Feature + * @uses \Symfony\Component\FeatureToggle\Strategy\GrantStrategy + * @uses \Symfony\Component\FeatureToggle\Strategy\DenyStrategy + */ +final class FeatureCheckerTest extends TestCase +{ + public function testItCorrectlyCheckTheFeaturesEvenIfNotFound(): void + { + $featureChecker = new FeatureChecker( + new FeatureCollection([]), + true + ); + + self::assertTrue($featureChecker->isEnabled('not-found-1')); + + $featureChecker = new FeatureChecker( + new FeatureCollection([ + new Feature( + name: 'fake-1', + description: 'Fake description 1', + default: true, + strategy: new GrantStrategy() + ), + new Feature( + name: 'fake-2', + description: 'Fake description 2', + default: true, + strategy: new DenyStrategy() + ), + new Feature( + name: 'fake-3', + description: 'Fake description 3', + default: false, + strategy: new class implements StrategyInterface { + public function compute(): StrategyResult + { + return StrategyResult::Abstain; + } + } + ), + ]), + false + ); + + self::assertFalse($featureChecker->isEnabled('not-found-1')); + self::assertTrue($featureChecker->isEnabled('fake-1')); + self::assertFalse($featureChecker->isEnabled('fake-2')); + self::assertFalse($featureChecker->isEnabled('fake-3')); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php new file mode 100644 index 0000000000000..b4ce1a55c5013 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests; + +use Generator; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\FeatureCollection; +use Symfony\Component\FeatureToggle\FeatureNotFoundException; +use Symfony\Component\FeatureToggle\Strategy\GrantStrategy; +use function is_a; + +/** + * @covers \Symfony\Component\FeatureToggle\FeatureCollection + * + * @uses \Symfony\Component\FeatureToggle\Feature + * @uses \Symfony\Component\FeatureToggle\Strategy\GrantStrategy + */ +final class FeatureCollectionTest extends TestCase +{ + public function testEnsureItIsIterable(): void + { + $featureCollection = new FeatureCollection([ + new Feature( + name: 'fake-1', + description: 'Fake description 1', + default: true, + strategy: new GrantStrategy() + ), + new Feature( + name: 'fake-2', + description: 'Fake description 2', + default: true, + strategy: new GrantStrategy() + ), + ]); + + self::assertIsIterable($featureCollection); + self::assertCount(2, $featureCollection); + } + + public function testEnsureItImplementsContainerInterface(): void + { + self::assertTrue(is_a(FeatureCollection::class, ContainerInterface::class, true)); + } + + public function testEnsureItIsMergeableWithDifferentTypesOfIterable(): void + { + $featureCollection = new FeatureCollection([ + new Feature( + name: 'fake-1', + description: 'Fake description 1', + default: true, + strategy: new GrantStrategy() + ), + new Feature( + name: 'fake-2', + description: 'Fake description 2', + default: true, + strategy: new GrantStrategy() + ), + ]); + + $featureCollection->withFeatures(function (): Generator { + yield new Feature( + name: 'fake-3', + description: 'Fake description 3', + default: true, + strategy: new GrantStrategy() + ); + }); + + self::assertCount(3, $featureCollection); + + $featureCollection->withFeatures([new Feature( + name: 'fake-4', + description: 'Fake description 4', + default: true, + strategy: new GrantStrategy() + )]); + + self::assertCount(4, $featureCollection); + } + + public function testItCanFindTheFeature(): void + { + $featureFake1 = new Feature( + name: 'fake-1', + description: 'Fake description 1', + default: true, + strategy: new GrantStrategy() + ); + + $featureFake2 = new Feature( + name: 'fake-2', + description: 'Fake description 2', + default: true, + strategy: new GrantStrategy() + ); + + $featureCollection = new FeatureCollection([$featureFake1, $featureFake2]); + + self::assertTrue($featureCollection->has('fake-1')); + self::assertSame($featureFake1, $featureCollection->get('fake-1')); + + self::assertTrue($featureCollection->has('fake-2')); + self::assertSame($featureFake2, $featureCollection->get('fake-2')); + } + + public function testItThrowsWhenFeatureNotFound(): void + { + $featureCollection = new FeatureCollection([]); + + self::assertFalse($featureCollection->has('not-found-1')); + + self::expectException(FeatureNotFoundException::class); + $featureCollection->get('not-found-1'); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php new file mode 100644 index 0000000000000..f388bd2eed6f0 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests; + +use Generator; +use PHPUnit\Framework\TestCase; +use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; + +/** + * @covers \Symfony\Component\FeatureToggle\Feature + * + * @uses \Symfony\Component\FeatureToggle\StrategyResult + */ +final class FeatureTest extends TestCase +{ + public function testItCanBeInstantiated(): void + { + new Feature( + name: 'fake', + description: 'Fake description', + default: false, + strategy: new class implements StrategyInterface { + public function compute(): StrategyResult + { + return StrategyResult::Abstain; + } + } + ); + + self::addToAssertionCount(1); + } + + public static function generateValidStrategy(): Generator + { + // Grant + yield "grant and default 'true'" => [ + StrategyResult::Grant, + true, + true, + ]; + + yield "grant and default 'false'" => [ + StrategyResult::Grant, + false, + true, + ]; + + // Deny + yield "deny and default 'true'" => [ + StrategyResult::Deny, + true, + false, + ]; + + yield "deny and default 'false'" => [ + StrategyResult::Deny, + false, + false, + ]; + + // Abstain + yield "abstain and default 'true'" => [ + StrategyResult::Abstain, + true, + true, + ]; + + yield "abstain and default 'false'" => [ + StrategyResult::Abstain, + false, + false, + ]; + } + + /** + * @dataProvider generateValidStrategy + */ + public function testItCorrectlyComputeAndHandlesDefault(StrategyResult $strategyResult, bool $default, bool $expectedResult): void + { + $strategy = self::createMock(StrategyInterface::class); + + $strategy + ->expects(self::once()) + ->method('compute') + ->willReturn($strategyResult) + ; + + $feature = new Feature( + name: 'fake', + description: 'Fake description', + default: $default, + strategy: $strategy + ); + + self::assertSame($expectedResult, $feature->isEnabled()); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AbstractOuterStrategiesTestCase.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AbstractOuterStrategiesTestCase.php new file mode 100644 index 0000000000000..3bb04a9e28897 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AbstractOuterStrategiesTestCase.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests\Strategy; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; + +abstract class AbstractOuterStrategiesTestCase extends TestCase +{ + protected static function generateStrategy(StrategyResult $strategyResult): StrategyInterface + { + return new class($strategyResult) implements StrategyInterface { + public function __construct(private StrategyResult $result) + { + } + + public function compute(): StrategyResult + { + return $this->result; + } + }; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php new file mode 100644 index 0000000000000..28c927342d63f --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests\Strategy; + +use Generator; +use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; +use Symfony\Component\FeatureToggle\Strategy\OuterStrategiesInterface; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; +use function is_a; + +/** + * @covers \Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy + */ +final class AffirmativeStrategyTest extends AbstractOuterStrategiesTestCase +{ + public function testEnsureItExposesInnerStrategies(): void + { + self::assertTrue(is_a(AffirmativeStrategy::class, OuterStrategiesInterface::class, true)); + } + + public static function generatesValidStrategies(): Generator + { + yield 'no strategies' => [ + [], + StrategyResult::Abstain, + ]; + + yield 'if all abstain' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + ], + StrategyResult::Abstain, + ]; + + yield 'if one denies after only abstain results' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Deny), + ], + StrategyResult::Deny, + ]; + + yield 'if one grants after only abstain results' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Grant), + ], + StrategyResult::Grant, + ]; + + yield 'if one grants after at least one Deny' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Deny), + self::generateStrategy(StrategyResult::Grant), + ], + StrategyResult::Grant, + ]; + + yield 'if one denies after at least one grant' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Grant), + self::generateStrategy(StrategyResult::Deny), + ], + StrategyResult::Grant, + ]; + } + + /** + * @dataProvider generatesValidStrategies + * + * @param iterable $strategies + */ + public function testItComputesCorrectly(iterable $strategies, StrategyResult $expected): void + { + $affirmativeStrategy = new AffirmativeStrategy($strategies); + + self::assertSame($expected, $affirmativeStrategy->compute()); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php new file mode 100644 index 0000000000000..12d6491208230 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests\Strategy; + +use DateTimeImmutable; +use Generator; +use InvalidArgumentException; +use PHPUnit\Framework\TestCase; +use Psr\Clock\ClockInterface; +use Symfony\Component\FeatureToggle\Strategy\DateStrategy; +use Symfony\Component\FeatureToggle\StrategyResult; + +/** + * @covers \Symfony\Component\FeatureToggle\Strategy\DateStrategy + */ +final class DateStrategyTest extends TestCase +{ + private static ClockInterface $nowClock; + + private static function generateClock(DateTimeImmutable|null $now = null): ClockInterface + { + $now = $now ?? DateTimeImmutable::createFromFormat('!d/m/Y', (new DateTimeImmutable())->format('d/m/Y')); + + return new class($now) implements ClockInterface { + public function __construct(private DateTimeImmutable $now) + { + } + + public function now(): DateTimeImmutable + { + return $this->now; + } + }; + } + + public static function setUpBeforeClass(): void + { + self::$nowClock = self::generateClock(); + } + + public function testItRequiresAtLeastOneDate(): void + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('Either from or until must be provided.'); + + new DateStrategy(self::generateClock()); + } + + public static function generateValidDates(): Generator + { + $now = self::generateClock()->now(); + + // from + no until + yield '[-2 days;∞' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now->modify('-2 days'), + 'until' => null, + 'includeFrom' => true, + 'includeUntil' => false, + ]; + + yield ']-2 days;∞' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now->modify('-2 days'), + 'until' => null, + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield '[now;∞' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now, + 'until' => null, + 'includeFrom' => true, + 'includeUntil' => false, + ]; + + yield ']now;∞' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now, + 'until' => null, + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield '[+2 days;∞' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('+2 days'), + 'until' => null, + 'includeFrom' => true, + 'includeUntil' => false, + ]; + + yield ']+2 days;∞' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('+2 days'), + 'until' => null, + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + // no from + until + yield '∞;-2 days]' => [ + 'expected' => StrategyResult::Deny, + 'from' => null, + 'until' => $now->modify('-2 days'), + 'includeFrom' => false, + 'includeUntil' => true, + ]; + + yield '∞;-2 days[' => [ + 'expected' => StrategyResult::Deny, + 'from' => null, + 'until' => $now->modify('-2 days'), + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield '∞;now]' => [ + 'expected' => StrategyResult::Grant, + 'from' => null, + 'until' => $now, + 'includeFrom' => false, + 'includeUntil' => true, + ]; + + yield '∞;now[' => [ + 'expected' => StrategyResult::Deny, + 'from' => null, + 'until' => $now, + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield '∞;+2 days]' => [ + 'expected' => StrategyResult::Grant, + 'from' => null, + 'until' => $now->modify('+2 days'), + 'includeFrom' => false, + 'includeUntil' => true, + ]; + + yield '∞;+2 days[' => [ + 'expected' => StrategyResult::Grant, + 'from' => null, + 'until' => $now->modify('+2 days'), + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + // from + until + yield '[-2 days;-1 days]' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('-1 days'), + 'includeFrom' => true, + 'includeUntil' => true, + ]; + + yield '[-2 days;-1 days[' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('-1 days'), + 'includeFrom' => true, + 'includeUntil' => false, + ]; + + yield ']-2 days;-1 days[' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('-1 days'), + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield ']-2 days;-1 days]' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('-1 days'), + 'includeFrom' => false, + 'includeUntil' => true, + ]; + + yield '[-2 days;+3 days]' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('+3 days'), + 'includeFrom' => true, + 'includeUntil' => true, + ]; + + yield '[-2 days;+3 days[' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('+3 days'), + 'includeFrom' => true, + 'includeUntil' => false, + ]; + + yield ']-2 days;+3 days[' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('+3 days'), + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield ']-2 days;+3 days]' => [ + 'expected' => StrategyResult::Grant, + 'from' => $now->modify('-2 days'), + 'until' => $now->modify('+3 days'), + 'includeFrom' => false, + 'includeUntil' => true, + ]; + + yield '[+1 days;+2 days]' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('+1 days'), + 'until' => $now->modify('+2 days'), + 'includeFrom' => true, + 'includeUntil' => true, + ]; + + yield '[+1 days;+2 days[' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('+1 days'), + 'until' => $now->modify('+2 days'), + 'includeFrom' => true, + 'includeUntil' => false, + ]; + + yield ']+1 days;+2 days[' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('+1 days'), + 'until' => $now->modify('+2 days'), + 'includeFrom' => false, + 'includeUntil' => false, + ]; + + yield ']+1 days;+2 days]' => [ + 'expected' => StrategyResult::Deny, + 'from' => $now->modify('+1 days'), + 'until' => $now->modify('+2 days'), + 'includeFrom' => false, + 'includeUntil' => true, + ]; + } + + /** + * @dataProvider generateValidDates + */ + public function testItComputeDatesCorrectly( + StrategyResult $expected, + \DateTimeImmutable|null $from = null, + \DateTimeImmutable|null $until = null, + bool $includeFrom = true, + bool $includeUntil = true, + ): void { + $dateStrategy = new DateStrategy(self::$nowClock, $from, $until, $includeFrom, $includeUntil); + + self::assertSame($expected, $dateStrategy->compute()); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php new file mode 100644 index 0000000000000..305e591e1e5df --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests\Strategy; + +use Generator; +use Symfony\Component\FeatureToggle\Strategy\OuterStrategiesInterface; +use Symfony\Component\FeatureToggle\Strategy\PriorityStrategy; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; +use function is_a; + +/** + * @covers \Symfony\Component\FeatureToggle\Strategy\PriorityStrategy + */ +final class PriorityStrategyTest extends AbstractOuterStrategiesTestCase +{ + public function testEnsureItExposesInnerStrategies(): void + { + self::assertTrue(is_a(PriorityStrategy::class, OuterStrategiesInterface::class, true)); + } + + public static function generatesValidStrategies(): Generator + { + yield 'no strategies' => [ + [], + StrategyResult::Abstain, + ]; + + yield 'if all abstain' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + ], + StrategyResult::Abstain, + ]; + + yield 'if one denies after only abstain results' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Deny), + ], + StrategyResult::Deny, + ]; + + yield 'if one grants after only abstain results' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Grant), + ], + StrategyResult::Grant, + ]; + + yield 'if one grants after at least one Deny' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Deny), + self::generateStrategy(StrategyResult::Grant), + ], + StrategyResult::Deny, + ]; + + yield 'if one denies after at least one grant' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Grant), + self::generateStrategy(StrategyResult::Deny), + ], + StrategyResult::Grant, + ]; + } + + /** + * @dataProvider generatesValidStrategies + * + * @param iterable $strategies + */ + public function testItComputesCorrectly(iterable $strategies, StrategyResult $expected): void + { + $affirmativeStrategy = new PriorityStrategy($strategies); + + self::assertSame($expected, $affirmativeStrategy->compute()); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php b/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php new file mode 100644 index 0000000000000..fbcaf90b63189 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests; + +use Generator; +use PHPUnit\Framework\TestCase; +use Symfony\Component\FeatureToggle\StrategyResult; +use function constant; + +/** + * @covers \Symfony\Component\FeatureToggle\StrategyResult + */ +final class StrategyResultTest extends TestCase +{ + public static function generateValidUseCases(): Generator + { + // Grant + yield "grant should ignore fallback #1" => [ + StrategyResult::Grant->name, + true, + true, + ]; + + yield "grant should ignore fallback #2" => [ + StrategyResult::Grant->name, + false, + true, + ]; + + // Deny + yield "deny should ignore fallback #1" => [ + StrategyResult::Deny->name, + true, + false, + ]; + + yield "deny should ignore fallback #2" => [ + StrategyResult::Deny->name, + false, + false, + ]; + + // Abstain + yield "abstain should use fallback #1" => [ + StrategyResult::Abstain->name, + true, + true, + ]; + + yield "abstain should use fallback #2" => [ + StrategyResult::Abstain->name, + false, + false, + ]; + } + + /** + * @dataProvider generateValidUseCases + */ + public function testItCorrectlyMatchesToBool(string $result, bool $fallback, bool $expectedResult): void + { + /** @var StrategyResult $strategyResult */ + $strategyResult = constant(StrategyResult::class.'::'.$result); + + self::assertSame($expectedResult, $strategyResult->isEnabled($fallback)); + } +} diff --git a/src/Symfony/Component/FeatureToggle/composer.json b/src/Symfony/Component/FeatureToggle/composer.json new file mode 100644 index 0000000000000..ccb3abb27e944 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/composer.json @@ -0,0 +1,32 @@ +{ + "name": "symfony/feature-toggle", + "type": "library", + "description": "Provide feature flags mechanism.", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "psr/container": "^2.0" + }, + "require-dev": { + "psr/clock": "^1.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\FeatureToggle\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/FeatureToggle/phpunit.xml.dist b/src/Symfony/Component/FeatureToggle/phpunit.xml.dist new file mode 100644 index 0000000000000..ecb615655d4ac --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Tests + ./vendor + + + From 48819ec01cd9eaaf866893eaf5cf008861ef3f40 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Sun, 30 Jul 2023 17:29:29 +0200 Subject: [PATCH 02/25] [UPDATE] Fix twig function must be snake_case + gitattributes for bundle --- src/Symfony/Bundle/FeatureToggleBundle/.gitattributes | 4 ++++ src/Symfony/Bundle/FeatureToggleBundle/.gitignore | 3 +++ .../FeatureToggleBundle/Twig/FeatureEnabledExtension.php | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/.gitattributes create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/.gitignore diff --git a/src/Symfony/Bundle/FeatureToggleBundle/.gitattributes b/src/Symfony/Bundle/FeatureToggleBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/FeatureToggleBundle/.gitignore b/src/Symfony/Bundle/FeatureToggleBundle/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php index e332fd74e4c01..7b59b20005f4e 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Twig/FeatureEnabledExtension.php @@ -25,7 +25,7 @@ public function __construct( public function getFunctions(): array { return [ - new TwigFunction('isFeatureEnabled', $this->featureEnabledChecker->isEnabled(...)), + new TwigFunction('is_feature_enabled', $this->featureEnabledChecker->isEnabled(...)), ]; } } From 2f875ea27b3038fe16b2a0557ee578757e7b011a Mon Sep 17 00:00:00 2001 From: Hubert Lenoir Date: Thu, 7 Sep 2023 17:48:29 +0200 Subject: [PATCH 03/25] cleanup before RFC (#1) * cleanup before RFC * Update Configuration.php * Update README.md --------- Co-authored-by: Adrien Roches --- .../FeatureCheckerDataCollector.php | 5 +- .../CompilerPass/FeatureCollectionPass.php | 5 +- .../DependencyInjection/Configuration.php | 18 ++--- .../FeatureToggleExtension.php | 6 +- .../Bundle/FeatureToggleBundle/LICENSE | 19 +++++ .../Bundle/FeatureToggleBundle/README.md | 18 +++++ .../DependencyInjection/ConfigurationTest.php | 5 +- .../FeatureToggleExtensionTest.php | 4 +- .../Bundle/FeatureToggleBundle/composer.json | 3 +- .../FeatureToggle/FeatureCollection.php | 25 +++--- src/Symfony/Component/FeatureToggle/LICENSE | 19 +++++ src/Symfony/Component/FeatureToggle/README.md | 77 +++++++++++++++++++ .../FeatureToggle/Strategy/DateStrategy.php | 3 +- .../FeatureToggle/Strategy/EnvStrategy.php | 4 +- .../Strategy/RequestHeaderStrategy.php | 4 +- .../Strategy/RequestQueryStrategy.php | 4 +- .../Tests/FeatureCollectionTest.php | 4 +- .../FeatureToggle/Tests/FeatureTest.php | 3 +- .../Strategy/AffirmativeStrategyTest.php | 3 +- .../Tests/Strategy/DateStrategyTest.php | 15 ++-- .../Tests/Strategy/PriorityStrategyTest.php | 4 +- .../Tests/StrategyResultTest.php | 4 +- .../Component/FeatureToggle/composer.json | 4 +- 23 files changed, 176 insertions(+), 80 deletions(-) create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/LICENSE create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/README.md create mode 100644 src/Symfony/Component/FeatureToggle/LICENSE create mode 100644 src/Symfony/Component/FeatureToggle/README.md diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php index 4111ea6f1b812..cf3fe276a0dfd 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FeatureToggleBundle\DataCollector; -use Closure; use Symfony\Component\FeatureToggle\Feature; use Symfony\Component\FeatureToggle\FeatureCollection; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; @@ -65,8 +64,8 @@ public function __construct( public function collect(Request $request, Response $response, \Throwable $exception = null): void { foreach ($this->featureCollection as $feature) { - $strategy = (Closure::bind(fn(): StrategyInterface => $this->strategy, $feature, Feature::class))(); - $default = (Closure::bind(fn(): bool => $this->default, $feature, Feature::class))(); + $strategy = (\Closure::bind(fn(): StrategyInterface => $this->strategy, $feature, Feature::class))(); + $default = (\Closure::bind(fn(): bool => $this->default, $feature, Feature::class))(); $this->data['features'][$feature->getName()] = [ 'default' => $default, diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php index f227e1c7d74b8..abdad612414fd 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass; -use Closure; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,8 +28,8 @@ public function process(ContainerBuilder $container): void $collection = $container->getDefinition('toggle_feature.feature_collection'); foreach ($this->findAndSortTaggedServices('feature_toggle.feature_provider', $container) as $provider) { - $collectionDefinition = (new Definition(Closure::class)) - ->setFactory([Closure::class, 'fromCallable']) + $collectionDefinition = (new Definition(\Closure::class)) + ->setFactory([\Closure::class, 'fromCallable']) ->setArguments([[$provider, 'provide']]) ; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php index 27f5612b4fcd9..49e07dccb26a7 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -11,12 +11,8 @@ namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection; -use InvalidArgumentException; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -use function implode; -use function sprintf; -use function trim; /** * @phpstan-type ConfigurationType array{ @@ -93,37 +89,37 @@ public function getConfigTreeBuilder(): TreeBuilder $validator = match ($strategy['type']) { 'date' => static function (array $with): void { if ('' === trim((string)$with['from'] . (string)$with['until'])) { - throw new InvalidArgumentException('Either "from" or "until" must be provided.'); + throw new \InvalidArgumentException('Either "from" or "until" must be provided.'); } }, 'not' => static function (array $with): void { if ('' === (string)$with['strategy']) { - throw new InvalidArgumentException('"strategy" must be provided.'); + throw new \InvalidArgumentException('"strategy" must be provided.'); } }, 'env' => static function (array $with): void { if ('' === (string)$with['name']) { - throw new InvalidArgumentException('"name" must be provided.'); + throw new \InvalidArgumentException('"name" must be provided.'); } }, 'native_request_header' => static function (array $with): void { if ('' === (string)$with['name']) { - throw new InvalidArgumentException('"name" must be provided.'); + throw new \InvalidArgumentException('"name" must be provided.'); } }, 'native_request_query' => static function (array $with): void { if ('' === (string)$with['name']) { - throw new InvalidArgumentException('"name" must be provided.'); + throw new \InvalidArgumentException('"name" must be provided.'); } }, 'request_attribute' => static function (array $with): void { if ('' === (string)$with['name']) { - throw new InvalidArgumentException('"name" must be provided.'); + throw new \InvalidArgumentException('"name" must be provided.'); } }, 'priority', 'affirmative' => static function (array $with): void { if ([] === (array)$with['strategies']) { - throw new InvalidArgumentException('"strategies" must be provided.'); + throw new \InvalidArgumentException('"strategies" must be provided.'); } }, default => static fn(): bool => true, diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index c3af597236867..e51596e0c9457 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection; -use DateTimeImmutable; use Symfony\Bundle\FeatureToggleBundle\Strategy\CustomStrategy; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -24,7 +23,6 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\Router; use Twig\Environment; -use function array_map; /** * @phpstan-import-type ConfigurationType from Configuration @@ -108,8 +106,8 @@ private function generateStrategy(string $type, array $with): Definition return match ($type) { 'date' => $definition->setArguments([ - '$from' => new Definition(DateTimeImmutable::class, [$with['from']]), - '$until' => new Definition(DateTimeImmutable::class, [$with['until']]), + '$from' => new Definition(\DateTimeImmutable::class, [$with['from']]), + '$until' => new Definition(\DateTimeImmutable::class, [$with['until']]), '$includeFrom' => $with['includeFrom'], '$includeUntil' => $with['includeUntil'], ]), diff --git a/src/Symfony/Bundle/FeatureToggleBundle/LICENSE b/src/Symfony/Bundle/FeatureToggleBundle/LICENSE new file mode 100644 index 0000000000000..0138f8f071351 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Bundle/FeatureToggleBundle/README.md b/src/Symfony/Bundle/FeatureToggleBundle/README.md new file mode 100644 index 0000000000000..6135ba870b005 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/README.md @@ -0,0 +1,18 @@ +ToggleFeatureBundle +=================== + +ToggleFeatureBundle provides a tight integration of the Symfony ToggleFeature +component into the Symfony full-stack framework. + +**This Bundle is experimental**. +[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html) +are not covered by Symfony's +[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). + +Resources +--------- + +* [Contributing](https://symfony.com/doc/current/contributing/index.html) +* [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php index 914e53a854313..c2d7ff5544612 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FeatureToggleBundle\Tests\DependencyInjection; -use Generator; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\Configuration; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -48,7 +47,7 @@ public function testDefaultConfig(): void } - public static function provideValidStrategyNameConfigurationTest(): Generator + public static function provideValidStrategyNameConfigurationTest(): \Generator { yield 'simple name' => ['foobar']; yield 'underscore name' => ['foo_bar']; @@ -78,7 +77,7 @@ public function testValidStrategyNameConfiguration(string $strategyName): void self::assertArrayHasKey($strategyName, $config['strategies']); } - public static function provideValidFeatureNameConfigurationTest(): Generator + public static function provideValidFeatureNameConfigurationTest(): \Generator { yield 'simple name' => ['foobar']; yield 'underscore name' => ['foo_bar']; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index 0ce016c9e49a0..ed1655a8be2f6 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -17,8 +17,6 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; -use function array_column; -use function in_array; /** * @covers \Symfony\Bundle\FeatureToggleBundle\DependencyInjection\FeatureToggleExtension @@ -110,7 +108,7 @@ public function testStrategiesAreDefinedAsServicesAndTagged(): void foreach ($expectedServiceIds as $expectedServiceId) { self::assertContains($expectedServiceId, $serviceIds); - if (in_array($expectedServiceId, $serviceIds, true)) { + if (\in_array($expectedServiceId, $serviceIds, true)) { $serviceDefinition = $containerBuilder->getDefinition($expectedServiceId); self::assertTrue( diff --git a/src/Symfony/Bundle/FeatureToggleBundle/composer.json b/src/Symfony/Bundle/FeatureToggleBundle/composer.json index 4b8d305746236..2d859d647a046 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/composer.json +++ b/src/Symfony/Bundle/FeatureToggleBundle/composer.json @@ -20,7 +20,6 @@ "symfony/contracts": "^3.3", "symfony/config": "^6.3", "symfony/dependency-injection": "^6.3", - "symfony/options-resolver": "^6.3", "symfony/http-kernel": "^6.3" }, "require-dev": { @@ -29,7 +28,7 @@ "symfony/twig-bridge": "^6.3" }, "autoload": { - "psr-4": { "Symfony\\Bundle\\FeatureToggle\\": "" }, + "psr-4": { "Symfony\\Bundle\\FeatureToggleBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" ] diff --git a/src/Symfony/Component/FeatureToggle/FeatureCollection.php b/src/Symfony/Component/FeatureToggle/FeatureCollection.php index 68851495e31c1..4e6c33a331c80 100644 --- a/src/Symfony/Component/FeatureToggle/FeatureCollection.php +++ b/src/Symfony/Component/FeatureToggle/FeatureCollection.php @@ -11,20 +11,15 @@ namespace Symfony\Component\FeatureToggle; -use ArrayIterator; -use Closure; -use IteratorAggregate; use Psr\Container\ContainerInterface; -use function array_values; -use function is_callable; -/** @implements IteratorAggregate */ -final class FeatureCollection implements ContainerInterface, IteratorAggregate +/** @implements \IteratorAggregate */ +final class FeatureCollection implements ContainerInterface, \IteratorAggregate { /** @var array|null */ private array|null $features = null; - /** @var array|(Closure(): iterable)> */ + /** @var array|(\Closure(): iterable)> */ private array $featureProviders = []; /** @@ -36,18 +31,18 @@ public function __construct(iterable $features) } /** - * @param iterable|(Closure(): iterable) $features + * @param iterable|(\Closure(): iterable) $features */ - private function append(iterable|Closure $features): void + private function append(iterable|\Closure $features): void { $this->featureProviders[] = $features; $this->features = null; } /** - * @param iterable|(Closure(): iterable) $features + * @param iterable|(\Closure(): iterable) $features */ - public function withFeatures(iterable|Closure $features): self + public function withFeatures(iterable|\Closure $features): self { $this->append($features); @@ -92,12 +87,12 @@ public function get(string $id): Feature } /** - * @return ArrayIterator + * @return \ArrayIterator */ - public function getIterator(): ArrayIterator + public function getIterator(): \ArrayIterator { $this->compile(); - return new ArrayIterator(array_values($this->features)); + return new \ArrayIterator(array_values($this->features)); } } diff --git a/src/Symfony/Component/FeatureToggle/LICENSE b/src/Symfony/Component/FeatureToggle/LICENSE new file mode 100644 index 0000000000000..0138f8f071351 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/FeatureToggle/README.md b/src/Symfony/Component/FeatureToggle/README.md new file mode 100644 index 0000000000000..527a0b95cd288 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/README.md @@ -0,0 +1,77 @@ +ToggleFeature Component +======================= + +The ToggleFeature component provides TODO + +**This Component is experimental**. +[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html) +are not covered by Symfony's +[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). + +Usage +----- + +This example implements the following logic : +* **Grants** when the `feat` parameter equals to `true` in the query string. +* **Denies** when the `feat` parameter equals to `false` in the query string. +* **Abstain** when the `feat` parameter is not found in the query string. So it + fallbacks to `false`. + +```php +isEnabled('new_feature')) { + // Use the new feature +} else { + // Use the legacy code +} +``` + +Available strategies +-------------------- + +**AffirmativeStrategy** : Takes a list of `StrategyInterface` and stops at the first `Grant`. + +**DateStrategy** : Grant if current date is after the `$from` and before the `$until` ones. + +**DenyStrategy** : Always Denies. + +**EnvStrategy** : Will look for a truthy value in the given `$name` env variable. + +**GrantStrategy** : Always Grants. + +**NotStrategy** : Takes a `StrategyInterface` and inverts its returned value (except if abstained). + +**PriorityStrategy** : Takes a list of `StrategyInterface` and stops at the first non-abstain (either `Grant` or `Deny`). + +**RandomStrategy** TODO + +**RequestHeaderStrategy** : Will look for a truthy value in the given `$name` header. + +**RequestQueryStrategy** : Will look for a truthy value in the given `$name` query string parameter. + +Resources +--------- + +* [Contributing](https://symfony.com/doc/current/contributing/index.html) +* [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php index dec0c921bd6cf..c80fcb5e8bb3c 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php @@ -12,7 +12,6 @@ namespace Symfony\Component\FeatureToggle\Strategy; use Symfony\Component\FeatureToggle\StrategyResult; -use InvalidArgumentException; use Psr\Clock\ClockInterface; final class DateStrategy implements StrategyInterface @@ -25,7 +24,7 @@ public function __construct( private readonly bool $includeUntil = true, ) { if (null === $this->from && null === $this->until) { - throw new InvalidArgumentException('Either from or until must be provided.'); + throw new \InvalidArgumentException('Either from or until must be provided.'); } } diff --git a/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php index 746f7c5358532..eab754033db71 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/EnvStrategy.php @@ -12,8 +12,6 @@ namespace Symfony\Component\FeatureToggle\Strategy; use Symfony\Component\FeatureToggle\StrategyResult; -use function filter_var; -use const FILTER_VALIDATE_BOOL; final class EnvStrategy implements StrategyInterface { @@ -30,6 +28,6 @@ public function compute(): StrategyResult return StrategyResult::Abstain; } - return filter_var($value, FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + return filter_var($value, \FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; } } diff --git a/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php index 8fccceaf1e5f1..c24f11349ed78 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/RequestHeaderStrategy.php @@ -12,8 +12,6 @@ namespace Symfony\Component\FeatureToggle\Strategy; use Symfony\Component\FeatureToggle\StrategyResult; -use function filter_var; -use const FILTER_VALIDATE_BOOL; // TODO: make it case insensitive ? final class RequestHeaderStrategy implements StrategyInterface @@ -29,6 +27,6 @@ public function compute(): StrategyResult return StrategyResult::Abstain; } - return filter_var($_SERVER['HTTP_'.$this->headerName], FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + return filter_var($_SERVER['HTTP_'.$this->headerName], \FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; } } diff --git a/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php index 7f26f24db9f70..6d4e1d9f045b2 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/RequestQueryStrategy.php @@ -12,8 +12,6 @@ namespace Symfony\Component\FeatureToggle\Strategy; use Symfony\Component\FeatureToggle\StrategyResult; -use function filter_var; -use const FILTER_VALIDATE_BOOL; // TODO: make it case insensitive ? final class RequestQueryStrategy implements StrategyInterface @@ -29,6 +27,6 @@ public function compute(): StrategyResult return StrategyResult::Abstain; } - return filter_var($_GET[$this->queryParameterName], FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + return filter_var($_GET[$this->queryParameterName], \FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; } } diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php index b4ce1a55c5013..d3c5d2e5ad7ae 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php @@ -11,14 +11,12 @@ namespace Symfony\Component\FeatureToggle\Tests; -use Generator; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Symfony\Component\FeatureToggle\Feature; use Symfony\Component\FeatureToggle\FeatureCollection; use Symfony\Component\FeatureToggle\FeatureNotFoundException; use Symfony\Component\FeatureToggle\Strategy\GrantStrategy; -use function is_a; /** * @covers \Symfony\Component\FeatureToggle\FeatureCollection @@ -71,7 +69,7 @@ public function testEnsureItIsMergeableWithDifferentTypesOfIterable(): void ), ]); - $featureCollection->withFeatures(function (): Generator { + $featureCollection->withFeatures(function (): \Generator { yield new Feature( name: 'fake-3', description: 'Fake description 3', diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php index f388bd2eed6f0..8e4db22686822 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\FeatureToggle\Tests; -use Generator; use PHPUnit\Framework\TestCase; use Symfony\Component\FeatureToggle\Feature; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; @@ -41,7 +40,7 @@ public function compute(): StrategyResult self::addToAssertionCount(1); } - public static function generateValidStrategy(): Generator + public static function generateValidStrategy(): \Generator { // Grant yield "grant and default 'true'" => [ diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php index 28c927342d63f..c2abf990e77be 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\FeatureToggle\Tests\Strategy; -use Generator; use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; use Symfony\Component\FeatureToggle\Strategy\OuterStrategiesInterface; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; @@ -28,7 +27,7 @@ public function testEnsureItExposesInnerStrategies(): void self::assertTrue(is_a(AffirmativeStrategy::class, OuterStrategiesInterface::class, true)); } - public static function generatesValidStrategies(): Generator + public static function generatesValidStrategies(): \Generator { yield 'no strategies' => [ [], diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php index 12d6491208230..6fe67ea07d885 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/DateStrategyTest.php @@ -11,9 +11,6 @@ namespace Symfony\Component\FeatureToggle\Tests\Strategy; -use DateTimeImmutable; -use Generator; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Psr\Clock\ClockInterface; use Symfony\Component\FeatureToggle\Strategy\DateStrategy; @@ -26,16 +23,16 @@ final class DateStrategyTest extends TestCase { private static ClockInterface $nowClock; - private static function generateClock(DateTimeImmutable|null $now = null): ClockInterface + private static function generateClock(\DateTimeImmutable|null $now = null): ClockInterface { - $now = $now ?? DateTimeImmutable::createFromFormat('!d/m/Y', (new DateTimeImmutable())->format('d/m/Y')); + $now = $now ?? \DateTimeImmutable::createFromFormat('!d/m/Y', (new \DateTimeImmutable())->format('d/m/Y')); return new class($now) implements ClockInterface { - public function __construct(private DateTimeImmutable $now) + public function __construct(private \DateTimeImmutable $now) { } - public function now(): DateTimeImmutable + public function now(): \DateTimeImmutable { return $this->now; } @@ -49,13 +46,13 @@ public static function setUpBeforeClass(): void public function testItRequiresAtLeastOneDate(): void { - self::expectException(InvalidArgumentException::class); + self::expectException(\InvalidArgumentException::class); self::expectExceptionMessage('Either from or until must be provided.'); new DateStrategy(self::generateClock()); } - public static function generateValidDates(): Generator + public static function generateValidDates(): \Generator { $now = self::generateClock()->now(); diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php index 305e591e1e5df..d1a22af0a6b15 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php @@ -11,12 +11,10 @@ namespace Symfony\Component\FeatureToggle\Tests\Strategy; -use Generator; use Symfony\Component\FeatureToggle\Strategy\OuterStrategiesInterface; use Symfony\Component\FeatureToggle\Strategy\PriorityStrategy; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; -use function is_a; /** * @covers \Symfony\Component\FeatureToggle\Strategy\PriorityStrategy @@ -28,7 +26,7 @@ public function testEnsureItExposesInnerStrategies(): void self::assertTrue(is_a(PriorityStrategy::class, OuterStrategiesInterface::class, true)); } - public static function generatesValidStrategies(): Generator + public static function generatesValidStrategies(): \Generator { yield 'no strategies' => [ [], diff --git a/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php b/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php index fbcaf90b63189..ee02c14889f5b 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php @@ -11,17 +11,15 @@ namespace Symfony\Component\FeatureToggle\Tests; -use Generator; use PHPUnit\Framework\TestCase; use Symfony\Component\FeatureToggle\StrategyResult; -use function constant; /** * @covers \Symfony\Component\FeatureToggle\StrategyResult */ final class StrategyResultTest extends TestCase { - public static function generateValidUseCases(): Generator + public static function generateValidUseCases(): \Generator { // Grant yield "grant should ignore fallback #1" => [ diff --git a/src/Symfony/Component/FeatureToggle/composer.json b/src/Symfony/Component/FeatureToggle/composer.json index ccb3abb27e944..ef6981d794465 100644 --- a/src/Symfony/Component/FeatureToggle/composer.json +++ b/src/Symfony/Component/FeatureToggle/composer.json @@ -17,11 +17,9 @@ ], "require": { "php": ">=8.2", + "psr/clock": "^1.0", "psr/container": "^2.0" }, - "require-dev": { - "psr/clock": "^1.0" - }, "autoload": { "psr-4": { "Symfony\\Component\\FeatureToggle\\": "" }, "exclude-from-classmap": [ From f2cc75b7a4130f62157351eaa4c70fe3e68aacf8 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 7 Sep 2023 17:51:00 +0200 Subject: [PATCH 04/25] [UPDATE] Removed the Random Strategy & Provider --- .../FeatureToggle/Provider/RandomProvider.php | 39 ------------------- src/Symfony/Component/FeatureToggle/README.md | 2 - .../FeatureToggle/Strategy/RandomStrategy.php | 26 ------------- 3 files changed, 67 deletions(-) delete mode 100644 src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php delete mode 100644 src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php diff --git a/src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php b/src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php deleted file mode 100644 index cf31d2549caab..0000000000000 --- a/src/Symfony/Component/FeatureToggle/Provider/RandomProvider.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\FeatureToggle\Provider; - -use Symfony\Component\FeatureToggle\Feature; -use Symfony\Component\FeatureToggle\FeatureCollection; -use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; - -final class RandomProvider implements ProviderInterface -{ - public function __construct( - private readonly StrategyInterface $defaultStrategy, - ) { - } - - public function provide(): FeatureCollection - { - $features = []; - for ($i = 1; $i <= random_int(2, 10); $i++) { - $features[] = new Feature( - name: "random-feature-$i", - description: "Random feature #$i", - default: (bool) random_int(0, 1), - strategy: $this->defaultStrategy, - ); - } - - return new FeatureCollection($features); - } -} diff --git a/src/Symfony/Component/FeatureToggle/README.md b/src/Symfony/Component/FeatureToggle/README.md index 527a0b95cd288..2c7bb88662675 100644 --- a/src/Symfony/Component/FeatureToggle/README.md +++ b/src/Symfony/Component/FeatureToggle/README.md @@ -62,8 +62,6 @@ Available strategies **PriorityStrategy** : Takes a list of `StrategyInterface` and stops at the first non-abstain (either `Grant` or `Deny`). -**RandomStrategy** TODO - **RequestHeaderStrategy** : Will look for a truthy value in the given `$name` header. **RequestQueryStrategy** : Will look for a truthy value in the given `$name` query string parameter. diff --git a/src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php deleted file mode 100644 index 017cf3c41a363..0000000000000 --- a/src/Symfony/Component/FeatureToggle/Strategy/RandomStrategy.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\FeatureToggle\Strategy; - -use Symfony\Component\FeatureToggle\StrategyResult; - -final class RandomStrategy implements StrategyInterface -{ - public function compute(): StrategyResult - { - return match(random_int(0, 2)) { - 0 => StrategyResult::Grant, - 1 => StrategyResult::Abstain, - 2 => StrategyResult::Deny, - }; - } -} From 13d6176acf90b9c3f0fef3b5adfcb674dc23b1b0 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Wed, 13 Sep 2023 19:05:22 +0200 Subject: [PATCH 05/25] [UPDATE] According to comments --- .../DependencyInjection/CompilerPass/DebugPass.php | 4 ++-- .../CompilerPass/FeatureCollectionPass.php | 2 +- .../DependencyInjection/FeatureToggleExtension.php | 4 ++-- src/Symfony/Bundle/FeatureToggleBundle/README.md | 4 ++-- .../Bundle/FeatureToggleBundle/Resources/config/debug.php | 2 +- .../FeatureToggleBundle/Resources/config/feature.php | 8 ++++---- .../FeatureToggleBundle/Resources/config/providers.php | 2 +- .../FeatureToggleBundle/Resources/config/routing.php | 6 +++--- .../FeatureToggleBundle/Resources/config/strategies.php | 2 +- .../Bundle/FeatureToggleBundle/Resources/config/twig.php | 4 ++-- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php index 2a4b2dc6acda5..9432cdc8c2b56 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php @@ -25,8 +25,8 @@ public function process(ContainerBuilder $container): void return; } - $container->register('debug.toggle_feature.feature_checker', TraceableFeatureChecker::class) - ->setDecoratedService('toggle_feature.feature_checker') + $container->register('debug.feature_toggle.feature_checker', TraceableFeatureChecker::class) + ->setDecoratedService('feature_toggle.feature_checker') ->setArguments([ '$featureChecker' => new Reference('.inner'), '$dataCollector' => new Reference('feature_toggle.data_collector'), diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php index abdad612414fd..fb911637e5220 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php @@ -25,7 +25,7 @@ public function process(ContainerBuilder $container): void { $container->registerForAutoconfiguration(ProviderInterface::class)->addTag('feature_toggle.feature_provider'); - $collection = $container->getDefinition('toggle_feature.feature_collection'); + $collection = $container->getDefinition('feature_toggle.feature_collection'); foreach ($this->findAndSortTaggedServices('feature_toggle.feature_provider', $container) as $provider) { $collectionDefinition = (new Definition(\Closure::class)) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index e51596e0c9457..644e0d08bd973 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -75,7 +75,7 @@ private function loadFeatures(ContainerBuilder $container, array $config): void $features[] = new Reference($featureName); } - $container->getDefinition('toggle_feature.provider.in_memory') + $container->getDefinition('feature_toggle.provider.in_memory') ->setArguments([ '$features' => $features, ]) @@ -102,7 +102,7 @@ private function loadStrategies(ContainerBuilder $container, array $config): voi */ private function generateStrategy(string $type, array $with): Definition { - $definition = new ChildDefinition("toggle_feature.abstract_strategy.{$type}"); + $definition = new ChildDefinition("feature_toggle.abstract_strategy.{$type}"); return match ($type) { 'date' => $definition->setArguments([ diff --git a/src/Symfony/Bundle/FeatureToggleBundle/README.md b/src/Symfony/Bundle/FeatureToggleBundle/README.md index 6135ba870b005..1a6ac2bbf05e1 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/README.md +++ b/src/Symfony/Bundle/FeatureToggleBundle/README.md @@ -1,7 +1,7 @@ -ToggleFeatureBundle +FeatureToggleBundle =================== -ToggleFeatureBundle provides a tight integration of the Symfony ToggleFeature +FeatureToggleBundle provides a tight integration of the Symfony ToggleFeature component into the Symfony full-stack framework. **This Bundle is experimental**. diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php index 24ab2e8cdd46a..bc2132345bdad 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php @@ -9,7 +9,7 @@ $services->set('feature_toggle.data_collector', FeatureCheckerDataCollector::class) ->args([ - '$featureCollection' => service('toggle_feature.feature_collection') + '$featureCollection' => service('feature_toggle.feature_collection') ]) ->tag('data_collector', ['template' => '@FeatureToggle/Collector/profiler.html.twig', 'id' => 'feature_toggle']) ; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php index b87e03c6d7724..e79a4c03b5d48 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php @@ -9,17 +9,17 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('toggle_feature.feature_collection', FeatureCollection::class) + $services->set('feature_toggle.feature_collection', FeatureCollection::class) ->args([ '$features' => [], ]) ; - $services->set('toggle_feature.feature_checker', FeatureChecker::class) + $services->set('feature_toggle.feature_checker', FeatureChecker::class) ->args([ - '$features' => service('toggle_feature.feature_collection'), + '$features' => service('feature_toggle.feature_collection'), '$whenNotFound' => false, ]) ; - $services->alias(FeatureCheckerInterface::class, service('toggle_feature.feature_checker')); + $services->alias(FeatureCheckerInterface::class, service('feature_toggle.feature_checker')); }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php index 9737bf7ccf440..9fe0f2baed86d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php @@ -7,7 +7,7 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('toggle_feature.provider.in_memory', InMemoryProvider::class) + $services->set('feature_toggle.provider.in_memory', InMemoryProvider::class) ->tag('feature_toggle.feature_provider', ['priority' => 16]) ; }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php index 1340a4d4b77bf..0f93de415a3e7 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php @@ -5,15 +5,15 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('toggle_feature.routing.provider', \Closure::class) + $services->set('feature_toggle.routing.provider', \Closure::class) ->factory([\Closure::class, 'fromCallable']) ->args([ - [service('toggle_feature.feature_checker'), 'isEnabled'], + [service('feature_toggle.feature_checker'), 'isEnabled'], ]) ->tag('routing.expression_language_function', ['function' => 'isFeatureEnabled']) ; - $services->get('toggle_feature.feature_checker') + $services->get('feature_toggle.feature_checker') ->tag('routing.condition_service', ['alias' => 'feature']) ; }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php index 0ade373ec9b4f..63332fac4b44b 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\Strategy\RequestQueryStrategy; return static function (ContainerConfigurator $container) { - $prefix = 'toggle_feature.abstract_strategy.'; + $prefix = 'feature_toggle.abstract_strategy.'; $services = $container->services(); $services->set($prefix.'grant', GrantStrategy::class)->abstract(); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php index d6e0879c3e037..ad617f2b41f4c 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php @@ -7,9 +7,9 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('toggle_feature.twig_extension', FeatureEnabledExtension::class) + $services->set('feature_toggle.twig_extension', FeatureEnabledExtension::class) ->args([ - service('toggle_feature.feature_checker'), + service('feature_toggle.feature_checker'), ]) ->tag('twig.extension') ; From 1d4fb6c6a0ab50926cae61190e41f98f56535d9b Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Wed, 13 Sep 2023 19:12:33 +0200 Subject: [PATCH 06/25] [FabBot] Apply some patches --- .../FeatureCheckerDataCollector.php | 4 +- .../DependencyInjection/Configuration.php | 20 +++---- .../FeatureToggleExtension.php | 2 +- .../Resources/config/debug.php | 2 +- .../Resources/config/strategies.php | 2 +- .../RequestStackAttributeStrategy.php | 2 +- .../DependencyInjection/ConfigurationTest.php | 17 +++--- .../FeatureToggleExtensionTest.php | 58 +++++++++---------- 8 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php index cf3fe276a0dfd..dc686b403e391 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php @@ -64,8 +64,8 @@ public function __construct( public function collect(Request $request, Response $response, \Throwable $exception = null): void { foreach ($this->featureCollection as $feature) { - $strategy = (\Closure::bind(fn(): StrategyInterface => $this->strategy, $feature, Feature::class))(); - $default = (\Closure::bind(fn(): bool => $this->default, $feature, Feature::class))(); + $strategy = (\Closure::bind(fn (): StrategyInterface => $this->strategy, $feature, Feature::class))(); + $default = (\Closure::bind(fn (): bool => $this->default, $feature, Feature::class))(); $this->data['features'][$feature->getName()] = [ 'default' => $default, diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php index 49e07dccb26a7..1120d060c8b53 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -19,13 +19,11 @@ * strategies: array, * features: array * } - * * @phpstan-type ConfigurationStrategy array{ * name: string, * type: string, * with: array * } - * * @phpstan-type ConfigurationFeature array{ * name: string, * description: string, @@ -68,7 +66,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->beforeNormalization() ->always() ->then(static function (array $strategy): array { - $defaultWith = match($strategy['type']) { + $defaultWith = match ($strategy['type']) { 'date' => ['from' => null, 'until' => null, 'includeFrom' => false, 'includeUntil' => false], 'not' => ['strategy' => null], 'env', 'native_request_header', 'native_request_query', 'request_attribute' => ['name' => null], @@ -88,41 +86,41 @@ public function getConfigTreeBuilder(): TreeBuilder /** @var ConfigurationStrategy $strategy */ $validator = match ($strategy['type']) { 'date' => static function (array $with): void { - if ('' === trim((string)$with['from'] . (string)$with['until'])) { + if ('' === trim((string) $with['from'].(string) $with['until'])) { throw new \InvalidArgumentException('Either "from" or "until" must be provided.'); } }, 'not' => static function (array $with): void { - if ('' === (string)$with['strategy']) { + if ('' === (string) $with['strategy']) { throw new \InvalidArgumentException('"strategy" must be provided.'); } }, 'env' => static function (array $with): void { - if ('' === (string)$with['name']) { + if ('' === (string) $with['name']) { throw new \InvalidArgumentException('"name" must be provided.'); } }, 'native_request_header' => static function (array $with): void { - if ('' === (string)$with['name']) { + if ('' === (string) $with['name']) { throw new \InvalidArgumentException('"name" must be provided.'); } }, 'native_request_query' => static function (array $with): void { - if ('' === (string)$with['name']) { + if ('' === (string) $with['name']) { throw new \InvalidArgumentException('"name" must be provided.'); } }, 'request_attribute' => static function (array $with): void { - if ('' === (string)$with['name']) { + if ('' === (string) $with['name']) { throw new \InvalidArgumentException('"name" must be provided.'); } }, 'priority', 'affirmative' => static function (array $with): void { - if ([] === (array)$with['strategies']) { + if ([] === (array) $with['strategies']) { throw new \InvalidArgumentException('"strategies" must be provided.'); } }, - default => static fn(): bool => true, + default => static fn (): bool => true, }; $validator($strategy['with']); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index 644e0d08bd973..0c93a034df06d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -34,7 +34,7 @@ public function load(array $configs, ContainerBuilder $container): void /** @var ConfigurationType $config */ $config = $this->processConfiguration(new Configuration(), $configs); - $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__) . '/Resources/config')); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); $loader->load('feature.php'); $loader->load('providers.php'); $loader->load('strategies.php'); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php index bc2132345bdad..8492099bc08c1 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php @@ -9,7 +9,7 @@ $services->set('feature_toggle.data_collector', FeatureCheckerDataCollector::class) ->args([ - '$featureCollection' => service('feature_toggle.feature_collection') + '$featureCollection' => service('feature_toggle.feature_collection'), ]) ->tag('data_collector', ['template' => '@FeatureToggle/Collector/profiler.html.twig', 'id' => 'feature_toggle']) ; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php index 63332fac4b44b..a2c7085867f33 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -18,7 +18,7 @@ $services = $container->services(); $services->set($prefix.'grant', GrantStrategy::class)->abstract(); $services->set($prefix.'not', NotStrategy::class)->abstract()->args([ - '$inner' => abstract_arg('Defined in FeatureToggleExtension') + '$inner' => abstract_arg('Defined in FeatureToggleExtension'), ]); $services->set($prefix.'env', EnvStrategy::class)->abstract()->args([ '$envName' => abstract_arg('Defined in FeatureToggleExtension'), diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php index d42df6c82286f..24daa784db52f 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php @@ -35,7 +35,7 @@ public function compute(): StrategyResult return StrategyResult::Abstain; } - if ($currentRequest->attributes->has($this->attributeName) === false) { + if (false === $currentRequest->attributes->has($this->attributeName)) { return StrategyResult::Abstain; } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php index c2d7ff5544612..a5ee8a4b0dccb 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -38,7 +38,7 @@ public static function getBundleDefaultConfig(): array public function testDefaultConfig(): void { $processor = new Processor(); - $config = $processor->processConfiguration( + $config = $processor->processConfiguration( new Configuration(), [], ); @@ -46,7 +46,6 @@ public function testDefaultConfig(): void self::assertEquals(self::getBundleDefaultConfig(), $config); } - public static function provideValidStrategyNameConfigurationTest(): \Generator { yield 'simple name' => ['foobar']; @@ -60,7 +59,7 @@ public static function provideValidStrategyNameConfigurationTest(): \Generator public function testValidStrategyNameConfiguration(string $strategyName): void { $processor = new Processor(); - $config = $processor->processConfiguration( + $config = $processor->processConfiguration( new Configuration(), [ [ @@ -90,16 +89,16 @@ public static function provideValidFeatureNameConfigurationTest(): \Generator public function testValidFeatureNameConfiguration(string $featureName): void { $processor = new Processor(); - $config = $processor->processConfiguration( + $config = $processor->processConfiguration( new Configuration(), [ [ 'features' => [ [ - 'name' => $featureName, + 'name' => $featureName, 'description' => "This is the description of {$featureName}", - 'strategy' => 'fake-strategy', - 'default' => false, + 'strategy' => 'fake-strategy', + 'default' => false, ], ], ], @@ -121,7 +120,7 @@ public function testFeatureRequiresDescriptionKey(): void [ 'features' => [ [ - 'name' => 'some-feature', + 'name' => 'some-feature', 'strategy' => 'fake-strategy', ], ], @@ -142,7 +141,7 @@ public function testFeatureRequiresStrategyKey(): void [ 'features' => [ [ - 'name' => 'some-feature', + 'name' => 'some-feature', 'default' => false, ], ], diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index ed1655a8be2f6..7ef88fa889f45 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -37,55 +37,55 @@ public function getConfig(): array [ 'strategies' => [ [ - 'name' => 'date.feature-strategy', - 'type' => 'date', - 'with' => ['from' => '-2 days'], + 'name' => 'date.feature-strategy', + 'type' => 'date', + 'with' => ['from' => '-2 days'], ], [ - 'name' => 'env.feature-strategy', - 'type' => 'env', - 'with' => ['name' => 'SOME_ENV'], + 'name' => 'env.feature-strategy', + 'type' => 'env', + 'with' => ['name' => 'SOME_ENV'], ], [ - 'name' => 'native_request_header.feature-strategy', - 'type' => 'native_request_header', - 'with' => ['name' => 'SOME-HEADER-NAME'], + 'name' => 'native_request_header.feature-strategy', + 'type' => 'native_request_header', + 'with' => ['name' => 'SOME-HEADER-NAME'], ], [ - 'name' => 'native_request_query.feature-strategy', - 'type' => 'native_request_query', - 'with' => ['name' => 'some_query_parameter'], + 'name' => 'native_request_query.feature-strategy', + 'type' => 'native_request_query', + 'with' => ['name' => 'some_query_parameter'], ], [ - 'name' => 'request_attribute.feature-strategy', - 'type' => 'request_attribute', - 'with' => ['name' => 'some_request_attribute'], + 'name' => 'request_attribute.feature-strategy', + 'type' => 'request_attribute', + 'with' => ['name' => 'some_request_attribute'], ], [ - 'name' => 'priority.feature-strategy', - 'type' => 'priority', - 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], + 'name' => 'priority.feature-strategy', + 'type' => 'priority', + 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], ], [ - 'name' => 'affirmative.feature-strategy', - 'type' => 'affirmative', - 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], + 'name' => 'affirmative.feature-strategy', + 'type' => 'affirmative', + 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], ], [ - 'name' => 'not.feature-strategy', - 'type' => 'not', - 'with' => ['strategy' => 'grant.feature-strategy'], + 'name' => 'not.feature-strategy', + 'type' => 'not', + 'with' => ['strategy' => 'grant.feature-strategy'], ], [ - 'name' => 'grant.feature-strategy', - 'type' => 'grant', + 'name' => 'grant.feature-strategy', + 'type' => 'grant', ], [ - 'name' => 'deny.feature-strategy', - 'type' => 'deny', + 'name' => 'deny.feature-strategy', + 'type' => 'deny', ], ], - 'features' => [ + 'features' => [ ], ], ]; From a8a2536143b01637eca22caba890af9408fe007b Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:01:29 +0200 Subject: [PATCH 07/25] [UPDATE] Remove Outer*Interface's --- .../Strategy/AffirmativeStrategy.php | 7 +------ .../Strategy/OuterStrategiesInterface.php | 20 ------------------- .../Strategy/OuterStrategyInterface.php | 17 ---------------- .../Strategy/PriorityStrategy.php | 7 +------ .../Strategy/AffirmativeStrategyTest.php | 6 ------ .../Tests/Strategy/PriorityStrategyTest.php | 6 ------ 6 files changed, 2 insertions(+), 61 deletions(-) delete mode 100644 src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php delete mode 100644 src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php diff --git a/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php index c6a6407dac070..d7fe2fe68d0d5 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\StrategyResult; -final class AffirmativeStrategy implements OuterStrategiesInterface +final class AffirmativeStrategy { /** * @param iterable $strategies @@ -40,9 +40,4 @@ public function compute(): StrategyResult return $result; } - - public function getInnerStrategies(): iterable - { - return $this->strategies; - } } diff --git a/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php b/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php deleted file mode 100644 index 4c899c89baded..0000000000000 --- a/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategiesInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\FeatureToggle\Strategy; - -interface OuterStrategiesInterface extends StrategyInterface -{ - /** - * @return iterable - */ - public function getInnerStrategies(): iterable; -} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php b/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php deleted file mode 100644 index c3ef440e92cec..0000000000000 --- a/src/Symfony/Component/FeatureToggle/Strategy/OuterStrategyInterface.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\FeatureToggle\Strategy; - -interface OuterStrategyInterface extends StrategyInterface -{ - public function getInnerStrategy(): StrategyInterface; -} diff --git a/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php index 536f7e00ca141..a4b121c399d50 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\StrategyResult; -final class PriorityStrategy implements OuterStrategiesInterface +final class PriorityStrategy { /** * @param iterable $strategies @@ -36,9 +36,4 @@ public function compute(): StrategyResult return $result; } - - public function getInnerStrategies(): iterable - { - return $this->strategies; - } } diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php index c2abf990e77be..257873692dd59 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\FeatureToggle\Tests\Strategy; use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; -use Symfony\Component\FeatureToggle\Strategy\OuterStrategiesInterface; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; use function is_a; @@ -22,11 +21,6 @@ */ final class AffirmativeStrategyTest extends AbstractOuterStrategiesTestCase { - public function testEnsureItExposesInnerStrategies(): void - { - self::assertTrue(is_a(AffirmativeStrategy::class, OuterStrategiesInterface::class, true)); - } - public static function generatesValidStrategies(): \Generator { yield 'no strategies' => [ diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php index d1a22af0a6b15..3fc7353ddfbfd 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/PriorityStrategyTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\FeatureToggle\Tests\Strategy; -use Symfony\Component\FeatureToggle\Strategy\OuterStrategiesInterface; use Symfony\Component\FeatureToggle\Strategy\PriorityStrategy; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; @@ -21,11 +20,6 @@ */ final class PriorityStrategyTest extends AbstractOuterStrategiesTestCase { - public function testEnsureItExposesInnerStrategies(): void - { - self::assertTrue(is_a(PriorityStrategy::class, OuterStrategiesInterface::class, true)); - } - public static function generatesValidStrategies(): \Generator { yield 'no strategies' => [ From 844ae651270c375f1de3b3ace396310f8deb3c51 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:01:57 +0200 Subject: [PATCH 08/25] [UPDATE] Use Traversable instead of ArrayIterator --- src/Symfony/Component/FeatureToggle/FeatureCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/FeatureToggle/FeatureCollection.php b/src/Symfony/Component/FeatureToggle/FeatureCollection.php index 4e6c33a331c80..7ee417ed78ef9 100644 --- a/src/Symfony/Component/FeatureToggle/FeatureCollection.php +++ b/src/Symfony/Component/FeatureToggle/FeatureCollection.php @@ -87,9 +87,9 @@ public function get(string $id): Feature } /** - * @return \ArrayIterator + * @return \Traversable */ - public function getIterator(): \ArrayIterator + public function getIterator(): \Traversable { $this->compile(); From 0473edc526a802e0ae9cfe08a5fff444631c9e0f Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:32:48 +0200 Subject: [PATCH 09/25] [UPDATE] Remove forgottent Outer*Interface's --- .../Component/FeatureToggle/Debug/TraceableStrategy.php | 8 +------- .../Component/FeatureToggle/Strategy/NotStrategy.php | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php b/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php index 7bb2eed32c727..6ab8f04d621ec 100644 --- a/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php @@ -12,11 +12,10 @@ namespace Symfony\Component\FeatureToggle\Debug; use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; -use Symfony\Component\FeatureToggle\Strategy\OuterStrategyInterface; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; -final class TraceableStrategy implements StrategyInterface, OuterStrategyInterface +final class TraceableStrategy implements StrategyInterface { public function __construct( private readonly StrategyInterface $strategy, @@ -35,9 +34,4 @@ public function compute(): StrategyResult return $result; } - - public function getInnerStrategy(): StrategyInterface - { - return $this->strategy; - } } diff --git a/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php index a6aafc3062f34..0b057aae66edd 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\StrategyResult; -final class NotStrategy implements OuterStrategyInterface +final class NotStrategy { public function __construct( private readonly StrategyInterface $inner, @@ -30,9 +30,4 @@ public function compute(): StrategyResult StrategyResult::Deny => StrategyResult::Grant, }; } - - public function getInnerStrategy(): StrategyInterface - { - return $this->inner; - } } From ba54221cf5ab4c4a8edf1f3b507cae1c06319e91 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:36:29 +0200 Subject: [PATCH 10/25] [UPDATE] Add StrategyInterface on previous Outer*Strategy's --- .../Component/FeatureToggle/Strategy/AffirmativeStrategy.php | 2 +- src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php | 2 +- .../Component/FeatureToggle/Strategy/PriorityStrategy.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php index d7fe2fe68d0d5..4aea92d4ae4b9 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/AffirmativeStrategy.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\StrategyResult; -final class AffirmativeStrategy +final class AffirmativeStrategy implements StrategyInterface { /** * @param iterable $strategies diff --git a/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php index 0b057aae66edd..1d90eaf147b49 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/NotStrategy.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\StrategyResult; -final class NotStrategy +final class NotStrategy implements StrategyInterface { public function __construct( private readonly StrategyInterface $inner, diff --git a/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php index a4b121c399d50..d39e927e43cb9 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/PriorityStrategy.php @@ -13,7 +13,7 @@ use Symfony\Component\FeatureToggle\StrategyResult; -final class PriorityStrategy +final class PriorityStrategy implements StrategyInterface { /** * @param iterable $strategies From 4f0351162cab56362baaa137ae34bc8efb3f2134 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:38:21 +0200 Subject: [PATCH 11/25] [FEATURE] Add header, query strategies through request stack and make it default when using framework --- .../DependencyInjection/Configuration.php | 10 ++-- .../FeatureToggleExtension.php | 4 +- .../Resources/config/strategies.php | 15 +++--- .../RequestStackAttributeStrategy.php | 22 ++------ .../Strategy/RequestStackHeaderStrategy.php | 32 +++++++++++ .../Strategy/RequestStackQueryStrategy.php | 32 +++++++++++ .../Strategy/RequestStackStrategy.php | 54 +++++++++++++++++++ .../FeatureToggleExtensionTest.php | 8 +-- 8 files changed, 141 insertions(+), 36 deletions(-) create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackQueryStrategy.php create mode 100644 src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackStrategy.php diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php index 1120d060c8b53..4bcca1e8b2eca 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -33,7 +33,7 @@ */ final class Configuration implements ConfigurationInterface { - private const KNOWN_STRATEGY_TYPES = ['grant', 'deny', 'not', 'date', 'env', 'native_request_header', 'native_request_query', 'request_attribute', 'priority', 'affirmative']; + private const KNOWN_STRATEGY_TYPES = ['grant', 'deny', 'not', 'date', 'env', 'request_header', 'request_query', 'request_attribute', 'priority', 'affirmative']; public function getConfigTreeBuilder(): TreeBuilder { @@ -55,7 +55,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->isRequired() ->cannotBeEmpty() ->info(sprintf('Can be one of : %s. Or a service ID.', implode(', ', self::KNOWN_STRATEGY_TYPES))) - ->example('native_request_header') + ->example('request_header') ->end() ->variableNode('with') ->defaultValue([]) @@ -69,7 +69,7 @@ public function getConfigTreeBuilder(): TreeBuilder $defaultWith = match ($strategy['type']) { 'date' => ['from' => null, 'until' => null, 'includeFrom' => false, 'includeUntil' => false], 'not' => ['strategy' => null], - 'env', 'native_request_header', 'native_request_query', 'request_attribute' => ['name' => null], + 'env', 'request_header', 'request_query', 'request_attribute' => ['name' => null], 'priority', 'affirmative' => ['strategies' => null], default => [], }; @@ -100,12 +100,12 @@ public function getConfigTreeBuilder(): TreeBuilder throw new \InvalidArgumentException('"name" must be provided.'); } }, - 'native_request_header' => static function (array $with): void { + 'request_header' => static function (array $with): void { if ('' === (string) $with['name']) { throw new \InvalidArgumentException('"name" must be provided.'); } }, - 'native_request_query' => static function (array $with): void { + 'request_query' => static function (array $with): void { if ('' === (string) $with['name']) { throw new \InvalidArgumentException('"name" must be provided.'); } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index 0c93a034df06d..6ce4cd2189d8d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -112,8 +112,8 @@ private function generateStrategy(string $type, array $with): Definition '$includeUntil' => $with['includeUntil'], ]), 'env' => $definition->setArguments(['$envName' => $with['name']]), - 'native_request_header' => $definition->setArguments(['$headerName' => $with['name']]), - 'native_request_query' => $definition->setArguments(['$queryParameterName' => $with['name']]), + 'request_header' => $definition->setArguments(['$headerName' => $with['name']]), + 'request_query' => $definition->setArguments(['$queryParameterName' => $with['name']]), 'request_attribute' => $definition->setArguments(['$attributeName' => $with['name']]), // Check if RequestStack class exists 'priority', 'affirmative' => $definition->setArguments([ '$strategies' => array_map( diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php index a2c7085867f33..bf6d792e4ca05 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -3,14 +3,14 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackAttributeStrategy; +use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackHeaderStrategy; +use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackQueryStrategy; use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; use Symfony\Component\FeatureToggle\Strategy\DateStrategy; use Symfony\Component\FeatureToggle\Strategy\EnvStrategy; use Symfony\Component\FeatureToggle\Strategy\GrantStrategy; use Symfony\Component\FeatureToggle\Strategy\NotStrategy; use Symfony\Component\FeatureToggle\Strategy\PriorityStrategy; -use Symfony\Component\FeatureToggle\Strategy\RequestHeaderStrategy; -use Symfony\Component\FeatureToggle\Strategy\RequestQueryStrategy; return static function (ContainerConfigurator $container) { $prefix = 'feature_toggle.abstract_strategy.'; @@ -32,14 +32,13 @@ ]); $services->set($prefix.'request_attribute', RequestStackAttributeStrategy::class)->abstract()->args([ '$attributeName' => abstract_arg('Defined in FeatureToggleExtension'), - '$requestStack' => service('request_stack')->nullOnInvalid(), - ]); - $services->set($prefix.'native_request_header', RequestHeaderStrategy::class)->abstract()->args([ + ])->call('setRequestStack', [service('request_stack')->nullOnInvalid()]); + $services->set($prefix.'request_header', RequestStackHeaderStrategy::class)->abstract()->args([ '$headerName' => abstract_arg('Defined in FeatureToggleExtension'), - ]); - $services->set($prefix.'native_request_query', RequestQueryStrategy::class)->abstract()->args([ + ])->call('setRequestStack', [service('request_stack')->nullOnInvalid()]); + $services->set($prefix.'request_query', RequestStackQueryStrategy::class)->abstract()->args([ '$queryParameterName' => abstract_arg('Defined in FeatureToggleExtension'), - ]); + ])->call('setRequestStack', [service('request_stack')->nullOnInvalid()]); $services->set($prefix.'priority', PriorityStrategy::class)->abstract()->args([ '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), ]); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php index 24daa784db52f..d16f44592b765 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackAttributeStrategy.php @@ -11,34 +11,22 @@ namespace Symfony\Bundle\FeatureToggleBundle\Strategy; -use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; -use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; -final class RequestStackAttributeStrategy implements StrategyInterface +final class RequestStackAttributeStrategy extends RequestStackStrategy { public function __construct( private readonly string $attributeName, - private readonly RequestStack|null $requestStack = null, ) { } - public function compute(): StrategyResult + protected function computeRequest(Request $request): StrategyResult { - if (null === $this->requestStack) { + if (false === $request->attributes->has($this->attributeName)) { return StrategyResult::Abstain; } - $currentRequest = $this->requestStack->getCurrentRequest(); - - if (null === $currentRequest) { - return StrategyResult::Abstain; - } - - if (false === $currentRequest->attributes->has($this->attributeName)) { - return StrategyResult::Abstain; - } - - return $currentRequest->attributes->getBoolean($this->attributeName) ? StrategyResult::Grant : StrategyResult::Deny; + return $request->attributes->getBoolean($this->attributeName) ? StrategyResult::Grant : StrategyResult::Deny; } } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php new file mode 100644 index 0000000000000..f15468b44aa47 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Component\HttpFoundation\Request; + +final class RequestStackHeaderStrategy extends RequestStackStrategy +{ + public function __construct( + private readonly string $headerName, + ) { + } + + protected function computeRequest(Request $request): StrategyResult + { + if (false === $request->headers->has($this->headerName)) { + return StrategyResult::Abstain; + } + + return \filter_var($request->headers->get($this->headerName), \FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackQueryStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackQueryStrategy.php new file mode 100644 index 0000000000000..0080b68a389d4 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackQueryStrategy.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Component\HttpFoundation\Request; + +final class RequestStackQueryStrategy extends RequestStackStrategy +{ + public function __construct( + private readonly string $queryParameterName, + ) { + } + + protected function computeRequest(Request $request): StrategyResult + { + if (false === $request->query->has($this->queryParameterName)) { + return StrategyResult::Abstain; + } + + return $request->query->getBoolean($this->queryParameterName) ? StrategyResult::Grant : StrategyResult::Deny; + } +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackStrategy.php new file mode 100644 index 0000000000000..e6d3e506852e9 --- /dev/null +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackStrategy.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FeatureToggleBundle\Strategy; + +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; + +abstract class RequestStackStrategy implements StrategyInterface +{ + private RequestStack|null $requestStack = null; + + public function setRequestStack(RequestStack|null $requestStack = null): void + { + $this->requestStack = $requestStack; + } + + public function compute(): StrategyResult + { + if (null === $this->requestStack) { + return StrategyResult::Abstain; + } + + $currentRequest = $this->requestStack->getCurrentRequest(); + + if (null === $currentRequest) { + return StrategyResult::Abstain; + } + + if (($result = $this->computeRequest($currentRequest)) !== StrategyResult::Abstain) { + return $result; + } + + $mainRequest = $this->requestStack->getMainRequest(); + + if (null === $mainRequest) { + return StrategyResult::Abstain; + } + + return $this->computeRequest($mainRequest); + } + + abstract protected function computeRequest(Request $request): StrategyResult; +} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index 7ef88fa889f45..c5eb596efdf4c 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -47,13 +47,13 @@ public function getConfig(): array 'with' => ['name' => 'SOME_ENV'], ], [ - 'name' => 'native_request_header.feature-strategy', - 'type' => 'native_request_header', + 'name' => 'request_header.feature-strategy', + 'type' => 'request_header', 'with' => ['name' => 'SOME-HEADER-NAME'], ], [ - 'name' => 'native_request_query.feature-strategy', - 'type' => 'native_request_query', + 'name' => 'request_query.feature-strategy', + 'type' => 'request_query', 'with' => ['name' => 'some_query_parameter'], ], [ From 416e91dfa91cf5f29b50bdd8dd24d601b926c69c Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:47:55 +0200 Subject: [PATCH 12/25] [UPDATE] Add UnanimousStrategy --- .../DependencyInjection/Configuration.php | 6 +- .../FeatureToggleExtension.php | 2 +- .../Resources/config/strategies.php | 4 + .../FeatureToggleExtensionTest.php | 5 ++ .../Strategy/UnanimousStrategy.php | 43 +++++++++ .../Strategy/AffirmativeStrategyTest.php | 1 - .../Tests/Strategy/UnanimousStrategyTest.php | 88 +++++++++++++++++++ 7 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/FeatureToggle/Strategy/UnanimousStrategy.php create mode 100644 src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php index 4bcca1e8b2eca..5df89543f9018 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -33,7 +33,7 @@ */ final class Configuration implements ConfigurationInterface { - private const KNOWN_STRATEGY_TYPES = ['grant', 'deny', 'not', 'date', 'env', 'request_header', 'request_query', 'request_attribute', 'priority', 'affirmative']; + private const KNOWN_STRATEGY_TYPES = ['grant', 'deny', 'not', 'date', 'env', 'request_header', 'request_query', 'request_attribute', 'priority', 'affirmative', 'unanimous']; public function getConfigTreeBuilder(): TreeBuilder { @@ -70,7 +70,7 @@ public function getConfigTreeBuilder(): TreeBuilder 'date' => ['from' => null, 'until' => null, 'includeFrom' => false, 'includeUntil' => false], 'not' => ['strategy' => null], 'env', 'request_header', 'request_query', 'request_attribute' => ['name' => null], - 'priority', 'affirmative' => ['strategies' => null], + 'priority', 'affirmative', 'unanimous' => ['strategies' => null], default => [], }; @@ -115,7 +115,7 @@ public function getConfigTreeBuilder(): TreeBuilder throw new \InvalidArgumentException('"name" must be provided.'); } }, - 'priority', 'affirmative' => static function (array $with): void { + 'priority', 'affirmative', 'unanimous' => static function (array $with): void { if ([] === (array) $with['strategies']) { throw new \InvalidArgumentException('"strategies" must be provided.'); } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index 6ce4cd2189d8d..3cc1c85f8f2b2 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -115,7 +115,7 @@ private function generateStrategy(string $type, array $with): Definition 'request_header' => $definition->setArguments(['$headerName' => $with['name']]), 'request_query' => $definition->setArguments(['$queryParameterName' => $with['name']]), 'request_attribute' => $definition->setArguments(['$attributeName' => $with['name']]), // Check if RequestStack class exists - 'priority', 'affirmative' => $definition->setArguments([ + 'priority', 'affirmative', 'unanimous' => $definition->setArguments([ '$strategies' => array_map( static fn (string $referencedStrategyName): Reference => new Reference($referencedStrategyName), // @phpstan-ignore-line (array) $with['strategies'], diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php index bf6d792e4ca05..82b3d92effaf7 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -11,6 +11,7 @@ use Symfony\Component\FeatureToggle\Strategy\GrantStrategy; use Symfony\Component\FeatureToggle\Strategy\NotStrategy; use Symfony\Component\FeatureToggle\Strategy\PriorityStrategy; +use Symfony\Component\FeatureToggle\Strategy\UnanimousStrategy; return static function (ContainerConfigurator $container) { $prefix = 'feature_toggle.abstract_strategy.'; @@ -45,4 +46,7 @@ $services->set($prefix.'affirmative', AffirmativeStrategy::class)->abstract()->args([ '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), ]); + $services->set($prefix.'unanimous', UnanimousStrategy::class)->abstract()->args([ + '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), + ]); }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index c5eb596efdf4c..a2a884dc7638c 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -71,6 +71,11 @@ public function getConfig(): array 'type' => 'affirmative', 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], ], + [ + 'name' => 'unanimous.feature-strategy', + 'type' => 'unanimous', + 'with' => ['strategies' => ['env.feature-strategy', 'grant.feature-strategy']], + ], [ 'name' => 'not.feature-strategy', 'type' => 'not', diff --git a/src/Symfony/Component/FeatureToggle/Strategy/UnanimousStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/UnanimousStrategy.php new file mode 100644 index 0000000000000..53ce5d53afd29 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Strategy/UnanimousStrategy.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Strategy; + +use Symfony\Component\FeatureToggle\StrategyResult; + +final class UnanimousStrategy implements StrategyInterface +{ + /** + * @param iterable $strategies + */ + public function __construct( + private readonly iterable $strategies, + ) { + } + + public function compute(): StrategyResult + { + $result = StrategyResult::Abstain; + foreach ($this->strategies as $strategy) { + $innerResult = $strategy->compute(); + + if (StrategyResult::Deny === $innerResult) { + return StrategyResult::Deny; + } + + if (StrategyResult::Grant === $innerResult) { + $result = StrategyResult::Grant; + } + } + + return $result; + } +} diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php index 257873692dd59..b9d020df4d035 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/AffirmativeStrategyTest.php @@ -14,7 +14,6 @@ use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; -use function is_a; /** * @covers \Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php new file mode 100644 index 0000000000000..f2b348bbab973 --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\FeatureToggle\Tests\Strategy; + +use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; +use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureToggle\Strategy\UnanimousStrategy; +use Symfony\Component\FeatureToggle\StrategyResult; + +/** + * @covers \Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy + */ +final class UnanimousStrategyTest extends AbstractOuterStrategiesTestCase +{ + public static function generatesValidStrategies(): \Generator + { + yield 'no strategies' => [ + [], + StrategyResult::Abstain, + ]; + + yield 'if all abstain' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + ], + StrategyResult::Abstain, + ]; + + yield 'if one denies after only abstain results' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Deny), + ], + StrategyResult::Deny, + ]; + + yield 'if one grants after only abstain results' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Grant), + ], + StrategyResult::Grant, + ]; + + yield 'if one grants after at least one Deny' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Deny), + self::generateStrategy(StrategyResult::Grant), + ], + StrategyResult::Deny, + ]; + + yield 'if one denies after at least one grant' => [ + [ + self::generateStrategy(StrategyResult::Abstain), + self::generateStrategy(StrategyResult::Grant), + self::generateStrategy(StrategyResult::Deny), + ], + StrategyResult::Deny, + ]; + } + + /** + * @dataProvider generatesValidStrategies + * + * @param iterable $strategies + */ + public function testItComputesCorrectly(iterable $strategies, StrategyResult $expected): void + { + $affirmativeStrategy = new UnanimousStrategy($strategies); + + self::assertSame($expected, $affirmativeStrategy->compute()); + } +} From 17fae0abde9cd1c2ad086768ae7f0af87c227773 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:49:56 +0200 Subject: [PATCH 13/25] [UPDATE] Prefixed some return phpdoc tag with phpstan- --- .../DataCollector/FeatureCheckerDataCollector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php index dc686b403e391..d002a5d004ab5 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php @@ -139,7 +139,7 @@ public function lateCollect(): void } /** - * @return list|Data + * @phpstan-return list|Data */ public function getFeatures(): array|Data { @@ -147,7 +147,7 @@ public function getFeatures(): array|Data } /** - * @return list|Data + * @phpstan-return list|Data */ public function getToggles(): array|Data { From ad36be4d41e1344781cf50a55c2536bc316858a8 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:54:00 +0200 Subject: [PATCH 14/25] [UPDATE] Rename to --- .../DependencyInjection/Configuration.php | 6 +++--- .../DependencyInjection/FeatureToggleExtension.php | 4 ++-- .../Resources/config/strategies.php | 4 ++-- .../FeatureToggleExtensionTest.php | 2 +- .../FeatureToggle/Strategy/DateStrategy.php | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php index 5df89543f9018..0738620a61262 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -67,7 +67,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->always() ->then(static function (array $strategy): array { $defaultWith = match ($strategy['type']) { - 'date' => ['from' => null, 'until' => null, 'includeFrom' => false, 'includeUntil' => false], + 'date' => ['since' => null, 'until' => null, 'includeSince' => false, 'includeUntil' => false], 'not' => ['strategy' => null], 'env', 'request_header', 'request_query', 'request_attribute' => ['name' => null], 'priority', 'affirmative', 'unanimous' => ['strategies' => null], @@ -86,8 +86,8 @@ public function getConfigTreeBuilder(): TreeBuilder /** @var ConfigurationStrategy $strategy */ $validator = match ($strategy['type']) { 'date' => static function (array $with): void { - if ('' === trim((string) $with['from'].(string) $with['until'])) { - throw new \InvalidArgumentException('Either "from" or "until" must be provided.'); + if ('' === trim((string) $with['since'].(string) $with['until'])) { + throw new \InvalidArgumentException('Either "since" or "until" must be provided.'); } }, 'not' => static function (array $with): void { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index 3cc1c85f8f2b2..b2a6ee49e9ade 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -106,9 +106,9 @@ private function generateStrategy(string $type, array $with): Definition return match ($type) { 'date' => $definition->setArguments([ - '$from' => new Definition(\DateTimeImmutable::class, [$with['from']]), + '$since' => new Definition(\DateTimeImmutable::class, [$with['since']]), '$until' => new Definition(\DateTimeImmutable::class, [$with['until']]), - '$includeFrom' => $with['includeFrom'], + '$includeSince' => $with['includeSince'], '$includeUntil' => $with['includeUntil'], ]), 'env' => $definition->setArguments(['$envName' => $with['name']]), diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php index 82b3d92effaf7..87c43c1beddaf 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -25,9 +25,9 @@ '$envName' => abstract_arg('Defined in FeatureToggleExtension'), ]); $services->set($prefix.'date', DateStrategy::class)->abstract()->args([ - '$from' => abstract_arg('Defined in FeatureToggleExtension'), + '$since' => abstract_arg('Defined in FeatureToggleExtension'), '$until' => abstract_arg('Defined in FeatureToggleExtension'), - '$includeFrom' => abstract_arg('Defined in FeatureToggleExtension'), + '$includeSince' => abstract_arg('Defined in FeatureToggleExtension'), '$includeUntil' => abstract_arg('Defined in FeatureToggleExtension'), '$clock' => service('clock')->nullOnInvalid(), ]); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index a2a884dc7638c..5b37ebf6c673a 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -39,7 +39,7 @@ public function getConfig(): array [ 'name' => 'date.feature-strategy', 'type' => 'date', - 'with' => ['from' => '-2 days'], + 'with' => ['since' => '-2 days'], ], [ 'name' => 'env.feature-strategy', diff --git a/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php b/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php index c80fcb5e8bb3c..a53092e6e7faa 100644 --- a/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php +++ b/src/Symfony/Component/FeatureToggle/Strategy/DateStrategy.php @@ -18,12 +18,12 @@ final class DateStrategy implements StrategyInterface { public function __construct( private readonly ClockInterface $clock, - private readonly \DateTimeImmutable|null $from = null, + private readonly \DateTimeImmutable|null $since = null, private readonly \DateTimeImmutable|null $until = null, - private readonly bool $includeFrom = true, + private readonly bool $includeSince = true, private readonly bool $includeUntil = true, ) { - if (null === $this->from && null === $this->until) { + if (null === $this->since && null === $this->until) { throw new \InvalidArgumentException('Either from or until must be provided.'); } } @@ -32,12 +32,12 @@ public function compute(): StrategyResult { $now = $this->clock->now(); - if (null !== $this->from) { - if ($this->includeFrom && $this->from > $now) { + if (null !== $this->since) { + if ($this->includeSince && $this->since > $now) { return StrategyResult::Deny; } - if (!$this->includeFrom && $this->from >= $now) { + if (!$this->includeSince && $this->since >= $now) { return StrategyResult::Deny; } } From 858f66d7ba0defbf5b9b77e243eee811184e4d29 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Thu, 21 Sep 2023 20:59:05 +0200 Subject: [PATCH 15/25] [UPDATE] Fix phpunit covers annotation --- .../FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php b/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php index f2b348bbab973..9d09f0ad6f46e 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/Strategy/UnanimousStrategyTest.php @@ -11,13 +11,12 @@ namespace Symfony\Component\FeatureToggle\Tests\Strategy; -use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\Strategy\UnanimousStrategy; use Symfony\Component\FeatureToggle\StrategyResult; /** - * @covers \Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy + * @covers \Symfony\Component\FeatureToggle\Strategy\UnanimousStrategy */ final class UnanimousStrategyTest extends AbstractOuterStrategiesTestCase { From 6a356aa5c07fe0eadec2a42f3f60cea8ed0aee82 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 22 Sep 2023 08:38:41 +0200 Subject: [PATCH 16/25] [UPDATE][fabbot] Coding standard --- .../FeatureToggleBundle/Resources/config/debug.php | 9 +++++++++ .../FeatureToggleBundle/Resources/config/feature.php | 9 +++++++++ .../FeatureToggleBundle/Resources/config/providers.php | 9 +++++++++ .../FeatureToggleBundle/Resources/config/routing.php | 9 +++++++++ .../FeatureToggleBundle/Resources/config/strategies.php | 9 +++++++++ .../Bundle/FeatureToggleBundle/Resources/config/twig.php | 9 +++++++++ .../Strategy/RequestStackHeaderStrategy.php | 2 +- 7 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php index 8492099bc08c1..5d929e8166f0d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php index e79a4c03b5d48..06283075ee0a0 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\FeatureToggle\FeatureChecker; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php index 9fe0f2baed86d..cf0c4bdea8993 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\FeatureToggle\Provider\InMemoryProvider; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php index 0f93de415a3e7..1751796d25fec 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; return static function (ContainerConfigurator $container) { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php index 87c43c1beddaf..a98329252534d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackAttributeStrategy; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php index ad617f2b41f4c..737816f4656a8 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FeatureToggleBundle\Twig\FeatureEnabledExtension; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php index f15468b44aa47..528de6d8ec466 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Strategy/RequestStackHeaderStrategy.php @@ -27,6 +27,6 @@ protected function computeRequest(Request $request): StrategyResult return StrategyResult::Abstain; } - return \filter_var($request->headers->get($this->headerName), \FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; + return filter_var($request->headers->get($this->headerName), \FILTER_VALIDATE_BOOL) ? StrategyResult::Grant : StrategyResult::Deny; } } From cec1e92fb5e2ba0f4396e8b66b77635b2ccfdddf Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 22 Sep 2023 08:41:23 +0200 Subject: [PATCH 17/25] [UPDATE][fabbot] usage of void in tests --- .../Tests/DependencyInjection/ConfigurationTest.php | 10 +++++----- .../DependencyInjection/FeatureToggleExtensionTest.php | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php index a5ee8a4b0dccb..b7ef99a0ae7ff 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -35,7 +35,7 @@ public static function getBundleDefaultConfig(): array return ['strategies' => [], 'features' => []]; } - public function testDefaultConfig(): void + public function testDefaultConfig() { $processor = new Processor(); $config = $processor->processConfiguration( @@ -56,7 +56,7 @@ public static function provideValidStrategyNameConfigurationTest(): \Generator /** * @dataProvider provideValidStrategyNameConfigurationTest */ - public function testValidStrategyNameConfiguration(string $strategyName): void + public function testValidStrategyNameConfiguration(string $strategyName) { $processor = new Processor(); $config = $processor->processConfiguration( @@ -86,7 +86,7 @@ public static function provideValidFeatureNameConfigurationTest(): \Generator /** * @dataProvider provideValidFeatureNameConfigurationTest */ - public function testValidFeatureNameConfiguration(string $featureName): void + public function testValidFeatureNameConfiguration(string $featureName) { $processor = new Processor(); $config = $processor->processConfiguration( @@ -108,7 +108,7 @@ public function testValidFeatureNameConfiguration(string $featureName): void self::assertArrayHasKey($featureName, $config['features']); } - public function testFeatureRequiresDescriptionKey(): void + public function testFeatureRequiresDescriptionKey() { self::expectException(InvalidConfigurationException::class); self::expectExceptionMessage('The child config "default" under "feature_toggle.features.some-feature" must be configured: Will be used as a fallback mechanism if the strategy return StrategyResult::Abstain.'); @@ -129,7 +129,7 @@ public function testFeatureRequiresDescriptionKey(): void ); } - public function testFeatureRequiresStrategyKey(): void + public function testFeatureRequiresStrategyKey() { self::expectException(InvalidConfigurationException::class); self::expectExceptionMessage('The child config "strategy" under "feature_toggle.features.some-feature" must be configured: Strategy to be used for this feature. Can be one of "feature_toggle.strategies[].name" or a valid service id that implements StrategyInterface::class.'); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index 5b37ebf6c673a..404dc6282d372 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -101,7 +101,7 @@ public function getContainerBuilder(): ContainerBuilder return new ContainerBuilder(new ParameterBag(['kernel.debug' => true])); } - public function testStrategiesAreDefinedAsServicesAndTagged(): void + public function testStrategiesAreDefinedAsServicesAndTagged() { $extension = new FeatureToggleExtension(); @@ -127,7 +127,7 @@ public function testStrategiesAreDefinedAsServicesAndTagged(): void } } - public function testAutoconfigurationForInterfaces(): void + public function testAutoconfigurationForInterfaces() { $extension = new FeatureToggleExtension(); From 7618497190338551a7e22a4647014ad6688b06a5 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 22 Sep 2023 20:45:40 +0200 Subject: [PATCH 18/25] [UPDATE] Improve performance on FeatureCollection to stop at first found --- .../FeatureToggle/FeatureCollection.php | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/FeatureToggle/FeatureCollection.php b/src/Symfony/Component/FeatureToggle/FeatureCollection.php index 7ee417ed78ef9..59f09352d7327 100644 --- a/src/Symfony/Component/FeatureToggle/FeatureCollection.php +++ b/src/Symfony/Component/FeatureToggle/FeatureCollection.php @@ -12,12 +12,15 @@ namespace Symfony\Component\FeatureToggle; use Psr\Container\ContainerInterface; +use function array_key_exists; +use function array_shift; +use function is_callable; /** @implements \IteratorAggregate */ final class FeatureCollection implements ContainerInterface, \IteratorAggregate { - /** @var array|null */ - private array|null $features = null; + /** @var array */ + private array $features = []; /** @var array|(\Closure(): iterable)> */ private array $featureProviders = []; @@ -36,7 +39,6 @@ public function __construct(iterable $features) private function append(iterable|\Closure $features): void { $this->featureProviders[] = $features; - $this->features = null; } /** @@ -49,18 +51,13 @@ public function withFeatures(iterable|\Closure $features): self return $this; } - /** - * @phpstan-assert-if-true null $this->features - */ - private function compile(): void + private function findFeature(string $featureName): ?Feature { - if (null !== $this->features) { - return; + if (array_key_exists($featureName, $this->features)) { + return $this->features[$featureName]; } - $this->features = []; - - foreach ($this->featureProviders as $featureProvider) { + while (($featureProvider = array_shift($this->featureProviders)) !== null) { if (is_callable($featureProvider)) { $featureProvider = $featureProvider(); } @@ -68,13 +65,18 @@ private function compile(): void foreach ($featureProvider as $feature) { $this->features[$feature->getName()] = $feature; } + + if (array_key_exists($featureName, $this->features)) { + return $this->features[$featureName]; + } } + + return null; } public function has(string $id): bool { - $this->compile(); - return array_key_exists($id, $this->features); + return $this->findFeature($id) !== null; } /** @@ -82,8 +84,7 @@ public function has(string $id): bool */ public function get(string $id): Feature { - $this->compile(); - return $this->features[$id] ?? throw new FeatureNotFoundException($id); + return $this->findFeature($id) ?? throw new FeatureNotFoundException($id); } /** @@ -91,7 +92,7 @@ public function get(string $id): Feature */ public function getIterator(): \Traversable { - $this->compile(); + $this->findFeature(''); return new \ArrayIterator(array_values($this->features)); } From 0ff46e49fd6879a127e06cf6c6489bc008d61ded Mon Sep 17 00:00:00 2001 From: Hubert Lenoir Date: Mon, 25 Sep 2023 14:57:11 +0200 Subject: [PATCH 19/25] [Update] Move Debug files --- .../FeatureCheckerDataCollector.php | 61 ++++----------- .../Debug/TraceableFeatureChecker.php | 2 +- .../Debug/TraceableStrategy.php | 2 +- .../CompilerPass/DebugPass.php | 4 +- .../DependencyInjection/Configuration.php | 18 ----- .../FeatureToggleExtension.php | 4 - .../Resources/config/debug.php | 3 - .../views/Collector/profiler.html.twig | 74 ++++++------------- .../DependencyInjection/ConfigurationTest.php | 14 +--- .../FeatureToggleExtensionTest.php | 5 -- .../Component/FeatureToggle/Feature.php | 6 +- .../FeatureToggle/StrategyResult.php | 9 --- .../Tests/StrategyResultTest.php | 74 ------------------- 13 files changed, 44 insertions(+), 232 deletions(-) rename src/Symfony/{Component/FeatureToggle => Bundle/FeatureToggleBundle}/Debug/TraceableFeatureChecker.php (94%) rename src/Symfony/{Component/FeatureToggle => Bundle/FeatureToggleBundle}/Debug/TraceableStrategy.php (95%) delete mode 100644 src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php index d002a5d004ab5..022ab3bd90bf4 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php @@ -11,9 +11,6 @@ namespace Symfony\Bundle\FeatureToggleBundle\DataCollector; -use Symfony\Component\FeatureToggle\Feature; -use Symfony\Component\FeatureToggle\FeatureCollection; -use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\FeatureToggle\StrategyResult; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -23,26 +20,17 @@ use Symfony\Component\VarDumper\Cloner\Data; /** - * @phpstan-type FeatureType array{ - * default: bool, - * description: string, - * strategy: StrategyInterface, - * } - * @phpstan-type ToggleType array{ - * feature: string, - * result: bool|null, - * computes: array, - * } - * @phpstan-type ComputeType array{ - * strategyId: string, - * strategyClass: string, - * level: int, - * result: StrategyResult|null, - * } - * * @property Data|array{ - * features: array, - * toggles: array, + * toggles: array, + * }>, * } $data */ final class FeatureCheckerDataCollector extends DataCollector implements LateDataCollectorInterface @@ -53,26 +41,15 @@ final class FeatureCheckerDataCollector extends DataCollector implements LateDat /** @var \SplStack */ private \SplStack $currentCompute; - public function __construct( - private readonly FeatureCollection $featureCollection, - ) { - $this->data = ['features' => [], 'toggles' => []]; + public function __construct() + { + $this->data = ['toggles' => []]; $this->currentToggle = new \SplStack(); $this->currentCompute = new \SplStack(); } public function collect(Request $request, Response $response, \Throwable $exception = null): void { - foreach ($this->featureCollection as $feature) { - $strategy = (\Closure::bind(fn (): StrategyInterface => $this->strategy, $feature, Feature::class))(); - $default = (\Closure::bind(fn (): bool => $this->default, $feature, Feature::class))(); - - $this->data['features'][$feature->getName()] = [ - 'default' => $default, - 'description' => $feature->getDescription(), - 'strategy' => $strategy, - ]; - } } public function collectIsEnabledStart(string $featureName): void @@ -128,7 +105,6 @@ public function getName(): string public function reset(): void { $this->data = [ - 'features' => [], 'toggles' => [], ]; } @@ -138,17 +114,6 @@ public function lateCollect(): void $this->data = $this->cloneVar($this->data); } - /** - * @phpstan-return list|Data - */ - public function getFeatures(): array|Data - { - return $this->data['features']; - } - - /** - * @phpstan-return list|Data - */ public function getToggles(): array|Data { return $this->data['toggles']; diff --git a/src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php b/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableFeatureChecker.php similarity index 94% rename from src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php rename to src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableFeatureChecker.php index da33190a60da2..727be093a0c4d 100644 --- a/src/Symfony/Component/FeatureToggle/Debug/TraceableFeatureChecker.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableFeatureChecker.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\FeatureToggle\Debug; +namespace Symfony\Bundle\FeatureToggleBundle\Debug; use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; use Symfony\Component\FeatureToggle\FeatureCheckerInterface; diff --git a/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php b/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableStrategy.php similarity index 95% rename from src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php rename to src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableStrategy.php index 6ab8f04d621ec..b4564fbd6a88d 100644 --- a/src/Symfony/Component/FeatureToggle/Debug/TraceableStrategy.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableStrategy.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\FeatureToggle\Debug; +namespace Symfony\Bundle\FeatureToggleBundle\Debug; use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php index 9432cdc8c2b56..b41a516f3b225 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php @@ -11,11 +11,11 @@ namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass; +use Symfony\Bundle\FeatureToggleBundle\Debug\TraceableFeatureChecker; +use Symfony\Bundle\FeatureToggleBundle\Debug\TraceableStrategy; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\FeatureToggle\Debug\TraceableFeatureChecker; -use Symfony\Component\FeatureToggle\Debug\TraceableStrategy; final class DebugPass implements CompilerPassInterface { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php index 0738620a61262..76db2cd43f8c9 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php @@ -14,23 +14,6 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -/** - * @phpstan-type ConfigurationType array{ - * strategies: array, - * features: array - * } - * @phpstan-type ConfigurationStrategy array{ - * name: string, - * type: string, - * with: array - * } - * @phpstan-type ConfigurationFeature array{ - * name: string, - * description: string, - * default: bool, - * strategy: string, - * } - */ final class Configuration implements ConfigurationInterface { private const KNOWN_STRATEGY_TYPES = ['grant', 'deny', 'not', 'date', 'env', 'request_header', 'request_query', 'request_attribute', 'priority', 'affirmative', 'unanimous']; @@ -83,7 +66,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->validate() ->always() ->then(static function (array $strategy): array { - /** @var ConfigurationStrategy $strategy */ $validator = match ($strategy['type']) { 'date' => static function (array $with): void { if ('' === trim((string) $with['since'].(string) $with['until'])) { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index b2a6ee49e9ade..93673f78da4fb 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -24,14 +24,10 @@ use Symfony\Component\Routing\Router; use Twig\Environment; -/** - * @phpstan-import-type ConfigurationType from Configuration - */ final class FeatureToggleExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { - /** @var ConfigurationType $config */ $config = $this->processConfiguration(new Configuration(), $configs); $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php index 5d929e8166f0d..df062b8c9486d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php @@ -17,9 +17,6 @@ $services = $container->services(); $services->set('feature_toggle.data_collector', FeatureCheckerDataCollector::class) - ->args([ - '$featureCollection' => service('feature_toggle.feature_collection'), - ]) ->tag('data_collector', ['template' => '@FeatureToggle/Collector/profiler.html.twig', 'id' => 'feature_toggle']) ; }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig b/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig index a2a83d08fcce1..f2b03e5e2340b 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig @@ -3,36 +3,32 @@ {% block page_title 'Feature Toggle' %} {% block toolbar %} - {% set icon %} - Feature Toggle - {{ collector.features|length }} - Features - {% endset %} + {% if 0 < collector.toggles|length %} + {% set icon %} + Feature Toggle + {{ collector.toggles|length }} + Toggles + {% endset %} - {% set text %} -
-
- Toggles - {% for toggle in collector.toggles %} - {% set unknown = collector.features[toggle['feature']] is not defined %} - {% set color = not unknown and toggle['result'] ? 'green' : 'red' %} - - {{ unknown ? '✗' : '' }}{{ toggle['feature'] }} - - {% endfor %} -
-
- Features - {{ collector.features|length }} + {% set text %} +
+
+ Toggles + {% for toggle in collector.toggles %} + + {{ toggle['feature'] }} + + {% endfor %} +
-
- {% endset %} + {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} + {% endif %} {% endblock %} {% block menu %} - + Feature Toggle Feature toggle @@ -99,7 +95,7 @@ {{ result|capitalize }} - {%- if collector.features[toggle['feature']] is not defined %} + {%- if toggle['computes'] is empty %} (unknown) {% elseif (toggle['computes']|first).result.name|lower is same as 'abstain' %} (default) @@ -123,34 +119,6 @@

No feature toggle checked

{% endif %} - -

Available features

- {% if collector.features|length > 0 %} - - - - - - - - - - - {% for featureName, featureData in collector.features %} - - - - - - - {% endfor %} - -
NameDefaultDescriptionStrategy
{{ featureName }}{{ featureData.default|json_encode }}{{ featureData.description }}{{ profiler_dump(featureData.strategy) }}
- {% else %} -
-

No features configured

-
- {% endif %} {% endblock %} {% macro indent(level) %} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php index b7ef99a0ae7ff..350f60391b2a9 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -20,21 +20,9 @@ * @covers \Symfony\Bundle\FeatureToggleBundle\DependencyInjection\Configuration * * @uses \Symfony\Component\Config\Definition\Processor - * - * @phpstan-import-type ConfigurationType from Configuration - * @phpstan-import-type ConfigurationStrategy from Configuration - * @phpstan-import-type ConfigurationFeature from Configuration */ final class ConfigurationTest extends TestCase { - /** - * @return ConfigurationType - */ - public static function getBundleDefaultConfig(): array - { - return ['strategies' => [], 'features' => []]; - } - public function testDefaultConfig() { $processor = new Processor(); @@ -43,7 +31,7 @@ public function testDefaultConfig() [], ); - self::assertEquals(self::getBundleDefaultConfig(), $config); + self::assertEquals(['strategies' => [], 'features' => []], $config); } public static function provideValidStrategyNameConfigurationTest(): \Generator diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index 404dc6282d372..23dedd73c1650 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -23,14 +23,9 @@ * * @uses \Symfony\Component\DependencyInjection\ContainerBuilder * @uses \Symfony\Component\DependencyInjection\ParameterBag\ParameterBag - * - * @phpstan-import-type ConfigurationType from Configuration */ final class FeatureToggleExtensionTest extends TestCase { - /** - * @return list - */ public function getConfig(): array { return [ diff --git a/src/Symfony/Component/FeatureToggle/Feature.php b/src/Symfony/Component/FeatureToggle/Feature.php index 76ed5f435d9a3..312b7cf7093e7 100644 --- a/src/Symfony/Component/FeatureToggle/Feature.php +++ b/src/Symfony/Component/FeatureToggle/Feature.php @@ -35,6 +35,10 @@ public function getDescription(): string public function isEnabled(): bool { - return $this->strategy->compute()->isEnabled($this->default); + return match($this->strategy->compute()) { + StrategyResult::Grant => true, + StrategyResult::Deny => false, + StrategyResult::Abstain => $this->default, + }; } } diff --git a/src/Symfony/Component/FeatureToggle/StrategyResult.php b/src/Symfony/Component/FeatureToggle/StrategyResult.php index 0d76f380808b2..fdc1fec78ce28 100644 --- a/src/Symfony/Component/FeatureToggle/StrategyResult.php +++ b/src/Symfony/Component/FeatureToggle/StrategyResult.php @@ -16,13 +16,4 @@ enum StrategyResult case Grant; case Deny; case Abstain; - - public function isEnabled(bool $fallback): bool - { - return match($this) { - self::Grant => true, - self::Deny => false, - self::Abstain => $fallback, - }; - } } diff --git a/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php b/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php deleted file mode 100644 index ee02c14889f5b..0000000000000 --- a/src/Symfony/Component/FeatureToggle/Tests/StrategyResultTest.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\FeatureToggle\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\FeatureToggle\StrategyResult; - -/** - * @covers \Symfony\Component\FeatureToggle\StrategyResult - */ -final class StrategyResultTest extends TestCase -{ - public static function generateValidUseCases(): \Generator - { - // Grant - yield "grant should ignore fallback #1" => [ - StrategyResult::Grant->name, - true, - true, - ]; - - yield "grant should ignore fallback #2" => [ - StrategyResult::Grant->name, - false, - true, - ]; - - // Deny - yield "deny should ignore fallback #1" => [ - StrategyResult::Deny->name, - true, - false, - ]; - - yield "deny should ignore fallback #2" => [ - StrategyResult::Deny->name, - false, - false, - ]; - - // Abstain - yield "abstain should use fallback #1" => [ - StrategyResult::Abstain->name, - true, - true, - ]; - - yield "abstain should use fallback #2" => [ - StrategyResult::Abstain->name, - false, - false, - ]; - } - - /** - * @dataProvider generateValidUseCases - */ - public function testItCorrectlyMatchesToBool(string $result, bool $fallback, bool $expectedResult): void - { - /** @var StrategyResult $strategyResult */ - $strategyResult = constant(StrategyResult::class.'::'.$result); - - self::assertSame($expectedResult, $strategyResult->isEnabled($fallback)); - } -} From 19b5c5f3b3648c4d800cc3c4fffecc5299305aa8 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 29 Sep 2023 10:34:12 +0200 Subject: [PATCH 20/25] Rework of providers --- .../CompilerPass/FeatureCollectionPass.php | 41 ------------- .../FeatureToggleExtension.php | 36 ++++++----- .../FeatureToggleBundle.php | 2 - .../Resources/config/feature.php | 2 +- .../Resources/config/providers.php | 7 ++- .../FeatureToggle/FeatureCollection.php | 59 ++++++++----------- .../Provider/InMemoryProvider.php | 24 ++++++-- .../Provider/LazyInMemoryProvider.php | 34 +++++++++++ .../Provider/ProviderInterface.php | 9 ++- src/Symfony/Component/FeatureToggle/README.md | 8 ++- .../Tests/FeatureCheckerTest.php | 4 +- .../Tests/FeatureCollectionTest.php | 51 +++------------- 12 files changed, 122 insertions(+), 155 deletions(-) delete mode 100644 src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php create mode 100644 src/Symfony/Component/FeatureToggle/Provider/LazyInMemoryProvider.php diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php deleted file mode 100644 index fb911637e5220..0000000000000 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/FeatureCollectionPass.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\FeatureToggle\Provider\ProviderInterface; - -final class FeatureCollectionPass implements CompilerPassInterface -{ - use PriorityTaggedServiceTrait; - - public function process(ContainerBuilder $container): void - { - $container->registerForAutoconfiguration(ProviderInterface::class)->addTag('feature_toggle.feature_provider'); - - $collection = $container->getDefinition('feature_toggle.feature_collection'); - - foreach ($this->findAndSortTaggedServices('feature_toggle.feature_provider', $container) as $provider) { - $collectionDefinition = (new Definition(\Closure::class)) - ->setFactory([\Closure::class, 'fromCallable']) - ->setArguments([[$provider, 'provide']]) - ; - - $collection - ->addMethodCall('withFeatures', [$collectionDefinition]) - ; - } - } -} diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php index 93673f78da4fb..0215369a379a6 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php @@ -13,12 +13,14 @@ use Symfony\Bundle\FeatureToggleBundle\Strategy\CustomStrategy; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\FeatureToggle\Feature; +use Symfony\Component\FeatureToggle\Provider\ProviderInterface; use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\Router; @@ -28,6 +30,10 @@ final class FeatureToggleExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { + $container->registerForAutoconfiguration(ProviderInterface::class) + ->addTag('feature_toggle.feature_provider') + ; + $config = $this->processConfiguration(new Configuration(), $configs); $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); @@ -53,34 +59,26 @@ public function load(array $configs, ContainerBuilder $container): void } } - /** - * @param ConfigurationType $config - */ private function loadFeatures(ContainerBuilder $container, array $config): void { $features = []; foreach ($config['features'] as $featureName => $featureConfig) { - $definition = new Definition(Feature::class, [ - '$name' => $featureName, - '$description' => $featureConfig['description'], - '$default' => $featureConfig['default'], - '$strategy' => new Reference($featureConfig['strategy']), - ]); - $container->setDefinition($featureName, $definition); - - $features[] = new Reference($featureName); + $features[$featureName] = new ServiceClosureArgument((new Definition(Feature::class)) + ->setShared(false) + ->setArguments([ + $featureName, + $featureConfig['description'], + $featureConfig['default'], + new Reference($featureConfig['strategy']), + ])) + ; } - $container->getDefinition('feature_toggle.provider.in_memory') - ->setArguments([ - '$features' => $features, - ]) + $container->getDefinition('feature_toggle.provider.lazy_in_memory') + ->setArgument('$features', $features) ; } - /** - * @param ConfigurationType $config - */ private function loadStrategies(ContainerBuilder $container, array $config): void { $container->registerForAutoconfiguration(StrategyInterface::class) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php b/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php index 1aa245f08ab68..ed92821f26138 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FeatureToggleBundle; use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass\DebugPass; -use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass\FeatureCollectionPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -20,7 +19,6 @@ final class FeatureToggleBundle extends Bundle { public function build(ContainerBuilder $container): void { - $container->addCompilerPass(new FeatureCollectionPass()); $container->addCompilerPass(new DebugPass()); } } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php index 06283075ee0a0..34beb65f86c3c 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php @@ -20,7 +20,7 @@ $services->set('feature_toggle.feature_collection', FeatureCollection::class) ->args([ - '$features' => [], + '$providers' => tagged_iterator('feature_toggle.feature_provider') ]) ; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php index cf0c4bdea8993..32c3b5316d31d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php @@ -11,12 +11,15 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Component\FeatureToggle\Provider\InMemoryProvider; +use Symfony\Component\FeatureToggle\Provider\LazyInMemoryProvider; return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('feature_toggle.provider.in_memory', InMemoryProvider::class) + $services->set('feature_toggle.provider.lazy_in_memory', LazyInMemoryProvider::class) + ->args([ + '$features' => abstract_arg('Defined in FeatureToggleExtension'), + ]) ->tag('feature_toggle.feature_provider', ['priority' => 16]) ; }; diff --git a/src/Symfony/Component/FeatureToggle/FeatureCollection.php b/src/Symfony/Component/FeatureToggle/FeatureCollection.php index 59f09352d7327..5ac212eddce3a 100644 --- a/src/Symfony/Component/FeatureToggle/FeatureCollection.php +++ b/src/Symfony/Component/FeatureToggle/FeatureCollection.php @@ -12,43 +12,33 @@ namespace Symfony\Component\FeatureToggle; use Psr\Container\ContainerInterface; +use Symfony\Component\FeatureToggle\Provider\InMemoryProvider; +use Symfony\Component\FeatureToggle\Provider\ProviderInterface; use function array_key_exists; -use function array_shift; -use function is_callable; +use function array_merge; -/** @implements \IteratorAggregate */ -final class FeatureCollection implements ContainerInterface, \IteratorAggregate +final class FeatureCollection implements ContainerInterface { /** @var array */ private array $features = []; - /** @var array|(\Closure(): iterable)> */ - private array $featureProviders = []; + /** @var iterable */ + private iterable $providers; /** - * @param iterable $features + * @param iterable $providers */ - public function __construct(iterable $features) + public function __construct(iterable $providers = []) { - $this->append($features); + $this->providers = $providers; } /** - * @param iterable|(\Closure(): iterable) $features + * @param list $features */ - private function append(iterable|\Closure $features): void + public static function withFeatures(array $features): self { - $this->featureProviders[] = $features; - } - - /** - * @param iterable|(\Closure(): iterable) $features - */ - public function withFeatures(iterable|\Closure $features): self - { - $this->append($features); - - return $this; + return new self([new InMemoryProvider($features)]); } private function findFeature(string $featureName): ?Feature @@ -57,17 +47,11 @@ private function findFeature(string $featureName): ?Feature return $this->features[$featureName]; } - while (($featureProvider = array_shift($this->featureProviders)) !== null) { - if (is_callable($featureProvider)) { - $featureProvider = $featureProvider(); - } - - foreach ($featureProvider as $feature) { + foreach ($this->providers as $provider) { + if (($feature = $provider->get($featureName)) !== null) { $this->features[$feature->getName()] = $feature; - } - if (array_key_exists($featureName, $this->features)) { - return $this->features[$featureName]; + return $feature; } } @@ -88,12 +72,17 @@ public function get(string $id): Feature } /** - * @return \Traversable + * @return list */ - public function getIterator(): \Traversable + public function names(): array { - $this->findFeature(''); + /** @var list> $namesStackedPerProvider */ + $namesStackedPerProvider = []; + + foreach ($this->providers as $provider) { + $namesStackedPerProvider[] = $provider->names(); + } - return new \ArrayIterator(array_values($this->features)); + return array_merge(...$namesStackedPerProvider); } } diff --git a/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php b/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php index dc6d385ccb2a2..a4daf52d81635 100644 --- a/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php +++ b/src/Symfony/Component/FeatureToggle/Provider/InMemoryProvider.php @@ -12,20 +12,36 @@ namespace Symfony\Component\FeatureToggle\Provider; use Symfony\Component\FeatureToggle\Feature; -use Symfony\Component\FeatureToggle\FeatureCollection; +use function array_keys; +use function array_reduce; final class InMemoryProvider implements ProviderInterface { + /** + * @var array $features + */ + private readonly array $features; + /** * @param list $features */ public function __construct( - private readonly array $features, + array $features, ) { + $this->features = array_reduce($features, static function (array $features, Feature $feature): array { + $features[$feature->getName()] = $feature; + + return $features; + }, []); + } + + public function get(string $featureName): ?Feature + { + return $this->features[$featureName] ?? null; } - public function provide(): FeatureCollection + public function names(): array { - return new FeatureCollection($this->features); + return array_keys($this->features); } } diff --git a/src/Symfony/Component/FeatureToggle/Provider/LazyInMemoryProvider.php b/src/Symfony/Component/FeatureToggle/Provider/LazyInMemoryProvider.php new file mode 100644 index 0000000000000..ce698652cfc8e --- /dev/null +++ b/src/Symfony/Component/FeatureToggle/Provider/LazyInMemoryProvider.php @@ -0,0 +1,34 @@ + $features + */ + public function __construct( + private readonly array $features, + ) { + } + + public function get(string $featureName): ?Feature + { + if (!array_key_exists($featureName, $this->features)) { + return null; + } + + return ($this->features[$featureName])(); + } + + public function names(): array + { + return array_keys($this->features); + } +} diff --git a/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php b/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php index 67ad172b10d87..6413712d5d485 100644 --- a/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php +++ b/src/Symfony/Component/FeatureToggle/Provider/ProviderInterface.php @@ -11,9 +11,14 @@ namespace Symfony\Component\FeatureToggle\Provider; -use Symfony\Component\FeatureToggle\FeatureCollection; +use Symfony\Component\FeatureToggle\Feature; interface ProviderInterface { - public function provide(): FeatureCollection; + public function get(string $featureName): ?Feature; + + /** + * @return list + */ + public function names(): array; } diff --git a/src/Symfony/Component/FeatureToggle/README.md b/src/Symfony/Component/FeatureToggle/README.md index 2c7bb88662675..3b46ce92b8587 100644 --- a/src/Symfony/Component/FeatureToggle/README.md +++ b/src/Symfony/Component/FeatureToggle/README.md @@ -25,7 +25,7 @@ use Symfony\Component\FeatureToggle\FeatureChecker; use Symfony\Component\FeatureToggle\FeatureCollection; use Symfony\Component\FeatureToggle\Strategy\RequestQueryStrategy; -$features = new FeatureCollection([ +$features = FeatureCollection::withFeatures([ new Feature( name: 'new_feature', description: 'My new feature', @@ -50,6 +50,10 @@ Available strategies **AffirmativeStrategy** : Takes a list of `StrategyInterface` and stops at the first `Grant`. +**PriorityStrategy** : Takes a list of `StrategyInterface` and stops at the first non-abstain (either `Grant` or `Deny`). + +**UnanimousStrategy** : Takes a list of `StrategyInterface` and stops at the first `Deny`. Will return `Deny` if at least one found. `Grant` If at least one found and no `Deny`. `Abstain` otherwise. + **DateStrategy** : Grant if current date is after the `$from` and before the `$until` ones. **DenyStrategy** : Always Denies. @@ -60,8 +64,6 @@ Available strategies **NotStrategy** : Takes a `StrategyInterface` and inverts its returned value (except if abstained). -**PriorityStrategy** : Takes a list of `StrategyInterface` and stops at the first non-abstain (either `Grant` or `Deny`). - **RequestHeaderStrategy** : Will look for a truthy value in the given `$name` header. **RequestQueryStrategy** : Will look for a truthy value in the given `$name` query string parameter. diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php index 7576985ec7813..05521091e335d 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureCheckerTest.php @@ -33,14 +33,14 @@ final class FeatureCheckerTest extends TestCase public function testItCorrectlyCheckTheFeaturesEvenIfNotFound(): void { $featureChecker = new FeatureChecker( - new FeatureCollection([]), + FeatureCollection::withFeatures([]), true ); self::assertTrue($featureChecker->isEnabled('not-found-1')); $featureChecker = new FeatureChecker( - new FeatureCollection([ + FeatureCollection::withFeatures([ new Feature( name: 'fake-1', description: 'Fake description 1', diff --git a/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php b/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php index d3c5d2e5ad7ae..1056e7b566f70 100644 --- a/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php +++ b/src/Symfony/Component/FeatureToggle/Tests/FeatureCollectionTest.php @@ -26,9 +26,9 @@ */ final class FeatureCollectionTest extends TestCase { - public function testEnsureItIsIterable(): void + public function testEnsureItListFeatureNames(): void { - $featureCollection = new FeatureCollection([ + $featureCollection = FeatureCollection::withFeatures([ new Feature( name: 'fake-1', description: 'Fake description 1', @@ -43,8 +43,9 @@ public function testEnsureItIsIterable(): void ), ]); - self::assertIsIterable($featureCollection); - self::assertCount(2, $featureCollection); + self::assertIsIterable($featureCollection->names()); + self::assertCount(2, $featureCollection->names()); + self::assertSame(['fake-1', 'fake-2'], $featureCollection->names()); } public function testEnsureItImplementsContainerInterface(): void @@ -52,44 +53,6 @@ public function testEnsureItImplementsContainerInterface(): void self::assertTrue(is_a(FeatureCollection::class, ContainerInterface::class, true)); } - public function testEnsureItIsMergeableWithDifferentTypesOfIterable(): void - { - $featureCollection = new FeatureCollection([ - new Feature( - name: 'fake-1', - description: 'Fake description 1', - default: true, - strategy: new GrantStrategy() - ), - new Feature( - name: 'fake-2', - description: 'Fake description 2', - default: true, - strategy: new GrantStrategy() - ), - ]); - - $featureCollection->withFeatures(function (): \Generator { - yield new Feature( - name: 'fake-3', - description: 'Fake description 3', - default: true, - strategy: new GrantStrategy() - ); - }); - - self::assertCount(3, $featureCollection); - - $featureCollection->withFeatures([new Feature( - name: 'fake-4', - description: 'Fake description 4', - default: true, - strategy: new GrantStrategy() - )]); - - self::assertCount(4, $featureCollection); - } - public function testItCanFindTheFeature(): void { $featureFake1 = new Feature( @@ -106,7 +69,7 @@ public function testItCanFindTheFeature(): void strategy: new GrantStrategy() ); - $featureCollection = new FeatureCollection([$featureFake1, $featureFake2]); + $featureCollection = FeatureCollection::withFeatures([$featureFake1, $featureFake2]); self::assertTrue($featureCollection->has('fake-1')); self::assertSame($featureFake1, $featureCollection->get('fake-1')); @@ -117,7 +80,7 @@ public function testItCanFindTheFeature(): void public function testItThrowsWhenFeatureNotFound(): void { - $featureCollection = new FeatureCollection([]); + $featureCollection = FeatureCollection::withFeatures([]); self::assertFalse($featureCollection->has('not-found-1')); From 5be52b0e17cb88eacb9960e4ff5305d10a3973f5 Mon Sep 17 00:00:00 2001 From: Adrien Roches Date: Fri, 29 Sep 2023 10:37:14 +0200 Subject: [PATCH 21/25] [UPDATE] Fix coding standards according to fabbot --- .../Bundle/FeatureToggleBundle/Resources/config/feature.php | 2 +- .../Tests/DependencyInjection/FeatureToggleExtensionTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php index 34beb65f86c3c..f2bf21dbacbfe 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php @@ -20,7 +20,7 @@ $services->set('feature_toggle.feature_collection', FeatureCollection::class) ->args([ - '$providers' => tagged_iterator('feature_toggle.feature_provider') + '$providers' => tagged_iterator('feature_toggle.feature_provider'), ]) ; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php index 23dedd73c1650..efd88947355a0 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php +++ b/src/Symfony/Bundle/FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FeatureToggleBundle\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\Configuration; use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\FeatureToggleExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; From 63f256f150edf8fe36b5cc60c01900d60013c681 Mon Sep 17 00:00:00 2001 From: Hubert Lenoir Date: Fri, 29 Sep 2023 21:57:09 +0200 Subject: [PATCH 22/25] rename to FeatureFlags (#6) --- .../.gitattributes | 0 .../.gitignore | 0 .../FeatureCheckerDataCollector.php | 38 +++++++------- .../Debug/TraceableFeatureChecker.php | 6 +-- .../Debug/TraceableStrategy.php | 8 +-- .../CompilerPass/DebugPass.php | 18 +++---- .../DependencyInjection/Configuration.php | 6 +-- .../FeatureFlagsExtension.php} | 22 ++++---- .../FeatureFlagsBundle.php} | 6 +-- .../LICENSE | 0 .../README.md | 6 +-- .../Resources/config/debug.php | 6 +-- .../Resources/config/feature.php | 16 +++--- .../Resources/config/providers.php | 8 +-- .../Resources/config/routing.php | 6 +-- .../Resources/config/strategies.php | 46 ++++++++--------- .../Resources/config/twig.php | 6 +-- .../views/Collector/profiler.html.twig | 51 +++++++++---------- .../Strategy/CustomStrategy.php | 6 +-- .../RequestStackAttributeStrategy.php | 4 +- .../Strategy/RequestStackHeaderStrategy.php | 4 +- .../Strategy/RequestStackQueryStrategy.php | 4 +- .../Strategy/RequestStackStrategy.php | 6 +-- .../DependencyInjection/ConfigurationTest.php | 10 ++-- .../FeatureFlagsExtensionTest.php} | 20 ++++---- .../Twig/FeatureEnabledExtension.php | 4 +- .../composer.json | 6 +-- .../phpunit.xml.dist | 2 +- .../.gitattributes | 0 .../.gitignore | 0 .../Feature.php | 4 +- .../FeatureChecker.php | 2 +- .../FeatureCheckerInterface.php | 2 +- .../FeatureCollection.php | 6 +-- .../FeatureNotFoundException.php | 2 +- .../{FeatureToggle => FeatureFlags}/LICENSE | 0 .../Provider/InMemoryProvider.php | 4 +- .../Provider/LazyInMemoryProvider.php | 4 +- .../Provider/ProviderInterface.php | 4 +- .../{FeatureToggle => FeatureFlags}/README.md | 14 ++--- .../Strategy/AffirmativeStrategy.php | 4 +- .../Strategy/DateStrategy.php | 4 +- .../Strategy/DenyStrategy.php | 4 +- .../Strategy/EnvStrategy.php | 4 +- .../Strategy/GrantStrategy.php | 4 +- .../Strategy/NotStrategy.php | 4 +- .../Strategy/PriorityStrategy.php | 4 +- .../Strategy/RequestHeaderStrategy.php | 4 +- .../Strategy/RequestQueryStrategy.php | 4 +- .../Strategy/StrategyInterface.php | 4 +- .../Strategy/UnanimousStrategy.php | 4 +- .../StrategyResult.php | 2 +- .../Tests/FeatureCheckerTest.php | 26 +++++----- .../Tests/FeatureCollectionTest.php | 16 +++--- .../Tests/FeatureTest.php | 12 ++--- .../AbstractOuterStrategiesTestCase.php | 6 +-- .../Strategy/AffirmativeStrategyTest.php | 10 ++-- .../Tests/Strategy/DateStrategyTest.php | 8 +-- .../Tests/Strategy/PriorityStrategyTest.php | 10 ++-- .../Tests/Strategy/UnanimousStrategyTest.php | 10 ++-- .../composer.json | 4 +- .../phpunit.xml.dist | 2 +- 62 files changed, 253 insertions(+), 254 deletions(-) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/.gitattributes (100%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/.gitignore (100%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/DataCollector/FeatureCheckerDataCollector.php (72%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Debug/TraceableFeatureChecker.php (80%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Debug/TraceableStrategy.php (76%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/DependencyInjection/CompilerPass/DebugPass.php (58%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/DependencyInjection/Configuration.php (96%) rename src/Symfony/Bundle/{FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php => FeatureFlagsBundle/DependencyInjection/FeatureFlagsExtension.php} (86%) rename src/Symfony/Bundle/{FeatureToggleBundle/FeatureToggleBundle.php => FeatureFlagsBundle/FeatureFlagsBundle.php} (73%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/LICENSE (100%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/README.md (83%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/config/debug.php (59%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/config/feature.php (53%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/config/providers.php (59%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/config/routing.php (77%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/config/strategies.php (51%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/config/twig.php (69%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Resources/views/Collector/profiler.html.twig (51%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Strategy/CustomStrategy.php (74%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Strategy/RequestStackAttributeStrategy.php (88%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Strategy/RequestStackHeaderStrategy.php (88%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Strategy/RequestStackQueryStrategy.php (88%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Strategy/RequestStackStrategy.php (88%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Tests/DependencyInjection/ConfigurationTest.php (86%) rename src/Symfony/Bundle/{FeatureToggleBundle/Tests/DependencyInjection/FeatureToggleExtensionTest.php => FeatureFlagsBundle/Tests/DependencyInjection/FeatureFlagsExtensionTest.php} (87%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/Twig/FeatureEnabledExtension.php (85%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/composer.json (83%) rename src/Symfony/Bundle/{FeatureToggleBundle => FeatureFlagsBundle}/phpunit.xml.dist (91%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/.gitattributes (100%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/.gitignore (100%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Feature.php (89%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/FeatureChecker.php (94%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/FeatureCheckerInterface.php (88%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/FeatureCollection.php (92%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/FeatureNotFoundException.php (91%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/LICENSE (100%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Provider/InMemoryProvider.php (91%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Provider/LazyInMemoryProvider.php (86%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Provider/ProviderInterface.php (80%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/README.md (88%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/AffirmativeStrategy.php (90%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/DateStrategy.php (93%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/DenyStrategy.php (79%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/EnvStrategy.php (86%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/GrantStrategy.php (79%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/NotStrategy.php (87%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/PriorityStrategy.php (88%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/RequestHeaderStrategy.php (87%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/RequestQueryStrategy.php (87%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/StrategyInterface.php (75%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Strategy/UnanimousStrategy.php (90%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/StrategyResult.php (87%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/FeatureCheckerTest.php (72%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/FeatureCollectionTest.php (84%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/FeatureTest.php (88%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/Strategy/AbstractOuterStrategiesTestCase.php (81%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/Strategy/AffirmativeStrategyTest.php (88%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/Strategy/DateStrategyTest.php (97%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/Strategy/PriorityStrategyTest.php (89%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/Tests/Strategy/UnanimousStrategyTest.php (88%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/composer.json (86%) rename src/Symfony/Component/{FeatureToggle => FeatureFlags}/phpunit.xml.dist (91%) diff --git a/src/Symfony/Bundle/FeatureToggleBundle/.gitattributes b/src/Symfony/Bundle/FeatureFlagsBundle/.gitattributes similarity index 100% rename from src/Symfony/Bundle/FeatureToggleBundle/.gitattributes rename to src/Symfony/Bundle/FeatureFlagsBundle/.gitattributes diff --git a/src/Symfony/Bundle/FeatureToggleBundle/.gitignore b/src/Symfony/Bundle/FeatureFlagsBundle/.gitignore similarity index 100% rename from src/Symfony/Bundle/FeatureToggleBundle/.gitignore rename to src/Symfony/Bundle/FeatureFlagsBundle/.gitignore diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php b/src/Symfony/Bundle/FeatureFlagsBundle/DataCollector/FeatureCheckerDataCollector.php similarity index 72% rename from src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php rename to src/Symfony/Bundle/FeatureFlagsBundle/DataCollector/FeatureCheckerDataCollector.php index 022ab3bd90bf4..4644311b74966 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DataCollector/FeatureCheckerDataCollector.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/DataCollector/FeatureCheckerDataCollector.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle\DataCollector; +namespace Symfony\Bundle\FeatureFlagsBundle\DataCollector; -use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Component\FeatureFlags\StrategyResult; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; @@ -21,7 +21,7 @@ /** * @property Data|array{ - * toggles: array */ - private \SplStack $currentToggle; + private \SplStack $currentCheck; /** @var \SplStack */ private \SplStack $currentCompute; public function __construct() { - $this->data = ['toggles' => []]; - $this->currentToggle = new \SplStack(); + $this->data = ['checks' => []]; + $this->currentCheck = new \SplStack(); $this->currentCompute = new \SplStack(); } @@ -54,14 +54,14 @@ public function collect(Request $request, Response $response, \Throwable $except public function collectIsEnabledStart(string $featureName): void { - $toggleId = uniqid(); + $checkId = uniqid(); - $this->data['toggles'][$toggleId] = [ + $this->data['checks'][$checkId] = [ 'feature' => $featureName, 'computes' => [], 'result' => null, ]; - $this->currentToggle->push($toggleId); + $this->currentCheck->push($checkId); } /** @@ -69,11 +69,11 @@ public function collectIsEnabledStart(string $featureName): void */ public function collectComputeStart(string $strategyId, string $strategyClass): void { - $toggleId = $this->currentToggle->top(); + $checkId = $this->currentCheck->top(); $computeId = uniqid(); $level = $this->currentCompute->count(); - $this->data['toggles'][$toggleId]['computes'][$computeId] = [ + $this->data['checks'][$checkId]['computes'][$computeId] = [ 'strategyId' => $strategyId, 'strategyClass' => new ClassStub($strategyClass), 'level' => $level, @@ -84,28 +84,28 @@ public function collectComputeStart(string $strategyId, string $strategyClass): public function collectComputeStop(StrategyResult $result): void { - $toggleId = $this->currentToggle->top(); + $checkId = $this->currentCheck->top(); $computeId = $this->currentCompute->pop(); - $this->data['toggles'][$toggleId]['computes'][$computeId]['result'] = $result; + $this->data['checks'][$checkId]['computes'][$computeId]['result'] = $result; } public function collectIsEnabledStop(bool $result): void { - $toggleId = $this->currentToggle->pop(); + $checkId = $this->currentCheck->pop(); - $this->data['toggles'][$toggleId]['result'] = $result; + $this->data['checks'][$checkId]['result'] = $result; } public function getName(): string { - return 'feature_toggle'; + return 'feature_flags'; } public function reset(): void { $this->data = [ - 'toggles' => [], + 'checks' => [], ]; } @@ -114,8 +114,8 @@ public function lateCollect(): void $this->data = $this->cloneVar($this->data); } - public function getToggles(): array|Data + public function getChecks(): array|Data { - return $this->data['toggles']; + return $this->data['checks']; } } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableFeatureChecker.php b/src/Symfony/Bundle/FeatureFlagsBundle/Debug/TraceableFeatureChecker.php similarity index 80% rename from src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableFeatureChecker.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Debug/TraceableFeatureChecker.php index 727be093a0c4d..0877aa2307c19 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableFeatureChecker.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Debug/TraceableFeatureChecker.php @@ -9,10 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle\Debug; +namespace Symfony\Bundle\FeatureFlagsBundle\Debug; -use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; -use Symfony\Component\FeatureToggle\FeatureCheckerInterface; +use Symfony\Bundle\FeatureFlagsBundle\DataCollector\FeatureCheckerDataCollector; +use Symfony\Component\FeatureFlags\FeatureCheckerInterface; final class TraceableFeatureChecker implements FeatureCheckerInterface { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableStrategy.php b/src/Symfony/Bundle/FeatureFlagsBundle/Debug/TraceableStrategy.php similarity index 76% rename from src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableStrategy.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Debug/TraceableStrategy.php index b4564fbd6a88d..79f8a098e60f9 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Debug/TraceableStrategy.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Debug/TraceableStrategy.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle\Debug; +namespace Symfony\Bundle\FeatureFlagsBundle\Debug; -use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; -use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; -use Symfony\Component\FeatureToggle\StrategyResult; +use Symfony\Bundle\FeatureFlagsBundle\DataCollector\FeatureCheckerDataCollector; +use Symfony\Component\FeatureFlags\Strategy\StrategyInterface; +use Symfony\Component\FeatureFlags\StrategyResult; final class TraceableStrategy implements StrategyInterface { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php b/src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/CompilerPass/DebugPass.php similarity index 58% rename from src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php rename to src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/CompilerPass/DebugPass.php index b41a516f3b225..12f41779ea27f 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/CompilerPass/DebugPass.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/CompilerPass/DebugPass.php @@ -9,10 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass; +namespace Symfony\Bundle\FeatureFlagsBundle\DependencyInjection\CompilerPass; -use Symfony\Bundle\FeatureToggleBundle\Debug\TraceableFeatureChecker; -use Symfony\Bundle\FeatureToggleBundle\Debug\TraceableStrategy; +use Symfony\Bundle\FeatureFlagsBundle\Debug\TraceableFeatureChecker; +use Symfony\Bundle\FeatureFlagsBundle\Debug\TraceableStrategy; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -21,25 +21,25 @@ final class DebugPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - if (!$container->has('feature_toggle.data_collector')) { + if (!$container->has('feature_flags.data_collector')) { return; } - $container->register('debug.feature_toggle.feature_checker', TraceableFeatureChecker::class) - ->setDecoratedService('feature_toggle.feature_checker') + $container->register('debug.feature_flags.feature_checker', TraceableFeatureChecker::class) + ->setDecoratedService('feature_flags.feature_checker') ->setArguments([ '$featureChecker' => new Reference('.inner'), - '$dataCollector' => new Reference('feature_toggle.data_collector'), + '$dataCollector' => new Reference('feature_flags.data_collector'), ]) ; - foreach ($container->findTaggedServiceIds('feature_toggle.feature_strategy') as $serviceId => $tags) { + foreach ($container->findTaggedServiceIds('feature_flags.feature_strategy') as $serviceId => $tags) { $container->register('debug.'.$serviceId, TraceableStrategy::class) ->setDecoratedService($serviceId) ->setArguments([ '$strategy' => new Reference('.inner'), '$strategyId' => $serviceId, - '$dataCollector' => new Reference('feature_toggle.data_collector'), + '$dataCollector' => new Reference('feature_flags.data_collector'), ]) ; } diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/Configuration.php similarity index 96% rename from src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php rename to src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/Configuration.php index 76db2cd43f8c9..49d114fc8bdc1 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/Configuration.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection; +namespace Symfony\Bundle\FeatureFlagsBundle\DependencyInjection; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -20,7 +20,7 @@ final class Configuration implements ConfigurationInterface public function getConfigTreeBuilder(): TreeBuilder { - $treeBuilder = new TreeBuilder('feature_toggle'); + $treeBuilder = new TreeBuilder('feature_flags'); $treeBuilder->getRootNode() // @phpstan-ignore-line ->children() // strategies @@ -134,7 +134,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->isRequired() ->cannotBeEmpty() ->example('header.feature-strategy') - ->info('Strategy to be used for this feature. Can be one of "feature_toggle.strategies[].name" or a valid service id that implements StrategyInterface::class.') + ->info('Strategy to be used for this feature. Can be one of "feature_flags.strategies[].name" or a valid service id that implements StrategyInterface::class.') ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php b/src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/FeatureFlagsExtension.php similarity index 86% rename from src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php rename to src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/FeatureFlagsExtension.php index 0215369a379a6..583302c36fe8d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/DependencyInjection/FeatureToggleExtension.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/DependencyInjection/FeatureFlagsExtension.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle\DependencyInjection; +namespace Symfony\Bundle\FeatureFlagsBundle\DependencyInjection; -use Symfony\Bundle\FeatureToggleBundle\Strategy\CustomStrategy; +use Symfony\Bundle\FeatureFlagsBundle\Strategy\CustomStrategy; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -19,19 +19,19 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\FeatureToggle\Feature; -use Symfony\Component\FeatureToggle\Provider\ProviderInterface; -use Symfony\Component\FeatureToggle\Strategy\StrategyInterface; +use Symfony\Component\FeatureFlags\Feature; +use Symfony\Component\FeatureFlags\Provider\ProviderInterface; +use Symfony\Component\FeatureFlags\Strategy\StrategyInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\Router; use Twig\Environment; -final class FeatureToggleExtension extends Extension +final class FeatureFlagsExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $container->registerForAutoconfiguration(ProviderInterface::class) - ->addTag('feature_toggle.feature_provider') + ->addTag('feature_flags.feature_provider') ; $config = $this->processConfiguration(new Configuration(), $configs); @@ -74,7 +74,7 @@ private function loadFeatures(ContainerBuilder $container, array $config): void ; } - $container->getDefinition('feature_toggle.provider.lazy_in_memory') + $container->getDefinition('feature_flags.provider.lazy_in_memory') ->setArgument('$features', $features) ; } @@ -82,12 +82,12 @@ private function loadFeatures(ContainerBuilder $container, array $config): void private function loadStrategies(ContainerBuilder $container, array $config): void { $container->registerForAutoconfiguration(StrategyInterface::class) - ->addTag('feature_toggle.feature_strategy') + ->addTag('feature_flags.feature_strategy') ; foreach ($config['strategies'] as $strategyName => $strategyConfig) { $container->setDefinition($strategyName, $this->generateStrategy($strategyConfig['type'], $strategyConfig['with'])) - ->addTag('feature_toggle.feature_strategy'); + ->addTag('feature_flags.feature_strategy'); } } @@ -96,7 +96,7 @@ private function loadStrategies(ContainerBuilder $container, array $config): voi */ private function generateStrategy(string $type, array $with): Definition { - $definition = new ChildDefinition("feature_toggle.abstract_strategy.{$type}"); + $definition = new ChildDefinition("feature_flags.abstract_strategy.{$type}"); return match ($type) { 'date' => $definition->setArguments([ diff --git a/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php b/src/Symfony/Bundle/FeatureFlagsBundle/FeatureFlagsBundle.php similarity index 73% rename from src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php rename to src/Symfony/Bundle/FeatureFlagsBundle/FeatureFlagsBundle.php index ed92821f26138..fca5656829ff2 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/FeatureToggleBundle.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/FeatureFlagsBundle.php @@ -9,13 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FeatureToggleBundle; +namespace Symfony\Bundle\FeatureFlagsBundle; -use Symfony\Bundle\FeatureToggleBundle\DependencyInjection\CompilerPass\DebugPass; +use Symfony\Bundle\FeatureFlagsBundle\DependencyInjection\CompilerPass\DebugPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; -final class FeatureToggleBundle extends Bundle +final class FeatureFlagsBundle extends Bundle { public function build(ContainerBuilder $container): void { diff --git a/src/Symfony/Bundle/FeatureToggleBundle/LICENSE b/src/Symfony/Bundle/FeatureFlagsBundle/LICENSE similarity index 100% rename from src/Symfony/Bundle/FeatureToggleBundle/LICENSE rename to src/Symfony/Bundle/FeatureFlagsBundle/LICENSE diff --git a/src/Symfony/Bundle/FeatureToggleBundle/README.md b/src/Symfony/Bundle/FeatureFlagsBundle/README.md similarity index 83% rename from src/Symfony/Bundle/FeatureToggleBundle/README.md rename to src/Symfony/Bundle/FeatureFlagsBundle/README.md index 1a6ac2bbf05e1..fc80290e548fe 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/README.md +++ b/src/Symfony/Bundle/FeatureFlagsBundle/README.md @@ -1,7 +1,7 @@ -FeatureToggleBundle -=================== +FeatureFlagsBundle +================== -FeatureToggleBundle provides a tight integration of the Symfony ToggleFeature +FeatureFlagsBundle provides a tight integration of the Symfony FeatureFlags component into the Symfony full-stack framework. **This Bundle is experimental**. diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/debug.php similarity index 59% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/debug.php index df062b8c9486d..8606ccd3f1696 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/debug.php @@ -11,12 +11,12 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Bundle\FeatureToggleBundle\DataCollector\FeatureCheckerDataCollector; +use Symfony\Bundle\FeatureFlagsBundle\DataCollector\FeatureCheckerDataCollector; return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('feature_toggle.data_collector', FeatureCheckerDataCollector::class) - ->tag('data_collector', ['template' => '@FeatureToggle/Collector/profiler.html.twig', 'id' => 'feature_toggle']) + $services->set('feature_flags.data_collector', FeatureCheckerDataCollector::class) + ->tag('data_collector', ['template' => '@FeatureFlags/Collector/profiler.html.twig', 'id' => 'feature_flags']) ; }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/feature.php similarity index 53% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/feature.php index f2bf21dbacbfe..4f22f939b0bc7 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/feature.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/feature.php @@ -11,24 +11,24 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Component\FeatureToggle\FeatureChecker; -use Symfony\Component\FeatureToggle\FeatureCheckerInterface; -use Symfony\Component\FeatureToggle\FeatureCollection; +use Symfony\Component\FeatureFlags\FeatureChecker; +use Symfony\Component\FeatureFlags\FeatureCheckerInterface; +use Symfony\Component\FeatureFlags\FeatureCollection; return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('feature_toggle.feature_collection', FeatureCollection::class) + $services->set('feature_flags.feature_collection', FeatureCollection::class) ->args([ - '$providers' => tagged_iterator('feature_toggle.feature_provider'), + '$providers' => tagged_iterator('feature_flags.feature_provider'), ]) ; - $services->set('feature_toggle.feature_checker', FeatureChecker::class) + $services->set('feature_flags.feature_checker', FeatureChecker::class) ->args([ - '$features' => service('feature_toggle.feature_collection'), + '$features' => service('feature_flags.feature_collection'), '$whenNotFound' => false, ]) ; - $services->alias(FeatureCheckerInterface::class, service('feature_toggle.feature_checker')); + $services->alias(FeatureCheckerInterface::class, service('feature_flags.feature_checker')); }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/providers.php similarity index 59% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/providers.php index 32c3b5316d31d..34d46222a153d 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/providers.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/providers.php @@ -11,15 +11,15 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Component\FeatureToggle\Provider\LazyInMemoryProvider; +use Symfony\Component\FeatureFlags\Provider\LazyInMemoryProvider; return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('feature_toggle.provider.lazy_in_memory', LazyInMemoryProvider::class) + $services->set('feature_flags.provider.lazy_in_memory', LazyInMemoryProvider::class) ->args([ - '$features' => abstract_arg('Defined in FeatureToggleExtension'), + '$features' => abstract_arg('Defined in FeatureFlagsExtension'), ]) - ->tag('feature_toggle.feature_provider', ['priority' => 16]) + ->tag('feature_flags.feature_provider', ['priority' => 16]) ; }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/routing.php similarity index 77% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/routing.php index 1751796d25fec..0eb4baab74662 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/routing.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/routing.php @@ -14,15 +14,15 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('feature_toggle.routing.provider', \Closure::class) + $services->set('feature_flags.routing.provider', \Closure::class) ->factory([\Closure::class, 'fromCallable']) ->args([ - [service('feature_toggle.feature_checker'), 'isEnabled'], + [service('feature_flags.feature_checker'), 'isEnabled'], ]) ->tag('routing.expression_language_function', ['function' => 'isFeatureEnabled']) ; - $services->get('feature_toggle.feature_checker') + $services->get('feature_flags.feature_checker') ->tag('routing.condition_service', ['alias' => 'feature']) ; }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/strategies.php similarity index 51% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/strategies.php index a98329252534d..14ac8fbac1754 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/strategies.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/strategies.php @@ -11,51 +11,51 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackAttributeStrategy; -use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackHeaderStrategy; -use Symfony\Bundle\FeatureToggleBundle\Strategy\RequestStackQueryStrategy; -use Symfony\Component\FeatureToggle\Strategy\AffirmativeStrategy; -use Symfony\Component\FeatureToggle\Strategy\DateStrategy; -use Symfony\Component\FeatureToggle\Strategy\EnvStrategy; -use Symfony\Component\FeatureToggle\Strategy\GrantStrategy; -use Symfony\Component\FeatureToggle\Strategy\NotStrategy; -use Symfony\Component\FeatureToggle\Strategy\PriorityStrategy; -use Symfony\Component\FeatureToggle\Strategy\UnanimousStrategy; +use Symfony\Bundle\FeatureFlagsBundle\Strategy\RequestStackAttributeStrategy; +use Symfony\Bundle\FeatureFlagsBundle\Strategy\RequestStackHeaderStrategy; +use Symfony\Bundle\FeatureFlagsBundle\Strategy\RequestStackQueryStrategy; +use Symfony\Component\FeatureFlags\Strategy\AffirmativeStrategy; +use Symfony\Component\FeatureFlags\Strategy\DateStrategy; +use Symfony\Component\FeatureFlags\Strategy\EnvStrategy; +use Symfony\Component\FeatureFlags\Strategy\GrantStrategy; +use Symfony\Component\FeatureFlags\Strategy\NotStrategy; +use Symfony\Component\FeatureFlags\Strategy\PriorityStrategy; +use Symfony\Component\FeatureFlags\Strategy\UnanimousStrategy; return static function (ContainerConfigurator $container) { - $prefix = 'feature_toggle.abstract_strategy.'; + $prefix = 'feature_flags.abstract_strategy.'; $services = $container->services(); $services->set($prefix.'grant', GrantStrategy::class)->abstract(); $services->set($prefix.'not', NotStrategy::class)->abstract()->args([ - '$inner' => abstract_arg('Defined in FeatureToggleExtension'), + '$inner' => abstract_arg('Defined in FeatureFlagsExtension'), ]); $services->set($prefix.'env', EnvStrategy::class)->abstract()->args([ - '$envName' => abstract_arg('Defined in FeatureToggleExtension'), + '$envName' => abstract_arg('Defined in FeatureFlagsExtension'), ]); $services->set($prefix.'date', DateStrategy::class)->abstract()->args([ - '$since' => abstract_arg('Defined in FeatureToggleExtension'), - '$until' => abstract_arg('Defined in FeatureToggleExtension'), - '$includeSince' => abstract_arg('Defined in FeatureToggleExtension'), - '$includeUntil' => abstract_arg('Defined in FeatureToggleExtension'), + '$since' => abstract_arg('Defined in FeatureFlagsExtension'), + '$until' => abstract_arg('Defined in FeatureFlagsExtension'), + '$includeSince' => abstract_arg('Defined in FeatureFlagsExtension'), + '$includeUntil' => abstract_arg('Defined in FeatureFlagsExtension'), '$clock' => service('clock')->nullOnInvalid(), ]); $services->set($prefix.'request_attribute', RequestStackAttributeStrategy::class)->abstract()->args([ - '$attributeName' => abstract_arg('Defined in FeatureToggleExtension'), + '$attributeName' => abstract_arg('Defined in FeatureFlagsExtension'), ])->call('setRequestStack', [service('request_stack')->nullOnInvalid()]); $services->set($prefix.'request_header', RequestStackHeaderStrategy::class)->abstract()->args([ - '$headerName' => abstract_arg('Defined in FeatureToggleExtension'), + '$headerName' => abstract_arg('Defined in FeatureFlagsExtension'), ])->call('setRequestStack', [service('request_stack')->nullOnInvalid()]); $services->set($prefix.'request_query', RequestStackQueryStrategy::class)->abstract()->args([ - '$queryParameterName' => abstract_arg('Defined in FeatureToggleExtension'), + '$queryParameterName' => abstract_arg('Defined in FeatureFlagsExtension'), ])->call('setRequestStack', [service('request_stack')->nullOnInvalid()]); $services->set($prefix.'priority', PriorityStrategy::class)->abstract()->args([ - '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), + '$strategies' => abstract_arg('Defined in FeatureFlagsExtension'), ]); $services->set($prefix.'affirmative', AffirmativeStrategy::class)->abstract()->args([ - '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), + '$strategies' => abstract_arg('Defined in FeatureFlagsExtension'), ]); $services->set($prefix.'unanimous', UnanimousStrategy::class)->abstract()->args([ - '$strategies' => abstract_arg('Defined in FeatureToggleExtension'), + '$strategies' => abstract_arg('Defined in FeatureFlagsExtension'), ]); }; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/twig.php similarity index 69% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/twig.php index 737816f4656a8..6126757304ff5 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/config/twig.php +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/config/twig.php @@ -11,14 +11,14 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Bundle\FeatureToggleBundle\Twig\FeatureEnabledExtension; +use Symfony\Bundle\FeatureFlagsBundle\Twig\FeatureEnabledExtension; return static function (ContainerConfigurator $container) { $services = $container->services(); - $services->set('feature_toggle.twig_extension', FeatureEnabledExtension::class) + $services->set('feature_flags.twig_extension', FeatureEnabledExtension::class) ->args([ - service('feature_toggle.feature_checker'), + service('feature_flags.feature_checker'), ]) ->tag('twig.extension') ; diff --git a/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/views/Collector/profiler.html.twig similarity index 51% rename from src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig rename to src/Symfony/Bundle/FeatureFlagsBundle/Resources/views/Collector/profiler.html.twig index f2b03e5e2340b..19b5c7f8d2c4b 100644 --- a/src/Symfony/Bundle/FeatureToggleBundle/Resources/views/Collector/profiler.html.twig +++ b/src/Symfony/Bundle/FeatureFlagsBundle/Resources/views/Collector/profiler.html.twig @@ -1,22 +1,21 @@ {% extends '@WebProfiler/Profiler/layout.html.twig' %} -{% block page_title 'Feature Toggle' %} +{% block page_title 'Feature Flags' %} {% block toolbar %} - {% if 0 < collector.toggles|length %} + {% if 0 < collector.checks|length %} {% set icon %} - Feature Toggle - {{ collector.toggles|length }} - Toggles + Feature Flags + {{ collector.checks|length }} {% endset %} {% set text %}
- Toggles - {% for toggle in collector.toggles %} - - {{ toggle['feature'] }} + Checks + {% for check in collector.checks %} + + {{ check['feature'] }} {% endfor %}
@@ -28,19 +27,19 @@ {% endblock %} {% block menu %} - - Feature Toggle - Feature toggle + + Feature Flags + Feature Flags {% endblock %} {% block head %} {{ parent() }}