Skip to content

Expression Language

Edit this page

Applications often need to define some logic in places where PHP code can't be used or is not safe to use: an access rule in the security configuration, the condition of a validation constraint, a discount rule stored in the database and editable by non-developers. The ExpressionLanguage component was created for these use cases. It provides an engine that can compile and evaluate expressions: one-liners that return a value (mostly, but not limited to, Booleans) using a syntax similar to PHP and Twig.

Symfony itself uses expressions in many features, such as security rules, route matching conditions, the Expression validation constraint and service container configuration.

In your own applications, the ExpressionLanguage component is a perfect candidate for the foundation of a business rule engine. The idea is to let non-developers 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

Expressions can be seen as a very restricted PHP sandbox and are less vulnerable to external injections because you must explicitly declare which variables and constants are available in an expression (but you should still sanitize any data given by end users and passed to expressions).

Installation

1
$ composer require symfony/expression-language

Note

If you install this component outside of a Symfony application, you must require the vendor/autoload.php file in your code to enable the class autoloading mechanism provided by Composer. Read this article for more details.

Usage

The ExpressionLanguage component can compile and evaluate expressions. Expressions are one-liners that often return a Boolean, which can be used by the code executing the expression in an if statement. A simple example of an expression is 1 + 2. You can also use more complicated expressions, such as someArray[3].someMethod('bar').

The component provides 2 ways to work with expressions:

  • evaluation: the expression is evaluated without being compiled to PHP;
  • compile: the expression is compiled to PHP, so it can be cached and evaluated.

The main class of the component is ExpressionLanguage:

1
2
3
4
5
6
7
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$expressionLanguage = new ExpressionLanguage();

var_dump($expressionLanguage->evaluate('1 + 2')); // displays 3

var_dump($expressionLanguage->compile('1 + 2')); // displays (1 + 2)

Tip

See The Expression Syntax to learn the syntax of the ExpressionLanguage component.

Passing in Variables

You can also pass variables into the expression, which can be of any valid PHP type (including objects):

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

$expressionLanguage = new ExpressionLanguage();

class Apple
{
    public string $variety;
}

$apple = new Apple();
$apple->variety = 'Honeycrisp';

var_dump($expressionLanguage->evaluate(
    'fruit.variety',
    [
        'fruit' => $apple,
    ]
)); // displays "Honeycrisp"

When using this component inside a Symfony application, certain objects and variables are automatically injected by Symfony so you can use them in your expressions (e.g. the request, the current user, etc.):

Limiting Access to Constants

The constant() and enum() functions can read any PHP constant and enum case of your application. When expressions come from end users, this lets them read sensitive values stored in constants (e.g. passwords or API keys). Register a ConstantFunctionProvider to list the only constants that these functions can read:

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

$expressionLanguage = new ExpressionLanguage(null, [
    new ConstantFunctionProvider([
        // a single global constant
        'PHP_EOL',
        // the constants of this class whose name starts with ROLE_
        'App\Security\Roles::ROLE_*',
        // the constants and enum cases of all classes and enums defined
        // in this namespace and its sub-namespaces
        'App\Enum\*::*',
    ]),
]);

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

// throws an exception: Constant "DB_PASSWORD" is not allowed.
$expressionLanguage->evaluate('constant("DB_PASSWORD")');

In each entry, * matches any sequence of characters except ::. Names are case-sensitive, and self::, static:: and parent:: are never allowed because their meaning depends on where the expression runs. Use ['*', '*::*'] to allow all global constants, class constants and enum cases.

The expressions evaluated by Symfony itself (security, routing, validation, service container, etc.) can read all constants.

8.2

The ConstantFunctionProvider class was introduced in Symfony 8.2.

8.2

Using the constant() and enum() functions without registering a ConstantFunctionProvider was deprecated in Symfony 8.2.

Parsing Expressions

The parse() method returns a ParsedExpression instance that can be used to inspect and manipulate the expression:

