Skip to content

[ExpressionLanguage] Create an ExpressionFunction from a PHP function name #21122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from

Conversation

maidmaid
Copy link
Contributor

@maidmaid maidmaid commented Jan 2, 2017

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, 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:

$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:

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

It includes PHP namespaced function:

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

@maidmaid maidmaid changed the title El phpfn [ExpressionLanguage] Add a new class ExpressionPhpFunction Jan 2, 2017
@nicolas-grekas nicolas-grekas added this to the 3.x milestone Jan 2, 2017
*
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class ExpressionPhpFunction extends ExpressionFunction
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make simple named constructor in ExpressionFunction rather than utilizing inheritance:

ExpressionFunction::php('strtoupper')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion of using a factory instead of a new class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@stof
Copy link
Member

stof commented Jan 6, 2017

Does it work for namespaced functions ? I think it does not in case they don't have the leading \ in the name (well, it works only if the compiled code is placed in the global namespace)

*
* @throws \InvalidArgumentException if given function name does not exist
*/
public static function php($name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a more expressive name like fromPhp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's better. I done.

@maidmaid
Copy link
Contributor Author

maidmaid commented Jan 7, 2017

@stof Indeed, for namespaced functions, it's doesn't work; a SyntaxError is thrown that said Unexpected character "\" around position 0.. To solve this, I throw an exception if given function is not in global namespace. WDYT?

@maidmaid
Copy link
Contributor Author

maidmaid commented Jan 7, 2017

@stof An another solution would be don't throw exception, to evaluate with short name of namespaced function and to compile with full name of namespaced function.

@stof
Copy link
Member

stof commented Jan 9, 2017

A syntax error ? Which error are you talking about ? Are you trying to put the namespace in the ExpressionLanguage function name too ? This is not supported, as EL does not have namespaced functions.
this tells me that the factory would need to allow 2 arguments: the PHP function name, and the EL function name used to expose it.
The EL name could be optional in case the PHP function is not namespaced, reusing the PHP function name. And the factory would throw an exception if the PHP function is namespaced and the EL name is omitted.

The alternative is to decide than namespaced PHP functions are unsupported, but I would throw an exception early in this case, saying that EL function names cannot use \ in their name.

@fabpot
Copy link
Member

fabpot commented Jan 9, 2017

I think supporting namespaced functions is better and having a second argument to change the function name in EL sounds like a good idea anyway, even for regular functions.

@maidmaid
Copy link
Contributor Author

I done the changes that you suggested @fabpot and @stof.

throw new \InvalidArgumentException(sprintf(
'An expression function name must be defined if PHP function "%s" is in namespace.',
$phpFunction
));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be on one line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

$reflection = new \ReflectionFunction($phpFunction);
if ($reflection->inNamespace() && !$expressionFunction) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using reflection, what about just checking for \\ in the function name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I will move the second part !$expressionFunction first to make it faster.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reflection is more expressive that \\ and is anyway used further.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved !$expressionFunction at first.

@maidmaid maidmaid changed the title [ExpressionLanguage] Add a new class ExpressionPhpFunction [ExpressionLanguage] Create an ExpressionFunction from a PHP function name Jan 16, 2017
* @throws \InvalidArgumentException if given PHP function name is in namespace
* and expression function name is not defined
*/
public static function fromPhp($phpFunction, $expressionFunction = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't Symfony have some convention to put factories/named constructors before the real constructor? I'm not sure, just asking.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, __construct() is always first

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to read the PHP doc to know what is  expressionFunction. I would rename it to expressionFunctionName or something else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, I renamed expressionFunction to expressionFunctionName and phpFunction to phpFunctionName.

@lyrixx
Copy link
Member

lyrixx commented Jan 22, 2017

@maidmaid could you update the PR description to match the new API?

@maidmaid
Copy link
Contributor Author

@lyrixx Yes, I updated the PR description now.


$reflection = new \ReflectionFunction($phpFunctionName);
if (!$expressionFunctionName && $reflection->inNamespace()) {
throw new \InvalidArgumentException(sprintf('An expression function name must be defined if PHP function "%s" is in namespace.', $phpFunctionName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An expression function name must be defined when PHP function "%s" is namespaced.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 with minor comment

@maidmaid
Copy link
Contributor Author

It's done @nicolas-grekas :)

@fabpot
Copy link
Member

fabpot commented Feb 16, 2017

Apparently, tests are broken when applying these changes.

@maidmaid
Copy link
Contributor Author

I fixed tests @fabpot.

@fabpot
Copy link
Member

fabpot commented Feb 19, 2017

@maidmaid I'm still not convinced that using reflection is needed. Here is my patch to get rid of it:

diff --git a/src/Symfony/Component/ExpressionLanguage/ExpressionFunction.php b/src/Symfony/Component/ExpressionLanguage/ExpressionFunction.php
index d011854804..87a494f82d 100644
--- a/src/Symfony/Component/ExpressionLanguage/ExpressionFunction.php
+++ b/src/Symfony/Component/ExpressionLanguage/ExpressionFunction.php
@@ -77,18 +77,16 @@ class ExpressionFunction
      */
     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));
         }

-        $reflection = new \ReflectionFunction($phpFunctionName);
-        if (!$expressionFunctionName && $reflection->inNamespace()) {
+        $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));
         }

-        $phpFunctionName = $reflection->getName();
-        $expressionFunctionName = $expressionFunctionName ?: $reflection->getShortName();
-
         $compiler = function () use ($phpFunctionName) {
             return sprintf('\%s(%s)', $phpFunctionName, implode(', ', func_get_args()));
         };
@@ -99,6 +97,6 @@ class ExpressionFunction
             return call_user_func_array($phpFunctionName, array_splice($args, 1));
         };

-        return new self($expressionFunctionName, $compiler, $evaluator);
+        return new self($expressionFunctionName ?: $parts[count($parts) - 1], $compiler, $evaluator);
     }
 }

Any downsides?

@fabpot
Copy link
Member

fabpot commented Feb 20, 2017

@maidmaid Can you submit a PR on the docs to document this new feature?

@fabpot
Copy link
Member

fabpot commented Feb 20, 2017

Thank you @maidmaid.

@maidmaid
Copy link
Contributor Author

Thank you to you @fabpot and others reviewers. I created doc PR.

xabbuh added a commit to symfony/symfony-docs that referenced this pull request Feb 28, 2017
…hp (maidmaid, javiereguiluz)

This PR was merged into the master branch.

Discussion
----------

[ExpressionLanguage] Document ExpressionFunction::fromPhp

cf symfony/symfony#21122

Commits
-------

415c4ec Update extending.rst
478400a Minor reword
059155f Add versionadded
6c9e09d Fix typo
c70ea7f Fix typo
6d9af37 Add fromPhp tip
@nicolas-grekas nicolas-grekas modified the milestones: 3.x, 3.3 Mar 24, 2017
@fabpot fabpot mentioned this pull request May 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants