8000 [FeatureFlag] Add ArgumentResolver by Jean-Beru · Pull Request #8 · Jean-Beru/symfony · GitHub
[go: up one dir, main page]

Skip to content

[FeatureFlag] Add ArgumentResolver #8

New issue 8000

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: rfc/simple-feature-flag
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ccbde2d
[FeatureFlag] Propose a simple version
Jean-Beru Dec 7, 2023
f789984
extract FrameworkBundle integration
Jean-Beru Dec 26, 2023
a4bfd4e
fix typos, doc and default feature value
Jean-Beru Dec 29, 2023
88280a8
review
Jean-Beru Jan 2, 2024
e3b5757
add some tests
Jean-Beru Jan 2, 2024
8e8f1e3
happy new year
Jean-Beru Jan 2, 2024
004e76d
cs
Jean-Beru Jan 2, 2024
93570fc
add base exception
Jean-Beru Jan 3, 2024
05df4d9
refact DataCollector
Jean-Beru Jan 3, 2024
3460e97
add isDisabled
Jean-Beru Jan 3, 2024
2634480
fix collector
Jean-Beru Jan 4, 2024
7854cd9
fix forgotten method in FeatureCheckerInterface
Jean-Beru Jan 4, 2024
ff279ab
fix interface
Jean-Beru Jan 4, 2024
72b893b
remove exception catching
Jean-Beru Jan 29, 2024
d343107
[FeatureFlag] Add FrameworkBundle integration
Jean-Beru Dec 26, 2023
07452ca
exception format
Jean-Beru Feb 29, 2024
c83e651
cs
Jean-Beru Feb 29, 2024
cac6b22
add Neirda24 as co-author
Jean-Beru Mar 1, 2024
b437a73
add ContainerInterface dependency
Jean-Beru Mar 1, 2024
9f8a4fd
fix exception namespace
Jean-Beru Mar 1, 2024
71b5558
fix namespace again
Jean-Beru Mar 1, 2024
1b7ba66
remove merge file
Jean-Beru Mar 1, 2024
bd9ad89
Fix bundle and add details in profiler
Jean-Beru Apr 5, 2024
36394ec
Add Twig functions to UndefinedCallableHandler::FUNCTION_COMPONENTS
Jean-Beru Apr 5, 2024
1b62db2
cs
Jean-Beru Apr 5, 2024
5a87737
rebase
Jean-Beru Apr 5, 2024
fce994a
review
Jean-Beru Apr 5, 2024
222a22b
cs
Jean-Beru Apr 5, 2024
997fc4a
tests
Jean-Beru Apr 5, 2024
ae25ba3
psalm
Jean-Beru Apr 5, 2024
f11eef5
readme
Jean-Beru Apr 12, 2024
e1a7dda
add functional tests
Jean-Beru Apr 12, 2024
4e37306
fix framework bundle test
Jean-Beru Apr 16, 2024
e55798f
[FeatureFlag] Use providers
Jean-Beru Oct 8, 2024
a99c32c
cs
Jean-Beru Oct 9, 2024
5e44fa4
[FeatureFlag] Add ArgumentResolver
Jean-Beru Oct 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"symfony/error-handler": "self.version",
"symfony/event-dispatcher": "self.version",
"symfony/expression-language": "self.version",
"symfony/feature-flag": "self.version",
"symfony/filesystem": "self.version",
"symfony/finder": "self.version",
"symfony/form": "self.version",
Expand Down
26 changes: 26 additions & 0 deletions src/Symfony/Bridge/Twig/Extension/FeatureFlagExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Twig\Extension;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class FeatureFlagExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('feature_is_enabled', [FeatureFlagRuntime::class, 'isEnabled']),
new TwigFunction('feature_get_value', [FeatureFlagRuntime::class, 'getValue']),
];
}
}
39 changes: 39 additions & 0 deletions src/Symfony/Bridge/Twig/Extension/FeatureFlagRuntime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bridge\Twig\Extension;

use Symfony\Component\FeatureFlag\FeatureCheckerInterface;

