Создание PHP кода через 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.

Генерация PHP-кода через PHP - comments

En
создание php через php (php)