From b5744601bff4c7d7ec43771fb5383f4424d00d12 Mon Sep 17 00:00:00 2001 From: Ahmed TAILOULOUTE Date: Fri, 14 Feb 2020 17:46:38 +0100 Subject: [PATCH] [Routing][FrameworkBundle] Allow using env() in route conditions --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/RouterMatchCommand.php | 7 +- .../Compiler/UnusedTagsPass.php | 1 + .../Resources/config/console.xml | 1 + .../Resources/config/routing.xml | 9 ++ .../Resources/config/secrets.xml | 4 +- .../Resources/config/services.xml | 21 ++--- src/Symfony/Component/Routing/CHANGELOG.md | 1 + .../Matcher/ExpressionLanguageProvider.php | 54 +++++++++++ .../ExpressionLanguageProviderTest.php | 89 +++++++++++++++++++ 10 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 src/Symfony/Component/Routing/Matcher/ExpressionLanguageProvider.php create mode 100644 src/Symfony/Component/Routing/Tests/Matcher/ExpressionLanguageProviderTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index a3478c0b4fd4..79086dbbb8f5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Deprecated passing a `RouteCollectionBuiler` to `MicroKernelTrait::configureRoutes()`, type-hint `RoutingConfigurator` instead * The `TemplateController` now accepts context argument * Deprecated *not* setting the "framework.router.utf8" configuration option as it will default to `true` in Symfony 6.0 + * Added tag `routing.expression_language_function` to define functions available in route conditions 5.0.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php index 454767e6a802..1e2fefbbacb2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php @@ -33,12 +33,14 @@ class RouterMatchCommand extends Command protected static $defaultName = 'router:match'; private $router; + private $expressionLanguageProviders; - public function __construct(RouterInterface $router) + public function __construct(RouterInterface $router, iterable $expressionLanguageProviders = []) { parent::__construct(); $this->router = $router; + $this->expressionLanguageProviders = $expressionLanguageProviders; } /** @@ -87,6 +89,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $matcher = new TraceableUrlMatcher($this->router->getRouteCollection(), $context); + foreach ($this->expressionLanguageProviders as $provider) { + $matcher->addExpressionLanguageProvider($provider); + } $traces = $matcher->getTraces($input->getArgument('path_info')); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 7a966fd2144e..0027505f25b6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -49,6 +49,7 @@ class UnusedTagsPass implements CompilerPassInterface 'mime.mime_type_guesser', 'monolog.logger', 'proxy', + 'routing.expression_language_function', 'routing.expression_language_provider', 'routing.loader', 'routing.route_loader', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index 6333f2d3cd0d..cbd43ac7a6a9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -145,6 +145,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 3482321a48ff..18b3429a7288 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -86,9 +86,18 @@ %request_listener.http_port% %request_listener.https_port% + + _functions + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml index 15dbabd437c0..5c514e3461b5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml @@ -11,8 +11,8 @@ - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 3c15f10abb8b..d9035ca7b867 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -130,18 +130,19 @@ + + + + + getEnv + + + + - + - - - - - - getEnv - - - + diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 8c712e0e0bb1..23d32c324273 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * added "priority" option to annotated routes * added argument `$priority` to `RouteCollection::add()` * deprecated the `RouteCompiler::REGEX_DELIMITER` constant + * added `ExpressionLanguageProvider` to expose extra functions to route conditions 5.0.0 ----- diff --git a/src/Symfony/Component/Routing/Matcher/ExpressionLanguageProvider.php b/src/Symfony/Component/Routing/Matcher/ExpressionLanguageProvider.php new file mode 100644 index 000000000000..9b1bfe3fb495 --- /dev/null +++ b/src/Symfony/Component/Routing/Matcher/ExpressionLanguageProvider.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\Component\Routing\Matcher; + +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use Symfony\Contracts\Service\ServiceProviderInterface; + +/** + * Exposes functions defined in the request context to route conditions. + * + * @author Ahmed TAILOULOUTE + */ +class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface +{ + private $functions; + + public function __construct(ServiceProviderInterface $functions) + { + $this->functions = $functions; + } + + /** + * {@inheritdoc} + */ + public function getFunctions() + { + foreach ($this->functions->getProvidedServices() as $function => $type) { + yield new ExpressionFunction( + $function, + static function (...$args) use ($function) { + return sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args)); + }, + function ($values, ...$args) use ($function) { + return $values['context']->getParameter('_functions')->get($function)(...$args); + } + ); + } + } + + public function get(string $function): callable + { + return $this->functions->get($function); + } +} diff --git a/src/Symfony/Component/Routing/Tests/Matcher/ExpressionLanguageProviderTest.php b/src/Symfony/Component/Routing/Tests/Matcher/ExpressionLanguageProviderTest.php new file mode 100644 index 000000000000..0aa3549b26c6 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Matcher/ExpressionLanguageProviderTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Tests\Matcher; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Routing\Matcher\ExpressionLanguageProvider; +use Symfony\Component\Routing\RequestContext; + +class ExpressionLanguageProviderTest extends TestCase +{ + private $context; + private $expressionLanguage; + + protected function setUp(): void + { + $functionProvider = new ServiceLocator([ + 'env' => function () { + // function with one arg + return function (string $arg) { + return [ + 'APP_ENV' => 'test', + 'PHP_VERSION' => '7.2', + ][$arg] ?? null; + }; + }, + 'sum' => function () { + // function with multiple args + return function ($a, $b) { return $a + $b; }; + }, + 'foo' => function () { + // function with no arg + return function () { return 'bar'; }; + }, + ]); + + $this->context = new RequestContext(); + $this->context->setParameter('_functions', $functionProvider); + + $this->expressionLanguage = new ExpressionLanguage(); + $this->expressionLanguage->registerProvider(new ExpressionLanguageProvider($functionProvider)); + } + + /** + * @dataProvider compileProvider + */ + public function testCompile(string $expression, string $expected) + { + $this->assertSame($expected, $this->expressionLanguage->compile($expression)); + } + + public function compileProvider(): iterable + { + return [ + ['env("APP_ENV")', '($context->getParameter(\'_functions\')->get(\'env\')("APP_ENV"))'], + ['sum(1, 2)', '($context->getParameter(\'_functions\')->get(\'sum\')(1, 2))'], + ['foo()', '($context->getParameter(\'_functions\')->get(\'foo\')())'], + ]; + } + + /** + * @dataProvider evaluateProvider + */ + public function testEvaluate(string $expression, $expected) + { + $this->assertSame($expected, $this->expressionLanguage->evaluate($expression, ['context' => $this->context])); + } + + public function evaluateProvider(): iterable + { + return [ + ['env("APP_ENV")', 'test'], + ['env("PHP_VERSION")', '7.2'], + ['env("unknown_env_variable")', null], + ['sum(1, 2)', 3], + ['foo()', 'bar'], + ]; + } +}