1
2
3
4
5
6
7
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$expressionLanguage = new ExpressionLanguage();

var_dump($expressionLanguage->parse('1 + 2', []));
// displays the AST nodes of the expression which can be
// inspected and manipulated

Linting Expressions

8.2

The lint:expressions command was introduced in Symfony 8.2.

Expressions are stored as strings, so their errors only show up when they are evaluated. In a Symfony application, run the lint:expressions command to find these errors in advance. It checks the expressions defined in controller attributes (e.g. #[IsGranted], #[IsCsrfTokenValid] and #[Cache]) and in the configuration (e.g. the allow_if option of access_control and the guard option of workflow transitions):

1
2
3
4
5
$ 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.

Each expression is linted with the same expression language that evaluates it. That's why the command always reports unknown functions, but it only reports unknown variables when that expression language knows which variables are available.

The command requires the debug mode to be enabled. It returns a non-zero exit code when some expression is not valid, so you can run it in your continuous integration pipeline along with the other lint:* commands.

When using the ExpressionLanguage component as a standalone library, call the lint() method instead. It throws a SyntaxError if the expression is not valid:

1
2
3
4
5
6
7
8
9
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$expressionLanguage = new ExpressionLanguage();

$expressionLanguage->lint('1 + 2', []); // doesn't throw anything

$expressionLanguage->lint('1 + a', []);
// throws a SyntaxError exception:
// "Variable "a" is not valid around position 5 for expression `1 + a`."

The behavior of the parse() and lint() methods can be configured with some flags defined in the Parser class:

  • IGNORE_UNKNOWN_VARIABLES: don't throw an exception if a variable is not defined in the expression;
  • IGNORE_UNKNOWN_FUNCTIONS: don't throw an exception if a function is not defined in the expression.

This is how you can use these flags:

1
2
3
4
5
6
7
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Parser;

$expressionLanguage = new ExpressionLanguage();

// does not throw a SyntaxError because the unknown variables and functions are ignored
$expressionLanguage->lint('unknown_var + unknown_function()', [], Parser::IGNORE_UNKNOWN_VARIABLES | Parser::IGNORE_UNKNOWN_FUNCTIONS);

Caching

The ExpressionLanguage component provides a compile() method to be able to cache the expressions in plain PHP. But internally, the component also caches the parsed expressions, so duplicated expressions can be compiled/evaluated quicker.

The Workflow

Both evaluate() and compile() need to do some things before each can provide the return values. For evaluate(), this overhead is even bigger.

Both methods need to tokenize and parse the expression. This is done by the parse() method. It returns a ParsedExpression. Now, the compile() method just returns the string conversion of this object. The evaluate() method needs to loop through the "nodes" (pieces of an expression saved in the ParsedExpression) and evaluate them dynamically.

To save time, the ExpressionLanguage caches the ParsedExpression so it can skip the tokenization and parsing steps with duplicate expressions. The caching is done by a PSR-6 CacheItemPoolInterface instance (by default, it uses an ArrayAdapter). You can customize this by creating a custom cache pool or using one of the available ones and injecting this using the constructor:

1
2
3
4
5
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$cache = new RedisAdapter(...);
$expressionLanguage = new ExpressionLanguage($cache);

See also

See the Cache documentation for more information about available cache adapters.

Using Parsed and Serialized Expressions

Both evaluate() and compile() can handle ParsedExpression and SerializedParsedExpression:

1
2
3
4
5
6
// ...

// the parse() method returns a ParsedExpression
$expression = $expressionLanguage->parse('1 + 4', []);

var_dump($expressionLanguage->evaluate($expression)); // prints 5
1
2
3
4
5
6
7
8
9
use Symfony\Component\ExpressionLanguage\SerializedParsedExpression;
// ...

$expression = new SerializedParsedExpression(
    '1 + 4',
    serialize($expressionLanguage->parse('1 + 4', [])->getNodes())
);

var_dump($expressionLanguage->evaluate($expression)); // prints 5

AST Dumping and Editing

It's difficult to manipulate or inspect the expressions created with the ExpressionLanguage component, because the expressions are plain strings. A better approach is to turn those expressions into an AST. In computer science, AST (Abstract Syntax Tree) is "a tree representation of the structure of source code written in a programming language". In Symfony, an ExpressionLanguage AST is a set of nodes that contain PHP classes representing the given expression.

Dumping the AST

Call the getNodes() method after parsing any expression to get its AST:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

$ast = new ExpressionLanguage()
    ->parse('1 + 2', [])
    ->getNodes()
;

// dump the AST nodes for inspection
var_dump($ast);

// dump the AST nodes as a string representation
$astAsString = $ast->dump();

Manipulating the AST

The nodes of the AST can also be dumped into a PHP array of nodes to allow manipulating them. Call the toArray() method to turn the AST into an array:

1
2
3
4
5
6
7
// ...

$astAsArray = new ExpressionLanguage()
    ->parse('1 + 2', [])
    ->getNodes()
    ->toArray()
;

Extending the ExpressionLanguage

The ExpressionLanguage can be extended by adding custom functions. For instance, in the Symfony Framework, the security has custom functions to check the user's role.

Note

If you want to learn how to use functions in an expression, read "The Expression Syntax".

Registering Functions

Functions are registered on each specific ExpressionLanguage instance. That means the functions can be used in any expression executed by that instance.

To register a function, use register(). This method has 3 arguments:

  • name - The name of the function in an expression;
  • compiler - A function executed when compiling an expression using the function;
  • evaluator - A function executed when the expression is evaluated.

Example:

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

$expressionLanguage = new ExpressionLanguage();
$expressionLanguage->register('lowercase', function ($str): string {
    return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str);
}, function ($arguments, $str): string {
    if (!is_string($str)) {
        return $str;
    }

    return strtolower($str);
});

