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 | useSymfony\Component\ExpressionLanguage\ExpressionLanguage;$language=newExpressionLanguage();echo$language->evaluate('1 + 1');// will echo 2echo$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'=>newstdClass()));$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(newExpression('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 | <serviceid="foo"class="Foo"><argumenttype="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()orhas_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 | {%ifis_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')") */publicfunctionshowAction(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") */publicfunctionshowAction(Request$request,Post$post){$response=newResponse();$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()") */publicfunctionshowAction(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']andrequest.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 | // helloif(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 | useSymfony\Component\Validator\ConstraintsasAssert;/** * @Assert\Expression("this.getFoo() == 'fo'", message="Not good!") */classObj{publicfunctiongetFoo(){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 ifuser.getGroup() in ['good_customers', 'collaborator']# Promote article to the homepage whenarticle.commentCount > 100 and article.category not in ["misc"]# Send an alert whenproduct.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.