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()
都可以处理 ParsedExpression
和 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 转储和编辑
很难操作或检查使用 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();
扩展 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);
}
}