Генерация кода на PHP: от строковых шаблонов до синтаксических деревьев
Введение в генерацию PHP-кода
Когда проекты становятся сложными, ручное написание однотипных конструкций утомляет. Автоматическая генерация кода позволяет создавать классы, конфигурации, SQL-запросы и даже целые модули на основе данных или метаданных. Рассмотрим основные подходы.
Основное решение: использование библиотеки PhpParser для программного построения AST
Как генерировать синтаксически корректный PHP-код, который будет поддерживать автозагрузку и автодополнение?
Библиотека nikic/php-parser позволяет создавать узлы абстрактного синтаксического дерева и затем преобразовывать их в строку кода. Это безопасно, легко читаемо и поддерживаемо.
use PhpParser\BuilderFactory;
use PhpParser\PrettyPrinter\Standard;
$factory = new BuilderFactory();
$class = $factory->class('User')
->makeFinal()
->addStmt($factory->property('name')->makePrivate())
->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'
)
))
);
$printer = new Standard();
$code = '<?php' . chr(10) . $printer->print($class->getNode());
echo $code;
Php exec docker (выполнение команд docker из php)
<?php
final class User
{
private $name;
public function getName(): string
{
return $this->name;
}
}
Php генерация кода (генерация кода php)
Проблемы и ошибки:
- Неверное построение дерева приводит к синтаксическим ошибкам. Используйте PrettyPrinter для проверки.
- Забыли указать пространство имен – добавьте
namepsaceчерез$factory->namespace(). - Сложность для новичков: требуется понимание структуры AST.
Цель использования: генерация классов сущностей, репозиториев, DTO на основе схемы БД или API-спецификации.
Вариант 1: heredoc и интерполяция переменных
Как быстро сгенерировать простой скрипт, не используя внешние библиотеки?
Самый простой способ – собирать строки с помощью heredoc или nowdoc.
$fields = ['id', 'name', 'email'];
$code = '<?php' . chr(10) . 'class User {' . chr(10);
foreach ($fields as $field) {
$code .= ' private $' . $field . ';' . chr(10);
}
$code .= '}' . chr(10);
file_put_contents('User.php', $code);
Проблемы: легко допустить синтаксическую ошибку, нет проверки типов, сложно поддерживать при большом объеме.
Решение: использовать функции форматирования, например sprintf или шаблонизатор.
Вариант 2: Twig как шаблонизатор для PHP
Как отделить логику генерации от представления кода?
Twig позволяет писать шаблоны с синтаксисом, похожим на PHP, но с безопасным экранированием.
// template.twig
<?php
class {{ className }} {
{% for field in fields %}
private ${{ field }};
{% endfor %}
}
$loader = new \Twig\Loader\FilesystemLoader('/templates');
$twig = new \Twig\Environment($loader);
$code = $twig->render('class.twig', [
'className' => 'Product',
'fields' => ['id','title','price']
]);
file_put_contents('Product.php', $code);
Проблема: для каждого нового типа кода нужен отдельный шаблон. Не подходит для динамических структур.
Вариант 3: Использование библиотеки Nette\PhpGenerator
Как генерировать код с поддержкой аннотаций и типов без ручного построения AST?
Библиотека предоставляет высокоуровневые классы для описания, например, классов, методов, констант.
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\PhpFile;
$file = new PhpFile;
$file->addNamespace('App\Entity');
$class = $file->addClass('Order');
$class->addProperty('total')->setPrivate()->setType('float');
$class->addMethod('getTotal')->setReturnType('float')->setBody('return $this->total;');
echo $file;
Проблема: библиотека менее популярна, чем PhpParser, и требует дополнительной установки.
Расширенные примеры генерации PHP-кода
1. Создание сущности с атрибутами (PhpParser)
Покажем, как сгенерировать класс с PHP 8 атрибутами, например, для валидации.
use PhpParser\BuilderFactory;
use PhpParser\PrettyPrinter\Standard;
use PhpParser\Node\Attribute;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Name;
$factory = new BuilderFactory();
$class = $factory->class('Product')
->makeFinal()
->addStmt(
$factory->property('price')
->makePrivate()
->setType('float')
->addAttribute(
new AttributeGroup([
new Attribute(
new Name\FullyQualified(['Symfony', 'Component', 'Validator', 'Constraints', 'PositiveOrZero'])
)
])
)
);
$printer = new Standard();
$code = '<?php' . chr(10) . $printer->print($class->getNode());
echo $code;
<?php
final class Product
{
#[Symfony\Component\Validator\Constraints\PositiveOrZero]
private float $price;
}
2. Генерация конфигурационного массива с помощью var_export
Нестандартный приём – использовать var_export для создания PHP-файла с массивом.
$config = [
'database' => ['host' => 'localhost', 'port' => 3306, 'name' => 'test'],
'debug' => true,
'items' => ['item1', 'item2'],
];
$code = '<?php' . chr(10) . 'return ' . var_export($config, true) . ';' . chr(10);
file_put_contents('config.php', $code);
<?php
return array (
'database' =>
array (
'host' => 'localhost',
'port' => 3306,
'name' => 'test',
),
'debug' => true,
'items' =>
array (
0 => 'item1',
1 => 'item2',
),
);
3. Генерация DTO с помощью Twig и произвольной структуры
Создадим шаблон для DTO с геттерами.
{# dto.twig #}
<?php
declare(strict_types=1);
namespace {{ namespace }};
class {{ className }}
{
{% for property in properties %}
private {{ property.type }} ${{ property.name }};
{% endfor %}
{% for property in properties %}
public function get{{ property.name|capitalize }}(): {{ property.type }}
{
return $this->{{ property.name }};
}
{% endfor %}
}
$twig = new \Twig\Environment(new \Twig\Loader\FilesystemLoader('.'));
$code = $twig->render('dto.twig', [
'namespace' => 'App\DTO',
'className' => 'UserDTO',
'properties' => [
['name' => 'id', 'type' => 'int'],
['name' => 'email', 'type' => 'string'],
]
]);
echo $code;
<?php
declare(strict_types=1);
namespace App\DTO;
class UserDTO
{
private int $id;
private string $email;
public function getId(): int
{
return $this->id;
}
public function getEmail(): string
{
return $this->email;
}
}