8000 feature #35747 [Routing][FrameworkBundle] Allow using env() in route … · symfony/symfony@53df70e · GitHub
[go: up one dir, main page]

Skip to content

Commit 53df70e

Browse files
committed
feature #35747 [Routing][FrameworkBundle] Allow using env() in route conditions (atailouloute)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Routing][FrameworkBundle] Allow using env() in route conditions | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | | License | MIT | Doc PR | TODO This is a second implementation of #35727, it overcomes the limitation mentioned by nicolas in (#35727 (comment)) The goal of this feature is to be able to use env variables in Route conditions ```php /** * @route("/only-for-dev", condition="env('APP_ENV') === 'dev'") */ public function __invoke() { echo "This will be executed only when APP_ENV = dev"; } ``` it supports also env processors/ loaders ```php /** * @route("/only-for-dev", condition="env('trim:APP_ENV') === 'dev'") */ ```` **TODOs:** - [x] Complete unit tests Commits ------- b574460 [Routing][FrameworkBundle] Allow using env() in route conditions
2 parents 8867f57 + b574460 commit 53df70e

File tree

10 files changed

+175
-13
lines changed

10 files changed

+175
-13
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CHANGELOG
1212
* Deprecated passing a `RouteCollectionBuiler` to `MicroKernelTrait::configureRoutes()`, type-hint `RoutingConfigurator` instead
1313
* The `TemplateController` now accepts context argument
1414
* Deprecated *not* setting the "framework.router.utf8" configuration option as it will default to `true` in Symfony 6.0
15+
* Added tag `routing.expression_language_function` to define functions available in route conditions
1516

1617
5.0.0
1718
-----

src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ class RouterMatchCommand extends Command
3333
protected static $defaultName = 'router:match';
3434

3535
private $router;
36+
private $expressionLanguageProviders;
3637

37-
public function __construct(RouterInterface $router)
38+
public function __construct(RouterInterface $router, iterable $expressionLanguageProviders = [])
3839
{
3940
parent::__construct();
4041

4142
$this->router = $router;
43+
$this->expressionLanguageProviders = $expressionLanguageProviders;
4244
}
4345

