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.
Nice ! Expression language reminds me drupal views a bit without the ugly interface. It would be a good candidate for a native extension too,
Twig syntax is great i hope we'll have access to more complex constructs in the future.
Not really amused :-(
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.
Just forgot to add that you did a great job, Fabien. This is awesome work, don't get me wrong, truly stunning! Many kudos for building such an awesome component!
I just see more abuse cases than use cases...
translated into Russian language http://habrahabr.ru/post/202058/ Relative to the component I have mixed feelings. And I didn't understand why it is added to the twig
is twig example right?
maybe it should be
{% if expression('has_role("FOO")') %}
instead of
{% if is_granted(expression('has_role("FOO")')) %}
Marco: I think this was done because "we can". Its based on the Twig Lexing/Parsing and this code is proven to be stable. So, why not extract some parts of it and build a custom DSL for Symfony2 (Configuration) ;-)