Компонент ExpressionLanguage

Дата обновления перевода 2024-07-03

Компонент ExpressionLanguage

Компонент ExpressionLanguage предоставляет движок, который может скомпилировать и оценить выражения. Выражения - это однострочник, который возвращает значение (в основном, но не только, булево).

Установка

1
$ composer require symfony/expression-language

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Чем мне может помочь Expression Engine?

Целью этого компонента является позволить пользователям использовать выражения внутри конфигурации для более сложной логики. Например, фреймворк Symfony использует выражения в безопасности, для валидации правил и в сопоставлении маршрутов.

Кроме использования комонента в самом фреймворке, компонент ExpressionLanguage также является идеальным кандидатом для основания двигателя бизнес-правила. Идея заключатеся в том, чтобы позволить веб-разработчику сайта сконфигурировать всё динамически, не используя PHP и не вызывая проблем безопасности:

1
2
3
4
5
6
7
8
# Получить специальную цену, если
user.getGroup() in ['good_customers', 'collaborator']

# Продвинуть статью на главную страницу, когда
article.commentCount > 100 and article.category not in ["misc"]

# Отправить уведомление, когда
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)

Tip

О синтаксисе компонента ExpressionLanguage читайте в Синтаксис выражений.

Оператор коалесценции нуля

Note

Это содержимое было перемещено в раздел справочной страницы по синтаксису языка ExpressionLanguage ref:`оператор коалесценции нуля <component-expression-null-coalescing-operator>`_.

Парсинг и линтинг выражений

Компонент ExpressionLanguage предоставляет способ парсинга и линтинга выражений.
Метод parse() возвращает экземпляр ParsedExpression, который может быть использован для проверки и манипулирования выражением.
lint(), с другой стороны, возвращает булево число, указывающее, является ли выражение валидным или нет:

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

$expressionLanguage = new ExpressionLanguage();

var_dump($expressionLanguage->parse('1 + 2'));
// отображает AST узлы выражения, которые можно
// исследовать и манипулировать

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

Поведение этих методов можно сконфигурировать с помощью некоторых флажков, определенных в классе 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();

// это возвращает true, так как неизвестные переменные и функции игнорируются
var_dump($expressionLanguage->lint('unknown_var + unknown_function()', Parser::IGNORE_UNKNOWN_VARIABLES | Parser::IGNORE_UNKNOWN_FUNCTIONS));

7.1

Поддержка флажков в методах parse() и lint() была представлена в Symfony 7.1.

Передача в переменных

Вы также можете передать переменные в выражение, которые могут быть любым влаидным 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,
    ]
)); // отображает "Honeycrisp"

При использовании этого компонента в приложении Symfony некоторые объекты и переменные автоматически внедряются Symfony, чтобы вы могли использовать их в своих выражениях (например, запрос, текущий пользователь и т.д.):

Caution

При использовании переменных в выражениях избегайте передачи недостоверных данных в массив переменных. Если этого избежать не удается, дезинфицируйте неалфавитно-цифровые символы в недоверенных данных, чтобы предотвратить внедрение злоумышленниками управляющих символов и изменение выражения.

Кеширование

Компонент 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);

See also

Дополнительную информацию о доступных адаптерах кеша см. в документации Компонент 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

Сброс и редактирование АСД

Сложно манипулировать или проверять выражения, созданные с помощью компонента ExpressionLanguage, поскольку выражения представляют собой обычные строки. Более эффективным подходом является преобразовать эти выражения в АСД. В информатике, АСД (Абстрактное синтаксическое дерево) - это "древовидное представление структуры исходного кода, написанного на каком-либо языке программирования ". В Symfony АСД языка ExpressionLanguage представляет собой набор узлов, содержащих PHP-классы, представляющие данное выражение.

Сброс АСД

Вызовите метод getNodes() после разбора любого выражения, чтобы получить его АСД:

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

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

// сбросить узлы АСД для проверки
var_dump($ast);

// сбросить узлы АСД как представление строки
$astAsString = $ast->dump();

Манипуляции с АСД

Узлы АСД также могут быть сброшены в массив узлов PHP, что позволяет манипулировать ими. Вызовите toArray() для преобразования АСД в массив:

// ...

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

;

Расширение ExpressionLanguage

ExpressionLanguage может быть расширен путем добавления пользовательских функций. Например, в фреймворке Symfony, безопасность имеет пользовательские функции для проверки роли пользователя.

Note

Если вы хотите узнать, как использовать функции в выражении, прочитайте "".

Регистрация функций

Функции регистрируются на каждом конкретном экземпляре 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")'));
// Это выведет: 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);
            }),
        ];
    }
}

Tip

Для создания функции выражения из функции PHP с помощью статического метода :метод:`Symfony\Component\ExpressionLanguage\ExpressionFunction::fromPhp`:

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;

// используя конструктор
$expressionLanguage = new ExpressionLanguage(null, [
    new StringExpressionLanguageProvider(),
    // ...
]);

// используя registerProvider()
$expressionLanguage->registerProvider(new StringExpressionLanguageProvider());

Tip

Рекомендуется создать собственный класс 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 = [])
    {
        // добавляет провайдер по умолчанию, чтобы пользователи могли его переопределить
        array_unshift($providers, new StringExpressionLanguageProvider());

        parent::__construct($cache, $providers);
    }
}