Это парсер и вычислетель для простых выражений. Может быть использован для вычисления формул или строк с подстановками, вводимых пользователем.
Требует PHP 5.3 или выше
Использует замыкания функций (Closure, которые были представлены в PHP 5.3) чтобы вычислять выражения быстро,
но, в то же время, не используя eval, безопасно изолируя введённый пользователем код от среды исполнения.
Реализует модель: "компилируется однажды - вычисляется многократно". Одно выражение может быть вычислено с разными значениями переменных сотни тысяч раз за секунду.
Работает в двух режимах:
Это режим по-умолчнию. В нём исходная строка воспринимается как одно выражение.
Например: x ^ 2 + sqrt(y) * 4
$expression = "x ^ 2 + sqrt(y) * 4";
$se = new SimpleExpression($expression); // Компилирование выражения с использованием контекста по-умолчанию
...
$vars = array('x' => 20, 'y' => 16); // Список значений переменных
$result = $se->run($vars); // Вычисление выражение с использованием данного списка
print $result . PHP_EOL; // Выводит результат: 416В этом режиме исходная строка рассматривается как простой текст, в котором встречаются выражения-подстановоки, заключённые в квадратные скобки.
Например: У меня есть [num_of_apple] яблок[num_of_apple % 100 >= 10 & num_of_apple % 100 < 20 | num_of_apple % 10 >= 5 | num_of_apple % 10 = 0 ? '' : num_of_apple > 1 ? 'а' : 'о']
$expression = "У меня есть [num_of_carrot] морков[num_of_carrot % 100 >= 10 & num_of_carrot % 100 < 20 | num_of_carrot % 10 >= 5 | num_of_carrot % 10 = 0 ? 'ок' : num_of_carrot > 1 ? 'ки' : 'ка'], [num_of_apple] яблок[num_of_apple % 100 >= 10 & num_of_apple % 100 < 20 | num_of_apple % 10 >= 5 | num_of_apple % 10 = 0 ? '' : num_of_apple > 1 ? 'а' : 'о'], [num_of_banana] банан[num_of_banana % 100 >= 10 & num_of_banana % 100 < 20 | num_of_banana % 10 >= 5 | num_of_banana % 10 = 0 ? 'ов' : num_of_banana > 1 ? 'а']";
$se = new SimpleExpression($expression, true, true); // Выражение компилируется с использованеим контекста по-умолчанию
// `true` во втором параметре означает использование контекста по-умолчанию. В третьем параметре `true` выбирает режим подстановок
$vars = array(
'num_of_carrot' => 20,
'num_of_apple' => 1,
'num_of_banana' => 3
);
print $se->run($vars) . PHP_EOL; // У меня есть 20 морковок, 1 яблоко, 3 бананаЧтобы пользователю не приходилось вводить подобные длинные формулы, более правильным вариантом было бы изготовление соответствующей функции
Класс SimpleContext содержит информацию о функциях и именованных констнантах, которые могут быть использованы в выражениях
Контексты могут быть объединены в иерархическую структуру, в этом случае, если функция или константа не найдена в конкретном контексте, поиск продолжается в родительском и так далее.
Существует созданный в единственном экземпляре контекст по-умолчанию, который описывает несколько полезных функций и константу PI.
Его можно получить следующим образом: SimpleContext::getDefaultContext()
Добавление или удаление функций и констант в контексте по-умолчанию, затронет все последующие компиляции выраженый, которые используют контекст по-умолчанию. Чтобы использовать все представленные по-умолчанию функции, но не затрагивая сам контекст, можно создать новый контекст, указав контекст по-умолчанию в качестве родительского. Это может быть сделано несколькими способами.
$default_context = SimpleContext::getDefaultContext();
$my_context = new SimpleContext($default_context); // Родительский контекст может быть передан как параметр конструктора SimpleContext.
$my_context = new SimpleContext(true); // Если передано булево значение `true`, то в качестве родительского будет использован контекст по-умолчанию.
$default_context = SimpleContext::getDefaultContext();
$my_context = $default_context->derive(); // Или же может быть использован метод `derive` контекста, который создаст новый контекст, с текущим контекстом в качестве родительскогоЧтобы создать корневой контекст, вызовите конструктор без параметров или передав NULL: $my_context = new SimpleContext();
Чтобы зарегистрировать константу, используйте методы registerConstant(name, value) или registerConstants(array).
$my_context->registerConstant('THETA', 0.000001); // метод registerConstant позволяет зарегистрировать одну константу
$my_context->registerConstants(array('MAX_WIDTH' => 128, 'MAX_HEIGHT' => 64)); // registerConstants принимает массив для массовой регистрации константЧтобы удалить константу, используйте метод unregisterConstant. Передайте имя удаляемой константы, либо массив имён, либо '*' для удаления всех констант из текущего контекста. Эта операция не затрагивает родительские контексты.
ВНИМАНИЕ: константа со значением NULL может быть зарегистрирована в контексте, но во время компиляции выражения, такая константа будет считаться необъявленной и её имя будет использовано для обращения к переменной.
NULL можно использовать, чтобы заглушить какую-либо константу, объявленную в родительском контексте, если такое имя требуется для переменной.
Можно объявить функции, которые будут использованы в выражениях.
Чтобы зарегистрировать функцию, используйте метод registerFunction(function[, alias[, is_volatile]]).
function может быть именем функции или замыканием.
alias позволяет задать альтернативное имя, под которым функция будет вызываться из выражения.
Установите is_volatile в true, если функция имеет побочные эффекты, т.е. может возвращать разные значения при одинаковом наборе параметров. rand, date - пример таких функций.
В противном случае, если на вход функции переданы константы, то её значение будет вычисленно на этапе компиляции и функция будет заменена полученным выражением.
function my_func($a, $b) {
return $a - $b * 2;
}
$my_context->registerFunction('my_func'); // Проще всего зарегистрировать функцию, передав её имя в `registerFunction`;
// Передача замыкания
$f = function($x) {
return $x * $x * 2;
};
$my_context->registerFunction($f, 'second_func'); // Регистрирует функцию под указанным именем
$my_context->registerFunction('time', 'get_time', true); // Регистрирует volatile функцию под альтернативным именем.Также можно использовать метод registerFunctions(array[, is_volatile]), чтобы зарегистрировать множество функций одновременно. Если ключ элемента массива не числовой, то он используется как alias.
Чтобы удалить зарегистрированную функцию, используйте метод unregisterFunction, передав имя (alias), массив имён или '*', чтобы удалить всё.
ВНИМАНИЕ: В процессе компиляции все именованные константы заменяются на их значения, а все вызовы функций замещаются ссылкой на ReflectionFunction. Таким образом, никакая информация об использованном контексте не сохраняется, и изменение контекста после компиляции выражения никак не повлияет на скомпилированное выражение.
Контекст по-умолчанию содержит константу PI, а также определяет несколько функций:
| Имя функции | Комментарий |
|---|---|
sin(x) |
Синус |
cos(x) |
Косинус |
asin(x) |
Арксинус |
acos(x) |
Арккосинус |
tan(x) |
Тангенс |
atan(x) |
Арктангенс |
atan2(x, y) |
Арктангенс от y / x в корректном квадранте (см.) |
deg2rad(x) |
Преобразование градусов в радианы |
rad2deg(x) |
Преобразование радиан в градусы |
abs(x) |
Абсолютное значение |
floor(x) |
Округление до целого вниз |
ceil(x) |
Округление до целого вверх |
round(x[, precision]) |
Округление до ближайшего значения (см.) |
exp(x) |
Экспонента (e ^ x) |
sqrt(x) |
Квадратный корень |
hypot(x, y) |
Гипотенуза (Квадратный корень суммы квадратов) |
ln(x) |
Натуральный логарифм (см. примечания) |
log(x, base) |
Логарифм по произвольному основанию (см. примечания) |
lg(x), log10(x) |
Логарифм по основанию 10 (см.) |
min(...) |
Меньшее значение из списка аргументов |
max(...) |
Большее значение из списка аргументов |
substr(string, start[, length]) |
Подстрока (см.) |
strlen(string) |
Длина строки |
upper(string) |
Приведение строки к верхнему регистру (см.) |
lower(string) |
Приведение строки к нижнему регистру (см.) |
replace(search, replace, subject) |
Замена вхождений search на replace в строке subject (см.) |
regexp(pattern, subject) |
Выполняет сопоставление регулярному выражению (Использует функцию php preg_match) |
regexp_replace(pattern, replacement, subject[, limit]) |
Выполняет замену по регулярному выражению (Использует функцию php preg_replace) |
number_format(number[, decimals[, dec_point, thousands_sep]]) |
Форматирует число (см.) |
format(format, ...) |
Форматирует строку (Использует функцию php sprintf) |
random([a[, b]]) (volatile) |
Возвращает случайное число: 1) если без параметров, то не меньше нуля, но меньше единицы; 2) если с одним параметром, то целое, меньше указанного числа, но не меньше нуля; 3) если оба параметра указаны, то целое число в диапазоне между первым и вторым включительно. Если второй параметр меньше первого, то первые возвращается. |
date(format[, timestamp]) (volatile) |
форматирует дату или время в строку (см.) |
NOTES:
- Некоторые функции зарегистрированы под другими именами, не так, как они обявлены в php. Это сделано чтобы сохранить одинаковые имена функций использующихся в подобных парсерах, написанных на разных языках.
- Функции
lnиlog- обе псевдонимы для функции php log, и, следовательно, имеют одинаковый функционал и необязательный второй параметр. Но, для совместимости, рекомендуется использоватьlnдля вычисления натуральных логарифмов иlog- с произвольным основанием.
Поддерживаются:
- Одноместные операции
+(преобразование к числу),-(отрицание),!(логическая инверсия) - Математические операции: '+' (сложение), '-' (вычитание),
*(умножение),/(деление), '%' (остаток),^or**(возведение в степень); - Логические операции:
&(логическое И),|(логическое ИЛИ),^^(логичсеское ИСКЛЮЧАЮЩЕЕ ИЛИ) - Сравнения:
=(равно),<>или!=(не равно),>(больше),>=(больше или равно),<(меньше),<=(меньше или равно) - Трёхместный оператор сравнения <условие>
?<выражение_если_истина> [:<выражение_если_ложь>] (Если последняя часть опущена, то на её месте подразумевается пустая строка) - Операция строковой конкатенации
#. А также доступен включаемый в настройках режим неявной строковая конкатенация (выражения без оператора между ними рассматриваютися как конкатенация их строковых значений) - Скобки
- Вызовы функций
- Именованные константы
- Обращения к переменным
ВНИМАНИЕ: операции деления и взятия остатка, в случае если делитель равен нулю, обрабатываются особым образом, возвращая INF, -INF или NAN. Это сделано, чтобы избежать предупреждений PHP в случае появления деления на ноль.
Числовые константы начинаются с цифры и могут содержать точку в качестве десятичного разделителя.
Строковые константы заключаются в одиночные или двойные кавычки (' or "). Чтобы отобразить используемую кавычку в строке её следует напечатать дважды.
Идентификаторы состоят из букв и символа подчёркивания, и могут содержать цифры (кроме первой позиции)
Приоритеты операций (от большего к меньшему):
- Выражения в скобках вычисляются в первую очередь;
- Одноместные операции;
- Неявная строковая конкатенация, если включена в настройках (когда между выражениями нет оператора);
- Возведение в степень (
^или**); - Умножение, деление, взятие остатка (
*,/,%); - Сложение и вычитание (
+,-) - Строковая конкатенация (
#) - Сравнение (
=,<>or!=,>,>=,<,<=) - Логическое И (
&) - Логическое ИЛИ (
|) - Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ (
^^) - Трёхместный условный оператор
ВНИМАНИЕ: Оператор сравнения обладает наименьшим приоритетом. Это значит, что всё выражение слева от ? будет воспринято, как условие, а всё выражение справа от : - как блок иначе. Это позволяет объединять несколько условных операторов, образуя селекторы:
условие1 ? вариант1 : условие2 ? вариант2 : ... условиеN ? вариантN : иначе
На этапе компиляции может быть выброшено исключение SimpleExpressionParseError, метод getPosition() которого позволяет узнать позицию в исходной строке, на которой возникла ошибка
$expression = "x ^ 2 + sqrt(y) * 4";
try {
$se = new SimpleExpression($expression); // Компилирование выражения с использованием контекста по-умолчанию
$vars = array('x' => 0, 'y' => 0);
// Когда выражение скомпилировано, его можно вычислять многократно, используя разные значения переменных
for ($x = 10 ; $x < 100 ; $x += 40) {
for ($y = 10 ; $y < 100 ; $y += 40) { // Запускаем многократно
$vars['x'] = $x;
$vars['y'] = $y;
$result = $se->run($vars);
print "Для x = $x, y = $y, результат $result" . PHP_EOL;
}
}
} catch (SimpleExpressionParseError $e) {
print "Ошибка разбора выражения на позиции {$e->getPosition()}: {$e->getMessage()}" . PHP_EOL;
print $expression . PHP_EOL;
print str_repeat(' ', $e->getPosition()) . '^' . PHP_EOL;
}Настройка может быть включена вызовом ->implicitConcatenation(true) на объекте SimpleContext перед компиляцией выражения.
Когда настройка включена, любые идущие подряд выражения без оператора между ними, трактуются как конкатенация их строковых значений. Например:
$my_context = new SimpleContext(true); // Если передано булево значние `true`, контекст по-умолчанию будет использован в качестве родительского
$my_context->implicitConcatenation(true); // Включении настройки
$expression = "'Здравствуй, ' obj '!'";
$se = new SimpleExpression($expression, $my_context); // Компиляция выражения с использованием предоставленного контекста
$vars = array('obj' => 'Мир');
print $se->run($vars); // Prints "Здравствуй, Мир!";В версии 1.0 это был единственный способ конкатенации строк, поэтому настройка была включена изначально.
В версии 1.1 появился явный оператор конкатенации #, и неявная конкатенация по-умолчанию отключена.
NOTE: Все неизвестные идентификаторы на этапе компиляции будут трактованы как обращения к переменным. Во время исполнения все неизвестные переменные будут без предупреждений трактованы как NULL.
Это может привести к нежелательным ситуациям.
Рассмотрим два выражения: "sin(x)" и "sinus(x)".
Первое (предполагая, что функция sin объявлена в контексте) будет скомпилировано как вызов функции, которой передаётся значение переменной x в качестве параметра.
Второе (предполагая что sinus не объявлено, а неявная конкатенация включена) будет без ошибок скомпилировано в строковую конкатенацию переменных sinus и x.
Чтобы избежать подобных ошибок, можно проверить все имена переменных, использованные в выражении.
Получить их список можно вызвав метод getVars() объекта SimpleExpression.
Он вернёт массив, в котором ключи представляют имена переменных, а значения - позиции, на которой каждая переменная впервые встретилась в исходном выражении.
Но чтобы упростить процесс проверки, можно просто вызвать метод checkVars(array), передав в качестве параметра массив, в котором ключи перечисляют все допустимые имена переменных
Если выражение использует переменную отличную от перечисленных, то будет выброшено исключение SimpleExpressionParseError .
$expression = 'sinus(x)';
$se = new SimpleExpression($expression); // Компиляция выражения с использованием контекста по-умолчанию
$vars = array('x' => 0, 'y' => 0);
$se->checkVars($vars); // Исключение будет выброшено, поскольку `sinus` - неизвестная переменнаяВНИМАНИЕ: выражения нечувствительны к регистру. Имена всех переменных приводятся к нижнему регистру, поэтому имена ключей в массиве значений переменных, передаваемых методу run, должны быть в нижнем регистре
В момент компиляции над выражением производятся некоторые оптимизации
Одна и наиболее важная оптимизация - это предвычисление константных выражений. Например в выражении sin(PI / 2) (подразумевая использование контекста по-умолчанию) PI будет заменена на значение константы, затем вычсилено PI / 2 и затем вычислено значение функции sin от констатного аргумента. Возвращенное значение будет подставлено как константное выражение на место функции, тем самым избегая повторного вычисления одного и того же выражения каждый раз.
Также некоторые менее очевидные оптимизации могут быть произведены. Такие как объединение вложеных операций конкатенации в одну последовательную, или комбинирование математических операндов (например (x * 4) / 2 => (x * 2) и т.п.)
Чтобы проверить как были выполнены оптимизации, можно использовать метод debugDump() у объекта SimpleExpression. Он вернёт текстовое представление дерева вычисления.
$s = "(PI > 3) ? sin(x * 2 * PI) : sqrt(y)";
$e = new SimpleExpression($s);
print $e->debugDump();Выведет @sin(({x} * (const 6.2831853071796)))
В этой строке @ означает вызов функции; {...} - обращение к переменной; (const ...) - константный элемент.
В данном выражение, PI - это константа, которая всегда больше 3, поэтому весь условный оператор заменяется его частью "если истинна", в которой именованная константа замещается значением, а две последовательные операции с константным выражением в правой части ... * 2 * PI объединяются в одну.
- Добавлен оператор
#(явная строковая конкатенация) - Неявная строковая конкатенация (без операторов) тепрь по-умолчанию отключена, но может быть включена в
SimpleContex(см. методSimpleContext::implicitConcatenation()) - Изменена логика условий и булевых операций. Теперь строка
'0'(а также пустой массив) расматривается как 'истина'.NULL,false,0,0.0,''по прежнему 'ложь' &(и),|(или) and^^(исключающее или) более не "булевы" операторы. Тип их результата зависит от типа операндов:A | BэквивалентноA ? A : BA & BэквивалентноA ? B : AA ^^ Bэквивалентно!A ? B : (!B ? A : '')- движок поддерживает обработчки ИЛИ-цепочки, которая используется, когда обнаружена конструкция вида
A | B | C ..., она возвращает первый операнд, чьё значение приводится к булевому "истина", а если таковой не найден, то возвращает значение последнего в цепочке операнда. - Добавлены некоторые новые оптимизации.
- Оптимизация (X * 0) => 0 удалена, для того, чтобы правильным образом обрабатывать значения NAN и INF выражения X