Skip to content

Commit

Permalink
feature #35747 [Routing][FrameworkBundle] Allow using env() in route …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
fabpot committed Feb 25, 2020
2 parents 8867f57 + b574460 commit 53df70e
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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'));

Expand Down
Expand Up @@ -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',
Expand Down
Expand Up @@ -145,6 +145,7 @@

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

Expand Down
Expand Up @@ -86,9 +86,18 @@
<argument></argument> <!-- scheme -->
<argument>%request_listener.http_port%</argument>
<argument>%request_listener.https_port%</argument>
<call method="setParameter">
<argument>_functions</argument>
<argument type="service" id="router.expression_language_provider" />
</call>
</service>
<service id="Symfony\Component\Routing\RequestContext" alias="router.request_context" />

<service id="router.expression_language_provider" class="Symfony\Component\Routing\Matcher\ExpressionLanguageProvider">
<argument type="tagged_locator" tag="routing.expression_language_function" index-by="function" />
<tag name="routing.expression_language_provider" />
</service>

<service id="router.cache_warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer">
<tag name="container.service_subscriber" id="router" />
<tag name="kernel.cache_warmer" />
Expand Down
Expand Up @@ -11,8 +11,8 @@
<argument type="service" id="secrets.decryption_key" on-invalid="ignore" />
</service>

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

<service id="secrets.local_vault" class="Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault">
Expand Down
21 changes: 11 additions & 10 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
Expand Up @@ -130,18 +130,19 @@
</service>
<service id="Symfony\Component\String\Slugger\SluggerInterface" alias="slugger" />

<service id="container.getenv" class="Closure">
<factory class="Closure" method="fromCallable" />
<argument type="collection">
<argument type="service" id="service_container" />
<argument>getEnv</argument>
</argument>
<tag name="routing.expression_language_function" function="env" />
</service>

<!-- inherit from this service to lazily access env vars -->
<service id="getenv" class="Symfony\Component\String\LazyString" abstract="true">
<service id="container.env" class="Symfony\Component\String\LazyString" abstract="true">
<factory class="Symfony\Component\String\LazyString" method="fromCallable" />
<argument type="service">
<service class="Closure">
<factory class="Closure" method="fromCallable" />
<argument type="collection">
<argument type="service" id="service_container" />
<argument>getEnv</argument>
</argument>
</service>
</argument>
<argument type="service" id="container.getenv" />
</service>
</services>
</container>
1 change: 1 addition & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
@@ -0,0 +1,54 @@
<?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\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 <ahmed.tailouloute@gmail.com>
*/
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);
}
}
@@ -0,0 +1,89 @@
<?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\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'],
];
}
}

0 comments on commit 53df70e

Please sign in to comment.