New in Symfony 2.4: The ExpressionLanguage Component
November 13, 2013 • Published by Fabien Potencier
Warning: This post is about an unsupported Symfony version. Some of this information may be out of date. Read the most recent Symfony Docs.
Symfony 2.4 comes with a new component: ExpressionLanguage. ExpressionLanguage provides an engine that can compile and evaluate expressions.
The language it is just a strip-down version of Twig expressions. So, an expression is a one-liner that returns a value (mostly, but not limited to, Booleans).
Unlike Twig, ExpressionLanguage works in two modes:
- compilation: the expression is compiled to PHP for later evaluation (note that the compiled PHP code does not rely on a runtime environment);
- evaluation: the expression is evaluated without being first compiled to PHP.
To be able to compile an expression to a plain PHP string without the need for
a runtime environment, the .
operator calls must be explicit to avoid any
ambiguities: foo.bar
for object properties, foo['bar']
for array calls,
and foo.getBar()
for method calls.
Using the component is as simple as it can get:
1 2 3 4 5 6 7 8 9
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
$language = new ExpressionLanguage();
echo $language->evaluate('1 + 1');
// will echo 2
echo $language->compile('1 + 2');
// will echo "(1 + 2)"
The language supports everything Twig supports in expressions: math operators, strings, numbers, arrays, hashes, Booleans, ...
Expressions can be seen as a very restricted PHP sandbox and are immune to external injections as you must explicitly declare which variables are available in an expression when compiling or evaluating:
1 2 3
$language->evaluate('a.b', array('a' => new stdClass()));
$language->compile('a.b', array('a'));
Last but not the least, you can easily extend the language via functions; they
work in the same way as their Twig counterparts (see the register()
method
for more information.)
What about some use cases? Well, we were able to leverage the new component in many different other built-in Symfony components.
Service Container
You can use an expression anywhere you can pass an argument in the service container:
1
$c->register('foo', 'Foo')->addArgument(new Expression('bar.getvalue()'));
In the container, an expression has access to two functions: service()
to
get a service, and parameter
to get a parameter value:
1
service("bar").getValue(parameter("value"))
Or in XML:
1 2 3
<service id="foo" class="Foo">
<argument type="expression">service('bar').getvalue(parameter('value'))</argument>
</service>
There is no overhead at runtime as the PHP dumper uses the expression compiler; the previous expression is compiled to the following PHP code:
1
$this->get("bar")->getvalue($this->getParameter("value"))
Access Control Rules
Configuring some security access control rules can be confusing, and this might lead to insecure applications.
The new allow_if
setting simplifies the way you configure access control
rules:
1 2
access_control:
- { path: ^/_internal/secure, allow_if: "'127.0.0.1' == request.getClientIp() or has_role('ROLE_ADMIN')" }
This rule restricts the URLs starting with /_internal/secure
to people
browsing from localhost; request
, token
and user
are the variables
you have access to and is_anonymous()
, is_authenticated()
,
is_fully_authenticated()
, is_rememberme()
, and has_role()
are the
functions defined in this context.
You can also use expressions in a Twig template by using the new expression
function:
1 2 3
{% if is_granted(expression('has_role("FOO")')) %}
...
{% endif %}
If you are using the SensioFrameworkExtraBundle, you also get a new annotation,
@Security
to secure controllers:
1 2 3 4 5 6 7
/**
* @Route("/post/{id}")
* @Security("has_role('ROLE_ADMIN')")
*/
public function showAction(Post $post)
{
}
Note
The @Security
annotation will be part of version 3 of the bundle, to be
released before Symfony 2.4 final.
Caching
Version 3 of SensioFrameworkExtraBundle also comes with an enhanced @Cache
annotation which gives you access to the HTTP validation caching model.
Instead of writing the same boilerplate code again and again for basic cases:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/**
* @Route("/post/{id}")
* @Cache(smaxage="15")
*/
public function showAction(Request $request, Post $post)
{
$response = new Response();
$response->setLastModified($post->getUpdated());
if ($response->isNotModified($request)) {
return $response;
}
// ...
}
You can just configure everything in the annotation instead (that works for ETags as well):
1 2 3 4 5 6 7 8
/**
* @Route("/post/{id}")
* @Cache(smaxage="15", lastModified="post.getUpdatedAt()")
*/
public function showAction(Post $post)
{
// ...
}
Routing
Out of the box, Symfony can only match an incoming request based on some pre-determined variables (like the path info, the method, the scheme, ...), but some people want to be able to match on some more complex logic, based on other information of the Request.
To cover those more "dynamic" use cases, you can now use the condition
setting, which allows you to add any valid expression by using the request
and the routing context
variables:
1 2 3
hello:
path: /hello/{name}
condition: "context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') =~ '/firefox/i'"
Again, when using the URL matcher PHP dumper, there is no overhead at runtime as the condition is compiled to plain PHP:
1 2 3 4 5 6 7
// hello
if (0 === strpos($pathinfo, '/hello') && preg_match('#^/hello/(?P<name>[^/]++)$#s', $pathinfo, $matches)
&& (in_array($context->getMethod(), array(0 => "GET", 1 => "HEAD"))
&& preg_match("/firefox/i", $request->headers->get("User-Agent")))
) {
return $this->mergeDefaults(array_replace($matches, array('_route' => 'hello')), array ());
}
Caution
Be warned that conditions are not taken into account when generating a URL.
Validation
The new Expression
constraint lets you use an expression to validate a
property:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Validator\Constraints as Assert;
/**
* @Assert\Expression("this.getFoo() == 'fo'", message="Not good!")
*/
class Obj
{
public function getFoo()
{
return 'foo';
}
}
In the expression, this
references the current object being validated.
Business Rule Engine
Besides using the component in the framework itself, the expression language component is a perfect candidate for the foundation of a business rule engine. The idea is to let the webmaster of a website configure things in a dynamic way without using PHP and without introducing security problems:
1 2 3 4 5 6 7 8
# Get the special price if
user.getGroup() in ['good_customers', 'collaborator']
# Promote article to the homepage when
article.commentCount > 100 and article.category not in ["misc"]
# Send an alert when
product.stock < 15
And that's the last post I'm going to publish about upcoming new features in Symfony 2.4. The next step will be the release of the first Symfony 2.4 release candidate in a few days.
Help the Symfony project!
As with any Open-Source project, contributing code or documentation is the most common way to help, but we also have a wide range of sponsoring opportunities.
Comments are closed.
To ensure that comments stay relevant, they are closed for old posts.
Twig syntax is great i hope we'll have access to more complex constructs in the future.
I like the idea per-se, especially for edge cases such as business rule engine or for example to code special DSLs. Another use-case may be user-input based rules, which is something really hard to build while keeping it safe from code injections.
I don't like the examples exposed here though, which may well be replaced by closures in PHP, which are less magic and easier to follow/understand.
Integrating this engine in annotations/configuration seems like a mistake to me, and I'm a guy that is very used to "magic".
It's more developer pr0n than actual help, especially when we'll have to debug this stuff.
I just see more abuse cases than use cases...
Relative to the component I have mixed feelings. And I didn't understand why it is added to the twig
maybe it should be
{% if expression('has_role("FOO")') %}
instead of
{% if is_granted(expression('has_role("FOO")')) %}