final class FeatureFlagRuntime
{
public function __construct(private readonly ?FeatureCheckerInterface $featureChecker = null)
{
}

public function isEnabled(string $featureName, mixed $expectedValue = true): bool
{
if (null === $this->featureChecker) {
throw new \LogicException(\sprintf('An instance of "%s" must be provided to use "%s()".', FeatureCheckerInterface::class, __METHOD__));
}

return $this->featureChecker->isEnabled($featureName, $expectedValue);
}

public function getValue(string $featureName): mixed
{
if (null === $this->featureChecker) {
throw new \LogicException(\sprintf('An instance of "%s" must be provided to use "%s()".', FeatureCheckerInterface::class, __METHOD__));
}

return $this->featureChecker->getValue($featureName);
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Bridge/Twig/UndefinedCallableHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class UndefinedCallableHandler
'field_choices' => 'form',
'logout_url' => 'security-http',
'logout_path' => 'security-http',
'feature_get_value' => 'feature-flag',
'feature_is_enabled' => 'feature-flag',
'is_granted' => 'security-core',
'impersonation_path' => 'security-http',
'impersonation_url' => 'security-http',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\FeatureFlag\Debug\TraceableFeatureChecker;

class FeatureFlagPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('feature_flag.feature_checker')) {
return;
}

$features = [];
foreach ($container->findTaggedServiceIds('feature_flag.feature') as $serviceId => $tags) {
$className = $this->getServiceClass($container, $serviceId);
$r = $container->getReflectionClass($className);

if (null === $r) {
throw new \RuntimeException(\sprintf('Invalid service "%s": class "%s" does not exist.', $serviceId, $className));
}

foreach ($tags as $tag) {
$featureName = ($tag['feature'] ?? '') ?: $className;
if (\array_key_exists($featureName, $features)) {
throw new \RuntimeException(\sprintf('Feature "%s" already defined.', $featureName));
}

$method = $tag['method'] ?? '__invoke';
if (!$r->hasMethod($method)) {
throw new \RuntimeException(\sprintf('Invalid feature method "%s": method "%s::%s()" does not exist.', $serviceId, $r->getName(), $method));
}

$features[$featureName] = $container->setDefinition(
'.feature_flag.feature',
(new Definition(\Closure::class))
->setLazy(true)
->setFactory([\Closure::class, 'fromCallable'])
->setArguments([[new Reference($serviceId), $method]]),
);
}
}

$container->getDefinition('feature_flag.provider.in_memory')
->setArgument('$features', $features)
;

if (!$container->has('feature_flag.data_collector')) {
return;
}

foreach ($container->findTaggedServiceIds('feature_flag.feature_checker') as $serviceId => $tags) {
$container->register('debug.'.$serviceId, TraceableFeatureChecker::class)
->setDecoratedService($serviceId)
->setArguments([
'$decorated' => new Reference('.inner'),
'$dataCollector' => new Reference('feature_flag.data_collector'),
])
;
}
}

private function getServiceClass(ContainerBuilder $container, string $serviceId): ?string
{
while (true) {
$definition = $container->findDefinition($serviceId);

if (!$definition->getClass() && $definition instanceof ChildDefinition) {
$serviceId = $definition->getParent();

continue;
}

return $definition->getClass();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class UnusedTagsPass implements CompilerPassInterface
'controller.targeted_value_resolver',
'data_collector',
'event_dispatcher.dispatcher',
'feature_flag.argument_value_resolver',
'feature_flag.feature',
'feature_flag.feature_checker',
'feature_flag.provider',
'form.type',
'form.type_extension',
'form.type_guesser',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\FeatureFlag\FeatureCheckerInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;
use Symfony\Component\HttpClient\HttpClient;
Expand Down Expand Up @@ -180,6 +181,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$this->addHtmlSanitizerSection($rootNode, $enableIfStandalone);
$this->addWebhookSection($rootNode, $enableIfStandalone);
$this->addRemoteEventSection($rootNode, $enableIfStandalone);
$this->addFeatureFlagSection($rootNode, $enableIfStandalone);

return $treeBuilder;
}
Expand Down Expand Up @@ -2546,4 +2548,16 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable
->end()
;
}

