feature #21122 [ExpressionLanguage] Create an ExpressionFunction from a PHP function name (maidmaid)

This PR was squashed before being merged into the 3.3-dev branch (closes #21122).

Discussion
----------

[ExpressionLanguage] Create an ExpressionFunction from a PHP function name

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

When we [extend Expression Language](http://symfony.com/doc/current/components/expression_language/extending.html), we often need to add PHP functions whose code is repetitive and redundant at the compiler/evaluator level. This PR proposes a new way more generic which allows to add a PHP function.

currently:

```php
$el = new ExpressionLanguage();
$compiler = function ($str) {
    return sprintf('strtoupper(%s)', $str);
};
$evaluator = function ($arguments, $str) {
    return strtoupper($str);
};
$el->addFunction(new ExpressionFunction('strtoupper', $compiler, $evaluator));
$el->evaluate('strtoupper("hello")'); // return "HELLO"
```

with this PR:

```php
$el->addFunction(ExpressionFunction::fromPhp('strtoupper'));
$el->evaluate('strtoupper("hello")'); // return "HELLO"
```

It includes PHP namespaced function:

```php
$el->addFunction(ExpressionFunction::fromPhp('My\strtoupper', 'my_strtoupper'));
$el->evaluate('my_strtoupper("hello")'); // return "HELLO"
```

Commits
-------

44d67ed5f5 [ExpressionLanguage] Create an ExpressionFunction from a PHP function name
This commit is contained in:
Fabien Potencier 2017-02-20 09:23:03 -08:00
commit 3e9b8f3ba5
5 changed files with 101 additions and 6 deletions

View File

@ -62,4 +62,41 @@ class ExpressionFunction
{
return $this->evaluator;
}
/**
* Creates an ExpressionFunction from a PHP function name.
*
* @param string $phpFunctionName The PHP function name
* @param string|null $expressionFunctionName The expression function name (default: same than the PHP function name)
*
* @return self
*
* @throws \InvalidArgumentException if given PHP function name does not exist
* @throws \InvalidArgumentException if given PHP function name is in namespace
* and expression function name is not defined
*/
public static function fromPhp($phpFunctionName, $expressionFunctionName = null)
{
$phpFunctionName = ltrim($phpFunctionName, '\\');
if (!function_exists($phpFunctionName)) {
throw new \InvalidArgumentException(sprintf('PHP function "%s" does not exist.', $phpFunctionName));
}
$parts = explode('\\', $phpFunctionName);
if (!$expressionFunctionName && count($parts) > 1) {
throw new \InvalidArgumentException(sprintf('An expression function name must be defined when PHP function "%s" is namespaced.', $phpFunctionName));
}
$compiler = function () use ($phpFunctionName) {
return sprintf('\%s(%s)', $phpFunctionName, implode(', ', func_get_args()));
};
$evaluator = function () use ($phpFunctionName) {
$args = func_get_args();
return call_user_func_array($phpFunctionName, array_splice($args, 1));
};
return new self($expressionFunctionName ?: end($parts), $compiler, $evaluator);
}
}

View File

@ -143,11 +143,7 @@ class ExpressionLanguage
protected function registerFunctions()
{
$this->register('constant', function ($constant) {
return sprintf('constant(%s)', $constant);
}, function (array $values, $constant) {
return constant($constant);
});
$this->addFunction(ExpressionFunction::fromPhp('constant'));
}
private function getLexer()

View File

@ -0,0 +1,44 @@
<?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\ExpressionLanguage\Tests;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
/**
* Tests ExpressionFunction.
*
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class ExpressionFunctionTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage PHP function "fn_does_not_exist" does not exist.
*/
public function testFunctionDoesNotExist()
{
ExpressionFunction::fromPhp('fn_does_not_exist');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage An expression function name must be defined when PHP function "Symfony\Component\ExpressionLanguage\Tests\fn_namespaced" is namespaced.
*/
public function testFunctionNamespaced()
{
ExpressionFunction::fromPhp('Symfony\Component\ExpressionLanguage\Tests\fn_namespaced');
}
}
function fn_namespaced()
{
}

View File

@ -109,7 +109,7 @@ class ExpressionLanguageTest extends TestCase
$this->assertEquals(PHP_VERSION, $expressionLanguage->evaluate('constant("PHP_VERSION")'));
$expressionLanguage = new ExpressionLanguage();
$this->assertEquals('constant("PHP_VERSION")', $expressionLanguage->compile('constant("PHP_VERSION")'));
$this->assertEquals('\constant("PHP_VERSION")', $expressionLanguage->compile('constant("PHP_VERSION")'));
}
public function testProviders()
@ -117,6 +117,12 @@ class ExpressionLanguageTest extends TestCase
$expressionLanguage = new ExpressionLanguage(null, array(new TestProvider()));
$this->assertEquals('foo', $expressionLanguage->evaluate('identity("foo")'));
$this->assertEquals('"foo"', $expressionLanguage->compile('identity("foo")'));
$this->assertEquals('FOO', $expressionLanguage->evaluate('strtoupper("foo")'));
$this->assertEquals('\strtoupper("foo")', $expressionLanguage->compile('strtoupper("foo")'));
$this->assertEquals('foo', $expressionLanguage->evaluate('strtolower("FOO")'));
$this->assertEquals('\strtolower("FOO")', $expressionLanguage->compile('strtolower("FOO")'));
$this->assertTrue($expressionLanguage->evaluate('fn_namespaced()'));
$this->assertEquals('\Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced()', $expressionLanguage->compile('fn_namespaced()'));
}
/**

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\ExpressionLanguage\Tests\Fixtures;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionPhpFunction;
class TestProvider implements ExpressionFunctionProviderInterface
{
@ -24,6 +25,17 @@ class TestProvider implements ExpressionFunctionProviderInterface
}, function (array $values, $input) {
return $input;
}),
ExpressionFunction::fromPhp('strtoupper'),
ExpressionFunction::fromPhp('\strtolower'),
ExpressionFunction::fromPhp('Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced', 'fn_namespaced'),
);
}
}
function fn_namespaced()
{
return true;
}