Прощание с eval(): Как переписать код и спать спокойно

Функция eval() — это попытка интерпретатора PHP выполнить произвольную строку как программный код. Это швейцарский нож, у которого вместо лезвия — циркулярная пила. Она удобна для написания «умных» конфигураций или динамических правил, но делает ваш проект уязвимым к RCE-атакам (Remote Code Execution) и невозможным для статического анализа инструментами вроде Psalm или PHPStan.

Хорошая новость: в 95% случаев можно найти безопасную альтернативу без потери функционала.

Прощание с eval(): Как переписать код и спать спокойно

Сценарий 1: Динамические правила и математические выражения


Проблема: Пользователь вводит формулу скидки, например (price * quantity) - bonus. Вы используете eval("return $formula;").

Решение 1: Библиотеки парсинга выражений
Вместо выполнения кода, разберите его синтаксис.
  • Инструмент: symfony/expression-language или nxp/math-parser.
  • Как работает: Эти библиотеки не исполняют код, а строят дерево операций (AST). Они понимают только математику и логику, игнорируя системные вызовы.

// Было (Опасно)
$result = eval('return ' . $userFormula . ';');

// Стало (Безопасно)
use SymfonyComponentExpressionLanguageExpressionLanguage;

$language = new ExpressionLanguage();
$result = $language->evaluate($userFormula, [
    'price' => 100,
    'quantity' => 2,
    'bonus' => 10
]);


Сценарий 2: Хранение настроек в виде PHP-массивов


Проблема: Конфигурационный файл возвращает массив через return [...]. Вы подключаете его через eval(file_get_contents()) или просто include.

Решение 2: Переход на стандартные форматы данных
PHP-код — худший формат для конфигов, так как он имеет полный доступ к серверу.
  • Вариант А (Рекомендуемый): Использовать NEON (из Nette), YAML или .env. Для их чтения нужны простые парсеры (nette/utils), которые не могут выполнить вредоносный код.
  • Вариант Б (Если нужно остаться в PHP): Используйте нативный require.
    • eval('?>' . file_get_contents('config.php')) заменяется на $config = require 'config.php';.
    • Почему это лучше: Хотя require все еще выполняет код, область видимости ограничена возвращаемым значением. Главное правило здесь — никогда не давать пользователям права записи в файлы конфигурации.



Сценарий 3: Вызов методов по строке (Callback/Dynamism)


Проблема:
eval("$class_name::MyStaticMethod();");

Это часто встречается в старых роутерах или системах хуков.

Решение 3: call_user_func и стрелочные функции (Callable)
Интерпретатор умеет работать со строками-функциями напрямую, если они валидны.
// Было
eval("$controller::indexAction($id);");

// Стало (Вариант 1: Простой вызов)
call_user_func([$controller, 'indexAction'], $id);

// Стало (Вариант 2: Современный callable)
$callback = [$controller, 'indexAction'];
$callback($id);

Для статических методов:
call_user_func([ClassName::class, 'methodName']);


Сценарий 4: Создание классов "на лету" (Proxy/Decorator)


Проблема: Некоторые старые библиотеки генерируют код класса в строку и запускают eval(), чтобы создать наследника прямо в памяти.

Решение 4: runkit7 или Anonymous Classes
  • Anonymous Classes (PHP 7+): Если вам нужен класс-заглушка, используйте встроенные анонимные классы. Они создаются безопасным способом внутри области видимости.
  • runkit7: Это PECL-расширение, которое позволяет модифицировать код во время выполнения более контролируемо, чем eval, хотя оно тоже считается мощным инструментом и требует осторожности.


Сценарий 5: Шаблонизаторы старого типа (preg_replace + /e)


Проблема: Использование устаревшего модификатора /e в регулярных выражениях, который выполнял найденный текст как PHP.

Решение 5: preg_replace_callback
Это стандарт де-факто уже много лет.
// Устарело и удалено в новых версиях
$html = preg_replace('/{{s*(.+?)s*}}/e', '$this->$1', $template);

// Современный способ
$html = preg_replace_callback('/{{s*(.+?)s*}}/', function ($matches) use ($obj) {
    return $obj->{$matches[1]}();
}, $template);


Итоговый чек-лист рефакторинга


  1. Поиск: Выполните команду grep -R "eval(" . в корне проекта.
  2. Проверка ввода: Если внутри eval() есть переменная из $_GET, $_POST или базы данных — это критическая уязвимость. Меняйте немедленно.
  3. Локализация: Если eval() используется для подключения изолированного файла настроек, замените на require, но убедитесь, что путь жестко прописан и пользователь не может его изменить.
  4. Логика: Если это бизнес-правило, вынесите его в отдельный сервис и используйте библиотеку выражений (Scenario 1).


Отказываясь от eval(), вы делаете свой код быстрее (OPcache сможет закэшировать файлы), надежнее и понятнее для IDE. Попробуйте заменить хотя бы один вызов сегодня — ваши глаза (и сервера) скажут вам спасибо.
Автор:  08.08.2026 07:18:02 am