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.

Published in #Living on the edge