private function addFeatureFlagSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
{
$rootNode
->children()
->arrayNode('feature_flag')
->info('FeatureFlag configuration')
->{$enableIfStandalone('symfony/feature-flag', FeatureCheckerInterface::class)}()
->fixXmlConfig('feature_flag')
->end()
->end();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\FeatureFlag\Attribute\AsFeature;
use Symfony\Component\FeatureFlag\FeatureChecker;
use Symfony\Component\FeatureFlag\Provider\ProviderInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\Glob;
Expand Down Expand Up @@ -142,6 +145,7 @@
use Symfony\Component\RateLimiter\Storage\CacheStorage;
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Routing\Router;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
Expand Down Expand Up @@ -273,6 +277,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->readConfigEnabled('property_access', $container, $config['property_access']);
$this->readConfigEnabled('profiler', $container, $config['profiler']);
$this->readConfigEnabled('workflows', $container, $config['workflows']);
$this->readConfigEnabled('feature_flag', $container, $config['feature_flag']);

// A translator must always be registered (as support is included by
// default in the Form and Validator component). If disabled, an identity
Expand Down Expand Up @@ -575,6 +580,13 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerHtmlSanitizerConfiguration($config['html_sanitizer'], $container, $loader);
}

if ($this->readConfigEnabled('feature_flag', $container, $config['feature_flag'])) {
if (!class_exists(FeatureChecker::class)) {
throw new LogicException('FeatureFlag support cannot be enabled as the FeatureFlag component is not installed. Try running "composer require symfony/feature-flag".');
}
$this->registerFeatureFlagConfiguration($config['feature_flag'], $container, $loader);
}

if (ContainerBuilder::willBeAvailable('symfony/mime', MimeTypes::class, ['symfony/framework-bundle'])) {
$loader->load('mime_type.php');
}
Expand Down Expand Up @@ -890,6 +902,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
$loader->load('serializer_debug.php');
}

if ($this->isInitializedConfigEnabled('feature_flag')) {
$loader->load('feature_flag_debug.php');
}

$container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']);
$container->setParameter('profiler_listener.only_main_requests', $config['only_main_requests']);

Expand Down Expand Up @@ -3137,6 +3153,64 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil
}
}

private function registerFeatureFlagConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
{
$loader->load('feature_flag.php');

$container->registerForAutoconfiguration(ProviderInterface::class)
->addTag('feature_flag.provider')
;

$container->registerAttributeForAutoconfiguration(AsFeature::class,
static function (ChildDefinition $definition, AsFeature $attribute, \ReflectionClass|\ReflectionMethod $reflector): void {
$featureName = $attribute->name;

if ($reflector instanceof \ReflectionClass) {
$className = $reflector->getName();
$method = $attribute->method;

$featureName ??= $className;
} else {
$className = $reflector->getDeclaringClass()->getName();
if (null !== $attribute->method && $reflector->getName() !== $attribute->method) {
throw new \LogicException(\sprintf('Using the #[%s(method: "%s")] attribute on a method is not valid. Either remove the method value or move this to the top of the class (%s).', AsFeature::class, $attribute->method, $className));
}

10000 $method = $reflector->getName();
$featureName ??= "{$className}::{$method}";
}

$definition->addTag('feature_flag.feature', [
'feature' => $featureName,
'method' => $method,
]);
},
);

if (ContainerBuilder::willBeAvailable('symfony/routing', Router::class, ['symfony/framework-bundle', 'symfony/routing'])) {
$loader->load('feature_flag_routing.php');
}
}

private function resolveTrustedHeaders(array $headers): int
{
$trustedHeaders = 0;

foreach ($headers as $h) {
$trustedHeaders |= match ($h) {
'forwarded' => Request::HEADER_FORWARDED,
'x-forwarded-for' => Request::HEADER_X_FORWARDED_FOR,
'x-forwarded-host' => Request::HEADER_X_FORWARDED_HOST,
'x-forwarded-proto' => Request::HEADER_X_FORWARDED_PROTO,
'x-forwarded-port' => Request::HEADER_X_FORWARDED_PORT,
'x-forwarded-prefix' => Request::HEADER_X_FORWARDED_PREFIX,
default => 0,
};
}

return $trustedHeaders;
}

public function getXsdValidationBasePath(): string|false
{
return \dirname(__DIR__).'/Resources/config/schema';
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ErrorLoggerCompilerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FeatureFlagPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
Expand Down Expand Up @@ -181,6 +182,7 @@ public function build(ContainerBuilder $container): void
// must be registered after MonologBundle's LoggerChannelPass
$container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new VirtualRequestStackPass());
$container->addCompilerPass(new FeatureFlagPass());

if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2);
Expand Down
Loading
Loading
0