The ExpressionLanguage component powers many Symfony features: the #[IsGranted] and #[Cache] attributes, access_control rules, workflow guards, and more. Symfony 8.2 makes these expressions faster, safer, and reports their errors before they reach production.

Compiled Expressions

Nicolas Grekas
Contributed by Nicolas Grekas in #66652

Symfony caches parsed expressions, but each evaluation still rebuilds them from the cache, which takes about as long as parsing them again (15-30 µs). Workflow guards weren't cached at all.

In Symfony 8.2, all these expressions are compiled to PHP when warming up the cache. Consider this controller:

1
2
3
4
5
6
7
8
9
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Security\Http\Attribute\IsGranted;
// ...

#[IsGranted(new Expression('is_granted("ROLE_ADMIN") or user === subject.getAuthor()'), subject: 'post')]
public function edit(Post $post): Response
{
    // ...
}

Its expression is compiled into the following static method:

1
2
3
4
5
6
7
8
9
10
11
// var/cache/prod/expression_language/security.expression_language.php
// ...

public static function e0(array $values): mixed
{
    $auth_checker = $values['auth_checker'] ?? null;
    $user = $values['user'] ?? null;
    $subject = $values['subject'] ?? null;

    return ($auth_checker->isGranted("ROLE_ADMIN", null) || ($user === $subject->getAuthor()));
}

Evaluating the expression is now a call to this method, which takes around 1 µs, the same cost as an equivalent PHP closure. This works without changes in your application. Compiling is skipped in debug mode, so you can edit expression functions without clearing the cache.

Validating Expressions

Nicolas Grekas
Contributed by Nicolas Grekas in #66664 and #66691

Expressions are strings, so typos are only detected when they are evaluated. In Symfony 8.2, the cache warmup fails when an access_control rule or a workflow guard contains an invalid expression, including unknown variables and functions.

Symfony 8.2 also adds a lint:expressions command. It checks those expressions plus the ones of controller attributes like #[IsGranted], #[IsCsrfTokenValid] or #[Cache]. Unlike the cache warmup, it also works in debug mode, so you can run it in your CI pipelines:

1
2
3
4
5
6
$ php bin/console lint:expressions

  ERROR  in security.access_control
  >> Variable "usr" is not valid around position 1 for expression `usr.id == 1`. Did you mean "user"?

 [ERROR] Found 1 error while linting 2 expressions.

Restricting Access to Constants

Nicolas Grekas
Contributed by Nicolas Grekas in #66630

Expressions can only read the variables you pass to them, but the constant() and enum() functions can read any constant and enum case of your application. When expressions come from end users, this could expose sensitive values like API keys.

Symfony 8.2 adds a ConstantFunctionProvider that restricts both functions to the constants you allow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\ExpressionLanguage\ConstantFunctionProvider;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$expressionLanguage = new ExpressionLanguage(null, [
    new ConstantFunctionProvider([
        'PHP_EOL',
        // the constants of this class whose names start with ROLE_
        'App\Security\Roles::ROLE_*',
        // all constants and enum cases of the classes in this namespace
        'App\Enum\*::*',
    ]),
]);

// returns App\Enum\Suit::Hearts
$expressionLanguage->evaluate('enum("App\\\\Enum\\\\Suit::Hearts")');

// throws an exception because this constant is not allowed
$expressionLanguage->evaluate('constant("DB_PASSWORD")');

The * wildcard matches anything except ::. Names are checked before reading the constant, so classes outside the list are never autoloaded.

The unrestricted constant() and enum() functions are now deprecated. Register a ConstantFunctionProvider in your own ExpressionLanguage instances (use ['*', '*::*'] to allow all constants). The expression languages created by Symfony already do that.

Published in #Living on the edge