var_dump($expressionLanguage->evaluate('lowercase("HELLO")'));
// this will print: hello

In addition to the custom function arguments, the evaluator is passed an arguments variable as its first argument, which is equal to the second argument of evaluate() (e.g. the "values" when evaluating an expression).

Using Expression Providers

When you use the ExpressionLanguage class in your library, you often want to add custom functions. To do so, you can create a new expression provider by creating a class that implements ExpressionFunctionProviderInterface.

This interface requires one method: getFunctions(), which returns an array of expression functions (instances of ExpressionFunction) to register:

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

class StringExpressionLanguageProvider implements ExpressionFunctionProviderInterface
{
    public function getFunctions(): array
    {
        return [
            new ExpressionFunction('lowercase', function ($str): string {
                return sprintf('(is_string(%1$s) ? strtolower(%1$s) : %1$s)', $str);
            }, function ($arguments, $str): string {
                if (!is_string($str)) {
                    return $str;
                }

                return strtolower($str);
            }),
        ];
    }
}

Tip

To create an expression function from a PHP function with the fromPhp() static method:

1
ExpressionFunction::fromPhp('strtoupper');

Namespaced functions are supported, but they require a second argument to define the name of the expression:

1
ExpressionFunction::fromPhp('My\strtoupper', 'my_strtoupper');

You can register providers using registerProvider() or by using the second argument of the constructor:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

// using the constructor
$expressionLanguage = new ExpressionLanguage(null, [
    new StringExpressionLanguageProvider(),
    // ...
]);

// using registerProvider()
$expressionLanguage->registerProvider(new StringExpressionLanguageProvider());

Tip

It is recommended to create your own ExpressionLanguage class in your library. Now you can add the extension by overriding the constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;

class ExpressionLanguage extends BaseExpressionLanguage
{
    public function __construct(?CacheItemPoolInterface $cache = null, array $providers = [])
    {
        // prepends the default provider to let users override it
        array_unshift($providers, new StringExpressionLanguageProvider());

        parent::__construct($cache, $providers);
    }
}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version