8000 feature #12006 Expression language extensibility (fabpot) · symfony/symfony@23cdf56 · GitHub
[go: up one dir, main page]

Skip to content

Commit 23cdf56

Browse files
committed
feature #12006 Expression language extensibility (fabpot)
This PR was merged into the 2.6-dev branch. Discussion ---------- Expression language extensibility | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #10512 | License | MIT | Doc PR | not yet The way we can add functions to an ExpressionLanguage instance is by using inheritance. #10512 tries to make the expression language in the routing flexible but using inheritance won't work when several bundles want to add functions. So, this PR takes another approach to solve the problem globally. Todo: * [x] add some more tests * [ ] add some docs Commits ------- 7c24188 [FrameworkBundle] added a compiler pass for expression language providers 4195a91 [Routing] added support for custom expression language functions 1a39046 [Security] added support for custom expression language functions 79bcd52 [DependencyInjection] added support for custom expression language functions 184742c [ExpressionLanguage] added ExpressionFunction and ExpressionFunctionProviderInterface
2 parents 4e0021b + 7c24188 commit 23cdf56

File tree

18 files changed

+450
-65
lines changed

18 files changed

+450
-65
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
/**
19+
* Registers the expression language providers.
20+
*
21+
* @author Fabien Potencier <fabien@symfony.com>
22+
*/
23+
class AddExpressionLanguageProvidersPass implements CompilerPassInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function process(ContainerBuilder $container)
29+
{
30+
// routing
31+
if ($container->hasDefinition('router')) {
32+
$definition = $container->findDefinition('router');
33+
foreach ($container->findTaggedServiceIds('routing.expression_language_provider') as $id => $attributes) {
34+
$definition->addMethodCall('addExpressionLanguageProvider', array(new Reference($id)));
35+
}
36+
}
37+
38+
// security
39+
if ($container->hasDefinition('security.access.expression_voter')) {
40+
$definition = $container->findDefinition('security.access.expression_voter');
41+
foreach ($container->findTaggedServiceIds('security.expression_language_provider') as $id => $attributes) {
42+
$definition->addMethodCall('addExpressionLanguageProvider', array(new Reference($id)));
43+
}
44+
}
45+
}
46+
}

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass;
2424
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheWarmerPass;
2525
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheClearerPass;
26+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass;
2627
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
2728
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass;
2829
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass;
@@ -81,6 +82,7 @@ public function build(ContainerBuilder $container)
8182
$container->addCompilerPass(new LoggingTranslatorPass());
8283
$container->addCompilerPass(new AddCacheWarmerPass());
8384
$container->addCompilerPass(new AddCacheClearerPass());
85+
$container->addCompilerPass(new AddExpressionLanguageProvidersPass());
8486
$container->addCompilerPass(new TranslationExtractorPass());
8587
$container->addCompilerPass(new TranslationDumperPass());
8688
$container->addCompilerPass(new FragmentRendererPass(), PassConfig::TYPE_AFTER_REMOVING);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Definition;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass;
18+
19+
class AddExpressionLanguageProvidersPassTest extends \PHPUnit_Framework_TestCase
20+
{
21+
public function testProcessForRouter()
22+
{
23+
$container = new ContainerBuilder();
24+
$container->addCompilerPass(new AddExpressionLanguageProvidersPass());
25+
26+
$definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TestProvider');
27+
$definition->addTag('routing.expression_language_provider');
28+
$container->setDefinition('some_routing_provider', $definition);
29+
30+
$container->register('router', '\stdClass');
31+
$container->compile();
32+
33+
$router = $container->getDefinition('router');
34+
$calls = $router->getMethodCalls();
35+
$this->assertCount(1, $calls);
36+
$this->assertEquals('addExpressionLanguageProvider', $calls[0][0]);
37+
$this->assertEquals(new Reference('some_routing_provider'), $calls[0][1][0]);
38+
}
39+
40+
public function testProcessForSecurity()
41+
{
42+
$container = new ContainerBuilder();
43+
$container->addCompilerPass(new AddExpressionLanguageProvidersPass());
44+
45+
$definition = new Definition('Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TestProvider');
46+
$definition->addTag('security.expression_language_provider');
47+
$container->setDefinition('some_security_provider', $definition);
48+
49+
$container->register('security.access.expression_voter', '\stdClass');
50+
$container->compile();
51+
52+
$router = $container->getDefinition('security.access.expression_voter');
53+
$calls = $router->getMethodCalls();
54+
$this->assertCount(1, $calls);
55+
$this->assertEquals('addExpressionLanguageProvider', $calls[0][0]);
56+
$this->assertEquals(new Reference('some_security_provider'), $calls[0][1][0]);
57+
}
58+
}
59+
60+
class TestProvider
61+
{
62+
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
2626
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;
2727
use Symfony\Component\ExpressionLanguage\Expression;
28+
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
2829

2930
/**
3031
* ContainerBuilder is a DI container that provides an API to easily describe services.
@@ -84,6 +85,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
8485
*/
8586
private $expressionLanguage;
8687

88+
/**
89+
* @var ExpressionFunctionProviderInterface[]
90+
*/
91+
private $expressionLanguageProviders = array();
92+
8793
/**
8894
* Sets the track resources flag.
8995
*
@@ -1056,6 +1062,11 @@ public function findTags()
10561062
return array_unique($tags);
10571063
}
10581064

1065+
public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
1066+
{
1067+
$this->expressionLanguageProviders[] = $provider;
1068+
}
1069+
10591070
/**
10601071
* Returns the Service Conditionals.
10611072
*
@@ -1161,7 +1172,7 @@ private function getExpressionLanguage()
11611172
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
11621173
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
11631174
}
1164-
$this->expressionLanguage = new ExpressionLanguage();
1175+
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
11651176
}
11661177

11671178
return $this->expressionLanguage;

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper;
2626
use Symfony\Component\DependencyInjection\ExpressionLanguage;
2727
use Symfony\Component\ExpressionLanguage\Expression;
28+
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
2829

2930
/**
3031
* PhpDumper dumps a service container as a PHP class.
@@ -55,6 +56,11 @@ class PhpDumper extends Dumper
5556
private $reservedVariables = array('instance', 'class');
5657
private $expressionLanguage;
5758

59+
/**
60+
* @var ExpressionFunctionProviderInterface[]
61+
*/
62+
private $expressionLanguageProviders = array();
63+
5864
/**
5965
* @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface
6066
*/
@@ -1316,6 +1322,11 @@ public function dumpParameter($name)
13161322
return sprintf("\$this->getParameter('%s')", strtolower($name));
13171323
}
13181324

