- PVSM.RU - https://www.pvsm.ru -
В нашем интернет-магазине возникла задача назначение скидок клиентам и их подсчета. Вернее, скидки мы уже считали давно и до этого, но сейчас бизнес пришёл с новой идеей, на которую наш скидочный движок рассчитан не был.
Так же, нужно пояснить, что у нас разделены отдел разработки и отдел эксплуатации. Скидку должны назначать админы ресурса. Если расчет скидки делать через CustomFee.php скрипт, в котором бы была зашита логика подсчета, то каждый раз, при каких-либо изменениях, пришлось бы его заново деплоить.
Сам процесс деплоя, в нашей компании - не очень быстрый, т. к. исправления должен отревьювить техлид, после чего он попадёт тестерам и уже после админ его пустит в прод. Согласитесь, не очень удобно для назначения скидок. Да и напрягать разрабов каждый раз, что бы поменяли циферки в скрипте подсчета — не совсем правильно.
В общем, было решено писать интерпретатор выражений. Использование функции eval отмёл сразу, т. к. это такая потенциальная мина в безопасности, которую сам себе закладываешь. Моё субъективное мнение, что минусы от её использования перекрывают плюсы.
Что бы не томить большинство читателей, привожу сразу результат того что получилось. Ссылка на github: https://github.com/iustato/bql [1]
Интерпретатор понимает следующие операторы:
+ , - , * , / , == , != , < , > , <= , >=, &&, AND,
! - не, in - Проверка в массиве, like - Поиск по шаблону (аналог SQL LIKE), ?? - если переменная null, то присвоить значение.
Простенький пример использования:
use IustatoBqlExpressionInterpreter;
$bql = new ExpressionInterpreter();
$a = 10;
// Определяем переменные
// Если мы хотим, что бы значение переменной могло быть изменено интерпретатором, то передаём его по ссылке &$a
$variables = [
'a' => &$a,
'b' => 5
];
// Устанавливаем переменные в интерпретатор.
$bql->setVariables($variables);
// Выполняем выражение
$bql->evaluate("a = a + b");
$result = $bql->getModifiedVariables();
echo "Результат: " . json_encode($result) . PHP_EOL; // 15
Особой фишкой является то, что интерпретатор может принимать и обрабатывать вложенные свойства и значения а так же их устанавливать.
Представьте себе, что в момент заказа у нас есть объект класса Order, с заполненными данными о заказе. У этого объекта есть свойство $customer, которое является объектом Customer, а у объекта Customer есть массив $counters, в котором может содержаться любое количество счетчиков, которые нам может понадобиться отслеживать при назначении нашей скидки.
После обработки платежа, можно вызывать интерпретатор выражений, примерно таким образом:
// представим что это метод пост-обработки заказа
public function OrderPostProcess (Order $current_order)
{
$bql = new ExpressionInterpreter();
$variables = [
'order' => $current_order
];
$bql->setVariables($variables);
// добавляем в общие счетчики (сколько итого заказов сделал клиент) в нашем магазине
$bql→evaluate("order.customer.counters.total_qnt++; order.customer.counters.total_sum+=order.amount; ");
echo "Так поменялся объект counters: " . json_encode($order->customer->counters) . PHP_EOL;
}
Так же, если присвоение будет происходить объекту класса, то интерпретатор хорошо работает с магическим методом __set или же с методом setVariable, который вы напишите для установки значения $variable в классе.
Вот еще один пример:
class A {
private $my_value;
public $var;
public function __construct($v)
{
$this->my_value = $v;
$this->var = 123;
}
public function setValue($value)
{
$this->my_value = $value;
}
public function getValue()
{
return $this->my_value; }
}
}
// index.php
$a = new A(15);
$bql = new ExpressionInterpreter();
$variables = [ 'A' => $a ];
$bql->setVariables($variables);
$bql->evaluate("A.Value = 5 + 3 * 8; A.var = A.Value - 7;");
echo "Так поменялся объект a: " . json_encode($a) . PHP_EOL;
$ModifiedVars = $bql→getModifiedVariables();
echo "Список изменённых переменных, по мнению интерпретатора: " . json_encode($ModifiedVars) . PHP_EOL;
Таким образом, вы можете встроить интерпретатор в любую вашу структуру классов и дать возможность пользователю (скорее всего админу ресурса) делать какие-то дополнительные действия с вашими объектами внутри приложения, но только с теми, с которыми вы ему разрешите.
Привожу пару ссылок на решения, аналогичные моему, которые нашёл на просторах интернета. Так или иначе, они чем-то мне не подошли, а некоторые нагуглил уже после реализации своего решения. Возможно, Вам подойдут:
https://github.com/madorin/matex [2]
https://github.com/xylemical/php-expressions [3]
https://symfony.com/doc/current/components/expression_language.html [4]
На моё удивление, написать интерпретатор было сильно проще, чем я себе представлял изначально. Как Вы могли заметить, основная логика интерпретатора, практически уместилась в один класс. Интерпретатор состоит из 3х основных частей:
1. Конечный автомат, который разбивает введённую человеком строчку на токены. Название переменной, оператор, скобка — это всё токены. Это наиболее удобный способ «спарсить» введённую человеком строку. Не писать же для этого регулярки, в самом деле?
2. Затем выражение преобразуется в обратную польскую нотацию [5] (бесскобочной способ записи математического выражения, когда оператор стоит не между двумя операндами, а в конце. То есть 2+2, будет записано как 2 2 +).
3. Непосредственно, исполнение выражения
Доступные операторы регистрируются в конструкторе, например:
$this->registerOperator('!=', fn(&$a, $b) => $a != $b, 3);
таким образом, можно легко добавить необходимые вам функции, которые я не добавил. Их можно добавлять и вне класса, но то что добавите в сам класс, буду признателен за pull request =)
Я никогда ранее не писал интерпретаторы и это для меня первый подобный опыт. Скажу, что это оказалось сильно проще, чем я себе представлял (Спасибо chatgpt он сильно упростил мне жизнь изначально).
Сложности немного возникли, когда я отлаживал присвоение значения по ссылке. В XDebug не понятно, если значение передано как копия или как ссылка и особенно подгорело, когда обнаружил что проблема, на которую я потратил пол дня крылась в том, что call_user_func игнорирует передачу параметра по ссылке и всегда передаёт копию (да-да, потом я нашёл что это написано в документации и вообще все кроме меня это знают, но всё это я уже узнал потом)
Мой интерпретатор не имеет условных операторов и циклов. Для моей задачи я обошелся тем, что в таблице правил завел два поля Condition и Action. Код в Action, срабатывает, только если код в Condition возвращает true. Я не уверен, если нужно добавлять условные операторы и циклы, что повлечет за собой необходимость добавлять операторские скобки. Всё таки, цель была сделать простенький интерпретатор выражений для админов, а не писать язык программирования. Ради спортивного интереса, конечно, могу заняться.
Это моя первая статья на Хабре, не судите строго. Буду благодарен за фидбек.
Автор: VArtem
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/412472
Ссылки в тексте:
[1] https://github.com/iustato/bql: https://github.com/iustato/bql
[2] https://github.com/madorin/matex: https://github.com/madorin/matex
[3] https://github.com/xylemical/php-expressions: https://github.com/xylemical/php-expressions
[4] https://symfony.com/doc/current/components/expression_language.html: https://symfony.com/doc/current/components/expression_language.html
[5] обратную польскую нотацию: https://habr.com/ru/articles/100869/
[6] Источник: https://habr.com/ru/articles/887800/?utm_source=habrahabr&utm_medium=rss&utm_campaign=887800
Нажмите здесь для печати.