跳到内容

ExpressionLanguage 组件

编辑此页

ExpressionLanguage 组件提供了一个引擎,可以编译和评估表达式。表达式是一个返回值的单行代码(主要是,但不限于,布尔值)。

安装

1
$ composer require symfony/expression-language

注意

如果您在 Symfony 应用程序之外安装此组件,则必须在代码中引入 vendor/autoload.php 文件,以启用 Composer 提供的类自动加载机制。阅读这篇文章以获取更多详细信息。

Expression Language 如何帮助我?

该组件的目的是允许用户在配置中使用表达式以实现更复杂的逻辑。例如,Symfony 框架在安全、验证规则和路由匹配中使用表达式。

除了在框架本身中使用该组件外,ExpressionLanguage 组件还是业务规则引擎的基础的完美候选者。其想法是让网站管理员以动态方式配置事物,而无需使用 PHP 并且不会引入安全问题

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

表达式可以看作是一个非常受限的 PHP 沙箱,并且不易受到外部注入的影响,因为您必须显式声明哪些变量在表达式中可用(但您仍然应该清理最终用户提供的并传递给表达式的任何数据)。

用法

ExpressionLanguage 组件可以编译和评估表达式。表达式是通常返回布尔值的单行代码,代码执行表达式时可以在 if 语句中使用。表达式的一个简单示例是 1 + 2。您还可以使用更复杂的表达式,例如 someArray[3].someMethod('bar')

该组件提供了 2 种使用表达式的方式

  • 评估:表达式在不编译为 PHP 的情况下进行评估;
  • 编译:表达式被编译为 PHP,因此可以被缓存和评估。

该组件的主要类是 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)

提示

请参阅 表达式语法以了解 ExpressionLanguage 组件的语法。

Null 合并运算符

注意

此内容已移动到 ExpressionLanguage 语法参考页面的null 合并运算符部分。

解析和检查表达式

ExpressionLanguage 组件提供了一种解析和检查表达式的方法。parse() 方法返回一个 ParsedExpression 实例,该实例可用于检查和操作表达式。另一方面,lint() 在表达式无效时抛出 SyntaxError

1
2
3
4
5
6
7
8
9
10
11
12
13
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

$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`."

这些方法的行为可以使用 Parser 类中定义的一些标志进行配置

  • IGNORE_UNKNOWN_VARIABLES:如果表达式中未定义变量,则不抛出异常;
  • IGNORE_UNKNOWN_FUNCTIONS:如果表达式中未定义函数,则不抛出异常。

以下是如何使用这些标志

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);

7.1

Symfony 7.1 中引入了对 parse()lint() 方法中标志的支持。

传入变量

您还可以将变量传递到表达式中,变量可以是任何有效的 PHP 类型(包括对象)

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"

当在 Symfony 应用程序内部使用此组件时,Symfony 会自动注入某些对象和变量,以便您可以在表达式中使用它们(例如,请求、当前用户等)

缓存

ExpressionLanguage 组件提供了一个 compile() 方法,以便能够在纯 PHP 中缓存表达式。但在内部,该组件还会缓存已解析的表达式,因此可以更快地编译/评估重复的表达式。

工作流程

evaluate()compile() 都需要在提供返回值之前执行一些操作。对于 evaluate(),这种开销甚至更大。

两种方法都需要标记化和解析表达式。这由 parse() 方法完成。它返回一个 ParsedExpression。现在,compile() 方法只返回此对象的字符串转换。evaluate() 方法需要循环遍历“节点”(保存在 ParsedExpression 中的表达式片段)并动态评估它们。

为了节省时间,ExpressionLanguage 缓存了 ParsedExpression,因此它可以跳过重复表达式的标记化和解析步骤。缓存由 PSR-6 CacheItemPoolInterface 实例完成(默认情况下,它使用 ArrayAdapter)。您可以通过创建自定义缓存池或使用可用的缓存池之一,并使用构造函数注入它来自定义此设置

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

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

另请参阅

有关可用缓存适配器的更多信息,请参阅缓存组件文档。

使用已解析和序列化的表达式

evaluate()compile() 都可以处理 ParsedExpressionSerializedParsedExpression

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 转储和编辑

很难操作或检查使用 ExpressionLanguage 组件创建的表达式,因为表达式是纯字符串。更好的方法是将这些表达式转换为 AST。在计算机科学中,AST抽象语法树)是“以编程语言编写的源代码结构的树形表示”。在 Symfony 中,ExpressionLanguage AST 是一组节点,其中包含表示给定表达式的 PHP 类。

转储 AST

在解析任何表达式后调用 getNodes() 方法以获取其 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();

操作 AST

AST 的节点也可以转储到 PHP 节点数组中,以允许操作它们。调用 toArray() 方法将 AST 转换为数组

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

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

扩展 ExpressionLanguage

可以通过添加自定义函数来扩展 ExpressionLanguage。例如,在 Symfony 框架中,安全组件具有自定义函数来检查用户的角色。

注意

如果您想学习如何在表达式中使用函数,请阅读“表达式语法”。

注册函数

函数在每个特定的 ExpressionLanguage 实例上注册。这意味着这些函数可以用于由该实例执行的任何表达式中。

要注册函数,请使用 register()。此方法有 3 个参数

  • name - 表达式中函数的名称;
  • compiler - 使用该函数编译表达式时执行的函数;
  • evaluator - 评估表达式时执行的函数。

示例

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

除了自定义函数参数外,evaluator 还被传递一个 arguments 变量作为其第一个参数,该变量等于 evaluate() 的第二个参数(例如,评估表达式时的“值”)。

使用表达式提供器

当您在库中使用 ExpressionLanguage 类时,您通常希望添加自定义函数。为此,您可以通过创建一个实现 ExpressionFunctionProviderInterface 的类来创建一个新的表达式提供器。

此接口需要一个方法:getFunctions(),它返回一个要注册的表达式函数数组(ExpressionFunction 的实例)

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);
            }),
        ];
    }
}

提示

要使用 fromPhp() 静态方法从 PHP 函数创建表达式函数

1
ExpressionFunction::fromPhp('strtoupper');

支持命名空间函数,但它们需要第二个参数来定义表达式的名称

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

您可以使用 registerProvider() 或使用构造函数的第二个参数来注册提供器

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());

提示

建议在您的库中创建自己的 ExpressionLanguage 类。现在,您可以通过覆盖构造函数来添加扩展

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);
    }
}
本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本