1325+
public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
1326+
{
1327+
$this->expressionLanguageProviders[] = $provider;
1328+
}
1329+
13191330
/**
13201331
* Gets a service call
13211332
*
@@ -1405,7 +1416,7 @@ private function getExpressionLanguage()
14051416
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
14061417
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
14071418
}
1408-
$this->expressionLanguage = new ExpressionLanguage();
1419+
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
14091420
}
14101421

14111422
return $this->expressionLanguage;

src/Symfony/Component/DependencyInjection/ExpressionLanguage.php

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,22 @@
1212
namespace Symfony\Component\DependencyInjection;
1313

1414
use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;
15+
use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface;
1516

1617
/**
1718
* Adds some function to the default ExpressionLanguage.
1819
*
19-
* To get a service, use service('request').
20-
* To get a parameter, use parameter('kernel.debug').
21-
*
2220
* @author Fabien Potencier <fabien@symfony.com>
21+
*
22+
* @see ExpressionLanguageProvider
2323
*/
2424
class ExpressionLanguage extends BaseExpressionLanguage
2525
{
26-
protected function registerFunctions()
26+
public function __construct(ParserCacheInterface $cache = null, array $providers = array())
2727
{
28-
parent::registerFunctions();
29-
30-
$this->register('service', function ($arg) {
31-
return sprintf('$this->get(%s)', $arg);
32-
}, function (array $variables, $value) {
33-
return $variables['container']->get($value);
34-
});
28+
// prepend the default provider to let users overide it easily
29+
array_unshift($providers, new ExpressionLanguageProvider());
3530

36-
$this->register('parameter', function ($arg) {
37-
return sprintf('$this->getParameter(%s)', $arg);
38-
}, function (array $variables, $value) {
39-
return $variables['container']->getParameter($value);
40-
});
31+
parent::__construct($cache, $providers);
4132
}
4233
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\DependencyInjection;
13+
14+
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
15+
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
16+
17+
/**
18+
* Define some ExpressionLanguage functions.
19+
*
20+
* To get a service, use service('request').
21+
* To get a parameter, use parameter('kernel.debug').
22+
*
23+
* @author Fabien Potencier <fabien@symfony.com>
24+
*/
25+
class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface
26+
{
27+
public function getFunctions()
28+
{
29+
return array(
30+
new ExpressionFunction('service', function ($arg) {
31+
return sprintf('$this->get(%s)', $arg);
32+
}, function (array $variables, $value) {
33+
return $variables['container']->get($value);
34+
}),
35+
36+
new ExpressionFunction('parameter', function ($arg) {
37+
return sprintf('$this->getParameter(%s)', $arg);
38+
}, function (array $variables, $value) {
39+
return $variables['container']->getParameter($value);
40+
}),
41+
);
42+
}
43+
}

src/Symfony/Component/ExpressionLanguage/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
2.6.0
5+
-----
6+
7+
* Added ExpressionFunction and ExpressionFunctionProviderInterface
8+
49
2.4.0
510
-----
611

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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\ExpressionLanguage;
13+
14+
/**
15+
* Represents a function that can be used in an expression.
16+
*
17+
* A function is defined by two PHP callables. The callables are used
18+
* by the language to compile and/or evaluate the function.
19+
*
20+
* The "compiler" function is used at compilation time and must return a
21+
* PHP representation of the function call (it receives the function
22+
* arguments as arguments).
23+
*
24+
* The "evaluator" function is used for expression evaluation and must return
25+
* the value of the function call based on the values defined for the
26+
* expression (it receives the values as a first argument and the function
27+
* arguments as remaining arguments).
28+
*
29+
* @author Fabien Potencier <fabien@symfony.com>
30+
*/
31+
class ExpressionFunction
32+
{
33+
private $name;
34+
private $compiler;
35+
private $evaluator;
36+
37+
/**
38+
* Constructor.
39+
*
40+
* @param string $name The function name
41+
* @param callable $compiler A callable able to compile the function
42+
* @param callable $evaluator A callable able to evaluate the function
43+
*/
44+
public function __construct($name, $compiler, $evaluator)
45+
{
46+
$this->name = $name;
47+
$this->compiler = $compiler;
48+
$this->evaluator = $evaluator;
49+
}
50+
51+
public function getName()
52+
{
53+
return $this->name;
54+
}
55+
56+
public function getCompiler()
57+
{
58+
return $this->compiler;
59+
}
60+
61+
public function getEvaluator()
62+
{
63+
return $this->evaluator;
64+
}
65+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\ExpressionLanguage;
13+
14+
/**
15+
* @author Fabien Potencier <fabien@symfony.com>
16+
*/
17+
interface ExpressionFunctionProviderInterface
18+
{
19+
/**
20+
* @return ExpressionFunction[] An array of Function instances
21+
*/
22+
public function getFunctions();
23+
}

0 commit comments

Comments
 (0)
0