4446
/**
@@ -87,6 +89,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8789
}
8890

8991
$matcher = new TraceableUrlMatcher($this->router->getRouteCollection(), $context);
92+
foreach ($this->expressionLanguageProviders as $provider) {
93+
$matcher->addExpressionLanguageProvider($provider);
94+
}
9095

9196
$traces = $matcher->getTraces($input->getArgument('path_info'));
9297

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class UnusedTagsPass implements CompilerPassInterface
4949
'mime.mime_type_guesser',
5050
'monolog.logger',
5151
'proxy',
52+
'routing.expression_language_function',
5253
'routing.expression_language_provider',
5354
'routing.loader',
5455
'routing.route_loader',

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145

146146
<service id="console.command.router_match" class="Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand">
147147
<argument type="service" id="router" />
148+
<argument type="tagged_iterator" tag="routing.expression_language_provider" />
148149
<tag name="console.command" command="router:match" />
149150
</service>
150151

src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,18 @@
8686
<argument& F438 gt;</argument> <!-- scheme -->
8787
<argument>%request_listener.http_port%</argument>
8888
<argument>%request_listener.https_port%</argument>
89+
<call method="setParameter">
90+
<argument>_functions</argument>
91+
<argument type="service" id="router.expression_language_provider" />
92+
</call>
8993
</service>
9094
<service id="Symfony\Component\Routing\RequestContext" alias="router.request_context" />
9195

96+
<service id="router.expression_language_provider" class="Symfony\Component\Routing\Matcher\ExpressionLanguageProvider">
97+
<argument type="tagged_locator" tag="routing.expression_language_function" index-by="function" />
98+
<tag name="routing.expression_language_provider" />
99+
</service>
100+
92101
<service id="router.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer">
93102
<tag name="container.service_subscriber" id="router" />
94103
<tag name="kernel.cache_warmer" />

src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<argument type="service" id="secrets.decryption_key" on-invalid="ignore" />
1212
</service>
1313

14-
<service id="secrets.decryption_key" parent="getenv">
15-
<argument />
14+
<service id="secrets.decryption_key" parent="container.env">
15+
<argument /><!-- the name of the env var to read -->
1616
</service>
1717

1818
<service id="secrets.local_vault" class="Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault">

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,19 @@
130130
</service>
131131
<service id="Symfony\Component\String\Slugger\SluggerInterface" alias="slugger" />
132132

133+
<service id="container.getenv" class="Closure">
134+
<factory class="Closure" method="fromCallable" />
135+
<argument type="collection">
136+
<argument type="service" id="service_container" />
137+
<argument>getEnv</argument>
138+
</argument>
139+
<tag name="routing.expression_language_function" function="env" />
140+
</service>
141+
133142
<!-- inherit from this service to lazily access env vars -->
134-
<service id="getenv" class="Symfony\Component\String\LazyString" abstract="true">
143+
<service id="container.env" class="Symfony\Component\String\LazyString" abstract="true">
135144
<factory class="Symfony\Component\String\LazyString" method="fromCallable" />
136-
<argument type="service">
137-
<service class="Closure">
138-
<factory class="Closure" method="fromCallable" />
139-
<argument type="collection">
140-
<argument type="service" id="service_container" />
141-
<argument>getEnv</argument>
142-
</argument>
143-
</service>
144-
</argument>
145+
<argument type="service" id="container.getenv" />
145146
</service>
146147
</services>
147148
</container>

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* added "priority" option to annotated routes
1010
* added argument `$priority` to `RouteCollection::add()`
1111
* deprecated the `RouteCompiler::REGEX_DELIMITER` constant
12+
* added `ExpressionLanguageProvider` to expose extra functions to route conditions
1213

1314
5.0.0
1415
-----
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Routing\Matcher;
13+
14+
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
15+
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
16+
use Symfony\Contracts\Service\ServiceProviderInterface;
17+
18+
/**
19+
* Exposes functions defined in the request context to route conditions.
20+
*
21+
* @author Ahmed TAILOULOUTE <ahmed.tailouloute@gmail.com>
22+
*/
23+
class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface
24+
{
25+
private $functions;
26+
27+
public function __construct(ServiceProviderInterface $functions)
28+
{
29+
$this->functions = $functions;
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function getFunctions()
36+
{
37+
foreach ($this->functions->getProvidedServices() as $function => $type) {
38+
yield new ExpressionFunction(
39+
$function,
40+
static function (...$args) use ($function) {
41+
return sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args));
42+
},
43+
function ($values, ...$args) use ($function) {
44+
return $values['context']->getParameter('_functions')->get($function)(...$args);
45+
}
46+
);
47+
}
48+
}
49+
50+
public function get(string $function): callable
51+
{
52+
return $this->functions->get($function);
53+
}
54+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Routing\Tests\Matcher;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\ServiceLocator;
16+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
17+
use Symfony\Component\Routing\Matcher\ExpressionLanguageProvider;
18+
use Symfony\Component\Routing\RequestContext;
19+
20+
class ExpressionLanguageProviderTest extends TestCase
21+
{
22+
private $context;
23+
private $expressionLanguage;
24+
25+
protected function setUp(): void
26+
{
27+
$functionProvider = new ServiceLocator([
28+
'env' => function () {
29+
// function with one arg
30+
return function (string $arg) {
31+
return [
32+
'APP_ENV' => 'test',
33+
'PHP_VERSION' => '7.2',
34+
][$arg] ?? null;
35+
};
36+
},
37+
'sum' => function () {
38+
// function with multiple args
39+
return function ($a, $b) { return $a + $b; };
40+
},
41+
'foo' => function () {
42+
// function with no arg
43+
return function () { return 'bar'; };
44+
},
45+
]);
46+
47+
$this->context = new RequestContext();
48+
$this->context->setParameter('_functions', $functionProvider);
49+
50+
$this->expressionLanguage = new ExpressionLanguage();
51+
$this->expressionLanguage->registerProvider(new ExpressionLanguageProvider($functionProvider));
52+
}
53+
54+
/**
55+
* @dataProvider compileProvider
56+
*/
57+
public function testCompile(string $expression, string $expected)
58+
{
59+
$this->assertSame($expected, $this->expressionLanguage->compile($expression));
60+
}
61+
62+
public function compileProvider(): iterable
63+
{
64+
return [
65+
['env("APP_ENV")', '($context->getParameter(\'_functions\')->get(\'env\')("APP_ENV"))'],
66+
['sum(1, 2)', '($context->getParameter(\'_functions\')->get(\'sum\')(1, 2))'],
67+
['foo()', '($context->getParameter(\'_functions\')->get(\'foo\')())'],
68+
];
69+
}
70+
71+
/**
72+
* @dataProvider evaluateProvider
73+
*/
74+
public function testEvaluate(string $expression, $expected)
75+
{
76+
$this->assertSame($expected, $this->expressionLanguage->evaluate($expression, ['context' => $this->context]));
77+
}
78+
79+
public function evaluateProvider(): iterable
80+
{
81+
return [
82+
['env("APP_ENV")', 'test'],
83+
['env("PHP_VERSION")', '7.2'],
84+
['env("unknown_env_variable")', null],
85+
['sum(1, 2)', 3],
86+
['foo()', 'bar'],
87+
];
88+
}
89+
}

0 commit comments

Comments
 (0)
0