Создание PHP кода через PHP: практическое руководство
Введение в генерацию PHP кода
Метапрограммирование в PHP позволяет создавать код во время выполнения или сохранять его в файлы для последующего использования. Такой подход применяется для генерации классов, функций, ORM-моделей, API-клиентов, миграций баз данных и других повторяющихся структур. В этой статье разобраны различные техники с примерами и указанием проблем, с которыми можно столкнуться.
Как сформировать PHP-код как обычную строку и выполнить его?
Самый прямолинейный способ – записать код в строковую переменную и передать её в eval(). Однако это опасно, если код поступает извне – возможна инъекция.
$code = 'echo \"Привет, мир!\";';
eval($code);
создание php через php (генерация php-кода через php)
Привет, мир!
Проблема: eval выполняет любой код, что открывает вектор атаки. Решение – избегать eval, проверять входные данные на строгое соответствие шаблону, использовать белый список конструкций.
Как создать PHP-файл на диске и подключить его?
Более безопасный вариант – записать сгенерированный код в файл и затем включить его через include или require. Такой подход позволяет кешировать результат и избежать многократного выполнения eval.
$code = '<?php\nfunction sum($a, $b) { return $a + $b; }';
file_put_contents('generated_sum.php', $code);
include 'generated_sum.php';
echo sum(5, 3);
8
Проблемы: права на запись в директорию, необходимость очистки старых файлов. Решение – использовать временную папку или автоматическое удаление после использования.
Как упростить форматирование многострочного кода?
Для больших фрагментов удобно использовать heredoc или nowdoc. They сохраняют отступы и не требуют экранирования кавычек (в heredoc).
$className = 'MyClass';
$code = <<<PHPCODE
<?php
class $className {
public function greet() {
return "Привет от $className!";
}
}
PHPCODE;
eval($code);
$obj = new MyClass();
echo $obj->greet();
Привет от MyClass!
Ошибка: забыть закрывающий идентификатор или использовать пробелы после него. Решение – строго соблюдать синтаксис heredoc (идентификатор на новой строке без отступов).
Как профессионально генерировать PHP-код с помощью абстрактного синтаксического дерева (AST)?
Библиотека nikic/PHP-Parser позволяет создавать код как структуру узлов (AST) и затем преобразовывать её в текст через PrettyPrinter. Этот подход безопасен, легко модифицируется и поддерживает современный синтаксис PHP.
Установка: composer require nikic/php-parser
Пример генерации класса с одним методом:
<?php
require 'vendor/autoload.php';
use PhpParser\BuilderFactory;
use PhpParser\PrettyPrinter\Standard;
$factory = new BuilderFactory();
$node = $factory->namespace('App\Generated')
->addStmt($factory->class('Calculator')
->addStmt($factory->method('add')
->makePublic()
->addParam($factory->param('a')->setType('int'))
->addParam($factory->param('b')->setType('int'))
->setReturnType('int')
->addStmt(new PhpParser\Node\Expr\BinaryOp\Plus(
new PhpParser\Node\Expr\Variable('a'),
new PhpParser\Node\Expr\Variable('b')
))
)
)
->getNode();
$prettyPrinter = new Standard();
echo $prettyPrinter->prettyPrintFile([$node]);
<?php
namespace App\Generated;
class Calculator
{
public function add(int $a, int $b) : int
{
return $a + $b;
}
}
Проблемы: крутая кривая обучения, необходимость понимания структуры AST. Решение – начать с готовых рецептов из документации библиотеки, постепенно осваивать построение сложных узлов.
Как динамически создать класс с методами на основе конфигурации?
Комбинируя конфигурацию (например, YAML) и генерацию строки, можно создать класс с произвольными свойствами и методами. Затем класс инстанциируется через имя, полученное из строки.
$config = [
'className' => 'DynamicEntity',
'properties' => ['id', 'name', 'email'],
'methods' => [
'getId' => 'return $this->id;',
'setName' => '$this->name = $name;'
]
];
$code = "<?php\nclass {$config['className']} {\n";
foreach ($config['properties'] as $prop) {
$code .= " public \$$prop;\n";
}
foreach ($config['methods'] as $method => $body) {
$code .= " public function $method() { $body }\n";
}
$code .= "}\n";
eval($code);
$obj = new DynamicEntity();
$obj->setName('Тестовый');
echo $obj->getName();
Тестовый
Ошибка: неправильное экранирование, ошибки синтаксиса в теле методов. Решение – проверять итоговый код через PHP-линтер перед eval или записью в файл.
Как использовать шаблонизаторы (Twig) для генерации PHP-файлов?
Шаблонизаторы отделяют логику генерации от контента. Можно написать шаблон PHP-кода со специальными метками, а затем передать данные для заполнения.
// template.php.twig
<?php
class {{ className }} {
public function {{ methodName }}() {
return '{{ returnValue }}';
}
}
// генерация
$loader = new \Twig\Loader\ArrayLoader([
'class_template' => file_get_contents('template.php.twig')
]);
$twig = new \Twig\Environment($loader);
$code = $twig->render('class_template', [
'className' => 'Greeter',
'methodName' => 'sayHello',
'returnValue' => 'Hello!'
]);
eval($code);
$g = new Greeter();
echo $g->sayHello();
Hello!
Проблема: избыточность для простых случаев, необходимость установки Twig. Решение – применять только если уже используется Twig в проекте или требуется сложная вложенность шаблонов.
Цели и случаи использования
Генерация PHP-кода применяется для:
- создания миграций базы данных на основе схемы;
- автоматической генерации прокси-классов для ORM (Doctrine);
- построения API-клиентов по OpenAPI-спецификации;
- динамической компиляции конфигураций в PHP-файлы для кеширования;
- создания фабрик, сервис-контейнеров, валидаторов.
Расширенные примеры генерации PHP кода
Генерация полноценного класса с пространством имён, трейтами и doc-блоком с помощью PHP-Parser
Этот пример показывает, как создать класс с использованием современных возможностей AST: объявление строгих типов, импорт трейта, doc-блоки.
<?php
require 'vendor/autoload.php';
use PhpParser\BuilderFactory;
use PhpParser\PrettyPrinter\Standard;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
$factory = new BuilderFactory();
$node = $factory->namespace('App\Entity')
->addStmt($factory->use('App\Traits\TimestampableTrait'))
->addStmt($factory->class('User')
->addStmt($factory->useTrait('TimestampableTrait'))
->addStmt($factory->property('id')->makePrivate()->setType('int')->setDocComment('/** @var int */'))
->addStmt($factory->property('name')->makePrivate()->setType('string')->setDocComment('/** @var string */'))
->addStmt($factory->method('__construct')
->makePublic()
->addParam($factory->param('name')->setType('string'))
->addStmt(new PhpParser\Node\Expr\Assign(
new PhpParser\Node\Expr\PropertyFetch(new PhpParser\Node\Expr\Variable('this'), 'name'),
new PhpParser\Node\Expr\Variable('name')
))
)
->addStmt($factory->method('getName')
->makePublic()
->setReturnType('string')
->addStmt(new PhpParser\Node\Stmt\Return_(
new PhpParser\Node\Expr\PropertyFetch(new PhpParser\Node\Expr\Variable('this'), 'name')
))
)
)
->getNode();
$prettyPrinter = new Standard(['shortArraySyntax' => true]);
echo $prettyPrinter->prettyPrintFile([$node]);
<?php
namespace App\Entity;
use App\Traits\TimestampableTrait;
class User
{
/** @var int */
private int $id;
/** @var string */
private string $name;
use TimestampableTrait;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName() : string
{
return $this->name;
}
}
Ошибка: пропуск точки с запятой после useTrait. Решение – сверять сгенерированный код с синтаксисом PHP. Библиотека сама добавляет точку с запятой, но при ручных манипуляциях легко ошибиться.
Генерация нескольких классов из YAML-конфигурации с записью в отдельные файлы
Практический пример генерации моделей данных из простого описания. Классы создаются и сохраняются в файлы для дальнейшего автозагрузки.
<?php
$yaml = << $entity) {
$code = "<?php\n\nclass $className {\n";
foreach ($entity['properties'] as $prop) {
$code .= " public \${$prop['name']};\n";
}
$code .= "}\n";
file_put_contents("generated/{$className}.php", $code);
echo "Создан класс $className\n";
}
Создан класс Product Создан класс Order
Проблема: YAML-парсер может не быть установлен. Решение – использовать библиотеку symfony/yaml или другие форматы (JSON, PHP-массивы).
Генерация кода с условиями и циклами через рефлексию и eval
Иногда требуется создать метод, который динамически строит цепочку условий на основе данных. Например, генерация сложного фильтра.
<?php
$conditions = [
['field' => 'age', 'operator' => '>', 'value' => 18],
['field' => 'status', 'operator' => '==', 'value' => 'active']
];
$code = '<?php function filter($item) {' . "\n";
foreach ($conditions as $i => $cond) {
$field = $cond['field'];
$op = $cond['operator'];
$value = var_export($cond['value'], true);
$code .= " if (\$item['$field'] $op $value) {\n";
}
$code .= ' return true;' . "\n";
$code .= str_repeat('}', count($conditions)) . "\n";
$code .= '}'. "\n";
eval($code);
$testItem = ['age' => 25, 'status' => 'active'];
var_dump(filter($testItem));
bool(true)
Ошибка: некорректное расставление фигурных скобок. Решение – использовать счётчик открытых блоков или строить код с помощью вложенных массивов и рекурсии.
Использование token_get_all для парсинга и модификации существующего PHP кода
Метод token_get_all() позволяет разложить любой PHP-файл на токены, изменить их и собрать обратно. Подходит для автоматического рефакторинга или вставки кода.
<?php
$original = '<?php echo \"Hello\";';
$tokens = token_get_all($original);
foreach ($tokens as &$token) {
if (is_array($token) && $token[0] === T_STRING && $token[1] === 'echo') {
$token[1] = 'print';
}
}
unset($token);
$modified = '';
foreach ($tokens as $token) {
$modified .= is_array($token) ? $token[1] : $token;
}
eval($modified);
Hello
Проблема: сложность работы с токенами, потеря контекста (пробелы, комментарии). Решение – использовать специализированные библиотеки (PHP-Parser), которые предоставляют удобное AST.