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
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
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
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.