Генератор кода PHP
- Поддерживает все новейшие возможности PHP (такие как перечисления и т.д.)
- Позволяет легко модифицировать существующие классы
- Выходные данные соответствуют стилю кодирования PSR-12 / PER
- Зрелая, стабильная и широко используемая библиотека
Установка
Загрузите и установите пакет с помощью Composer:
composer require nette/php-generator
Совместимость с PHP см. в таблице.
Классы
Начнём с простого примера генерации класса с использованием ClassType:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class
->setFinal()
->setExtends(ParentClass::class)
->addImplement(Countable::class)
->addComment("Description of class.\nSecond line\n")
->addComment('@property-read Nette\Forms\Form $form');
// для генерации PHP-кода просто приведите к строке или используйте echo:
echo $class;
Это приведет к следующему результату:
/**
* Description of class.
* Second line
*
* @property-read Nette\Forms\Form $form
*/
final class Demo extends ParentClass implements Countable
{
}
Мы также можем использовать класс Printer
для генерации кода,
который, в отличие от echo $class
, мы сможем дополнительно настроить:
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);
Можно добавлять константы (Constant) и свойства (Property):
$class->addConstant('ID', 123)
->setProtected() // постоянная видимость
->setType('int')
->setFinal();
$class->addProperty('items', [1, 2, 3])
->setPrivate() // или setVisibility('private')
->setStatic()
->addComment('@var int[]');
$class->addProperty('list')
->setType('?array')
->setInitialized(); // печатает '= null'
Он генерирует:
final protected const int ID = 123;
/** @var int[] */
private static $items = [1, 2, 3];
public ?array $list = null;
И мы можем добавить методы:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int') // тип возврата метода
->setBody('return count($items ?: $this->items);');
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
Это приводит к:
/**
* Count it.
*/
final protected function count(array &$items = []): ?int
{
return count($items ?: $this->items);
}
Продвигаемые параметры, введенные в PHP 8.0, могут быть переданы в конструктор:
$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
->setPrivate();
Это приводит к:
public function __construct(
public $name,
private $args = [],
) {
}
Свойства и классы, доступные для чтения, могут быть помечены с
помощью setReadOnly()
.
Если добавляемое свойство, константа, метод или параметр уже существуют, это вызовет исключение.
Члены можно удалить с помощью removeProperty()
, removeConstant()
,
removeMethod()
или removeParameter()
.
Вы также можете добавить существующие объекты Method
,
Property
или Constant
в класс:
$method = new Nette\PhpGenerator\Method('getHandle');
$property = new Nette\PhpGenerator\Property('handle');
$const = new Nette\PhpGenerator\Constant('ROLE');
$class = (new Nette\PhpGenerator\ClassType('Demo'))
->addMember($method)
->addMember($property)
->addMember($const);
Вы можете клонировать существующие методы, свойства и константы с
другим именем, используя cloneWithName()
:
$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);
Интерфейс или трейты
Вы можете создавать интерфейсы и трейты (классы InterfaceType и TraitType):
$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');
Использование признаков:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
->addResolution('sayHello as protected')
->addComment('@use MyTrait<Foo>');
echo $class;
Результат:
class Demo
{
use SmartObject;
/** @use MyTrait<Foo> */
use MyTrait {
sayHello as protected;
}
}
Энумы
Вы можете легко создавать перечисления, которые появились в PHP 8.1 (класс EnumType):
$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');
echo $enum;
Результат:
enum Suit
{
case Clubs;
case Diamonds;
case Hearts;
case Spades;
}
Вы также можете определить скалярные эквиваленты для случаев, чтобы создать подкрепленное перечисление:
$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');
Можно добавить комментарий или атрибуты к каждому
случаю, используя addComment()
или addAttribute()
.
Анонимные классы
Передайте null
в качестве имени, и у вас будет
анонимный класс:
$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
->addParameter('foo');
echo '$obj = new class ($val) ' . $class . ';';
Результат:
$obj = new class ($val) {
public function __construct($foo)
{
}
};
Глобальные функции
За генерацию кода обычных функций отвечает класс GlobalFunction:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;
// или использовать PsrPrinter для вывода, совместимого с PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);
Результат:
function foo($a, $b)
{
return $a + $b;
}
Замыкания
Класс Closure поможет вам сгенерировать код замыканий:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
->setReference();
echo $closure;
// или использовать PsrPrinter для вывода, совместимого с PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);
Результат:
function ($a, $b) use (&$c) {
return $a + $b;
}
Стрелочные функции
Вы также можете генерировать стрелочные функции:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');
echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);
Результат:
fn($a, $b) => $a + $b
Подпись метода и функции
Методы представлены классом Method. Вы можете установить видимость, возвращаемое значение, добавить комментарии, атрибуты и т.д:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int');
Каждый параметр представлен классом Parameter. Опять же, вы можете установить все возможные свойства:
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
// function count(&$items = [])
Для определения так называемых вариативных параметров (а также
операторов splat, spread, ellipsis, unpacking или three dots) используйте setVariadic()
:
$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');
Generates:
function count(...$items)
{
}
Метод и тело функции
Содержимое функции или метода может быть передано методу
setBody()
сразу или последовательно (строка за строкой) путем
многократного вызова addBody()
:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;
Результат:
function foo()
{
$a = rand(10, 20);
return $a;
}
Для удобного внедрения переменных можно использовать специальные заполнители.
Простой заполнитель ?
$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;
Результат:
function foo()
{
return substr('any string', 3);
}
Вариативный заполнитель ...?
$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;
Результат:
function foo()
{
myfunc(1, 2, 3);
}
Вы также можете использовать именованные параметры PHP 8 с помощью
заполнителя ...?:
$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);
// myfunc(foo: 1, bar: true);
Экранируйте заполнители с помощью косой черты: \?
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;
Результат:
function foo($a)
{
return $a ? 10 : 3;
}
Принтеры и соответствие PSR
Класс Printer используется для генерации PHP-кода:
$class = new Nette\PhpGenerator\ClassType('Demo');
// ...
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // то же самое, что: echo $class
Он может генерировать код для всех остальных элементов, предлагая
такие методы, как printFunction()
, printNamespace()
и т.д.
Кроме того, доступен класс PsrPrinter
, вывод которого
соответствует стилю кодирования PSR-2 / PSR-12 / PER:
$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);
Нужно точно настроить поведение под свои нужды? Создайте свой
собственный принтер, унаследовав его от класса Printer
. Вы можете
изменить конфигурацию этих переменных:
class MyPrinter extends Nette\PhpGenerator\Printer
{
// длина строки, после которой строка будет обрываться
public int $wrapLength = 120;
// символ отступа, может быть заменен последовательностью пробелов
public string $indentation = "\t";
// количество пустых строк между свойствами
public int $linesBetweenProperties = 0;
// количество пустых строк между методами
public int $linesBetweenMethods = 2;
// количество пустых строк между группами утверждений использования для классов, функций и констант
public int $linesBetweenUseTypes = 0;
// положение открывающей скобки для функций и методов размещения
public bool $bracesOnNextLine = true;
// размещение одного параметра в одной строке, даже если у него есть атрибут или он продвигается
public bool $singleParameterOnOneLine = false;
// omits namespaces that do not contain any class or function
public bool $omitEmptyNamespaces = true;
// разделитель между правой круглой скобкой и возвращаемым типом функций и методов
public string $returnTypeColon = ': ';
}
Чем и почему отличаются стандартные Printer
и PsrPrinter
?
Почему в пакете нет только одного принтера – PsrPrinter
?
Стандарт Printer
форматирует код так, как мы это делаем во всей
Nette. Так как Nette был создан намного раньше, чем PSR, а также потому, что PSR в
течение многих лет не поставлял стандарты вовремя, а иногда даже с
задержкой в несколько лет от введения новой функции в PHP, это привело к
нескольким незначительным различиям в стандарте кодирования. Более
существенным отличием является использование табуляции вместо
пробелов. Мы знаем, что использование табуляции в наших проектах
позволяет регулировать ширину текста, что очень важно для людей с
нарушениями зрения. Примером незначительного отличия является
размещение фигурной скобки на отдельной строке для функций и методов и
всегда. Мы считаем рекомендацию PSR нелогичной и ведущей к снижению
ясности кода.
Типы
Каждый тип или тип объединения/пересечения может быть передан как строка. Вы также можете использовать предопределенные константы для собственных типов:
use Nette\PhpGenerator\Type;
$member->setType('array'); // или Type::Array;
$member->setType('?array'); // or Type::nullable(Type::Array);
$member->setType('array|string'); // or Type::union(Type::Array, Type::String)
$member->setType('Foo&Bar'); // или Type::intersection(Foo::class, Bar::class)
$member->setType(null); // удаляет тип
То же самое относится и к методу setReturnType()
.
Литералы
С помощью Literal
вы можете передавать произвольный PHP-код,
например, значения свойств или параметров по умолчанию и т. д:
use Nette\PhpGenerator\Literal;
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addProperty('foo', new Literal('Iterator::SELF_FIRST'));
$class->addMethod('bar')
->addParameter('id', new Literal('1 + 2'));
echo $class;
Результат:
class Demo
{
public $foo = Iterator::SELF_FIRST;
public function bar($id = 1 + 2)
{
}
}
Вы также можете передавать параметры в Literal
и форматировать их
в правильный PHP-код с помощью специальных
заполнителей:
new Literal('substr(?, ?)', [$a, $b]);
// генерирует, например: substr('hello', 5);
Литерал, представляющий создание нового объекта, легко генерируется
методом new
:
Literal::new(Demo::class, [$a, 'foo' => $b]);
// генерирует, например: new Demo(10, foo: 20)
Атрибуты
Вы можете добавлять атрибуты PHP 8 ко всем классам, методам, свойствам, константам, перечислениям, функциям, замыканиям и параметрам. Литералы также можно использовать в качестве значений параметров.
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addAttribute('Table', [
'name' => 'user',
'constraints' => [
Literal::new('UniqueConstraint', ['name' => 'ean', 'columns' => ['ean']]),
],
]);
$class->addProperty('list')
->addAttribute('Deprecated');
$method = $class->addMethod('count')
->addAttribute('Foo\Cached', ['mode' => true]);
$method->addParameter('items')
->addAttribute('Bar');
echo $class;
Результат:
#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])]
class Demo
{
#[Deprecated]
public $list;
#[Foo\Cached(mode: true)]
public function count(
#[Bar]
$items,
) {
}
}
Пространство имен
Классы, черты, интерфейсы и перечисления (далее классы) могут быть сгруппированы в пространства имен (PhpNamespace):
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
// создаем новые классы в пространстве имен
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');
// или добавляем существующий класс в пространство имен
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);
Если класс уже существует, он будет перезаписан.
Вы можете добавлять используемые с помощью конструкции use
классы, трейты и функции:
// use Http\Request;
$namespace->addUse(Http\Request::class);
// use Http\Request as HttpReq;
$namespace->addUse(Http\Request::class, 'HttpReq');
// use function iter\range;
$namespace->addUseFunction('iter\range');
Чтобы упростить полное имя класса, функции или константы в
соответствии с определенными псевдонимами, используйте метод
simplifyName
:
echo $namespace->simplifyName('Foo\Bar'); // 'Bar', потому что 'Foo' является текущим пространством имен
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', из-за определенного положения об использовании
И наоборот, вы можете преобразовать упрощенное имя класса, функции
или константы в полное имя, используя метод resolveName
:
echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'
Разрешение имён классов
Когда класс является частью пространства имен, он отображается несколько иначе: все типы (например, подсказки типов, возвращаемые типы, имя родительского класса, реализованные интерфейсы, используемые трейты и атрибуты) автоматически разрешаются (если это не отключено, см. ниже). Это означает, что в определениях необходимо использовать полностью квалифицированные имена классов, и в результирующем коде они будут заменены псевдонимами (основанными на положениях use) или полностью квалифицированными именами:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');
$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // упрощается до A
->addTrait('Bar\AliasedClass'); // упрощается до AliasedClass
$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // в комментариях упрощаем вручную
$method->addParameter('arg')
->setType('Bar\OtherClass'); // он будет разрешен в \Bar\OtherClass
echo $namespace;
// или использовать PsrPrinter для вывода в соответствии с PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);
Результат:
namespace Foo;
use Bar\AliasedClass;
class Demo implements A
{
use AliasedClass;
/**
* @return D
*/
public function method(\Bar\OtherClass $arg)
{
}
}
Авторазрешение можно отключить следующим образом:
$printer = new Nette\PhpGenerator\Printer; // или PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);
PHP-файлы
Классы, функции и пространства имен могут быть сгруппированы в файлы PHP, представленные классом PhpFile:
$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes(); // добавляет declare(strict_types=1)
$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');
// или
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');
echo $file;
// или использовать PsrPrinter для вывода, совместимого с PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);
Результат:
<?php
/**
* This file is auto-generated.
*/
declare(strict_types=1);
namespace Foo;
class A
{
}
function foo()
{
}
Примечание: В файлы не может быть добавлен дополнительный код за пределами функций и классов.
Генерация с использованием Reflection
Помимо того, что вы можете моделировать классы и функции с помощью описанного выше API, вы также можете автоматически генерировать их на основе существующих:
// создает класс, идентичный классу PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);
// создает функцию, идентичную функции trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');
// создает закрытие, как указано
$closure = Nette\PhpGenerator\Closure::from(
function (stdClass $a, $b = null) {},
);
Тела функций и методов по умолчанию пусты. Если вы хотите загрузить и
их, воспользуйтесь следующим способом (он требует установки
nikic/php-parser
):
$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);
$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);
Загрузка класса из файла
Вы также можете загружать функции, классы, интерфейсы и перечисления
непосредственно из строки PHP-кода. Например, мы создаем объект
ClassType
таким образом:
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
<?php
class Demo
{
public $foo;
}
XX);
При загрузке классов из PHP-кода однострочные комментарии вне тела методов игнорируются (например, для свойств и т.д.), поскольку в этой библиотеке нет API для работы с ними.
Вы также можете загрузить непосредственно весь PHP-файл, который может содержать любое количество классов, функций или даже несколько пространств имен:
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
Начальный комментарий к файлу и объявление strict_types
также
загружаются. С другой стороны, весь остальной глобальный код
игнорируется.
Для этого необходимо установить nikic/php-parser
.
Если вам нужно манипулировать глобальным кодом в файлах или
отдельными утверждениями в телах методов, лучше использовать
непосредственно библиотеку nikic/php-parser
.
Манипулятор класса
Класс ClassManipulator предоставляет инструменты для манипулирования классами.
$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);
Метод inheritMethod()
копирует метод из родительского класса или
реализованного интерфейса в ваш класс. Это позволяет переопределить
метод или расширить его сигнатуру:
$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
Метод inheritProperty()
копирует свойство из родительского класса в
ваш класс. Это полезно, когда вы хотите иметь то же свойство в своем
классе, но, возможно, с другим значением по умолчанию:
$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');
Метод implementInterface()
автоматически реализует в вашем классе все
методы из заданного интерфейса:
$manipulator->implementInterface(SomeInterface::class);
// Теперь ваш класс реализует SomeInterface и включает все его методы
Дампинг переменных
Класс Dumper
возвращает разобранное строковое представление
переменной в формате PHP. Обеспечивает более качественный и четкий
вывод, чем родная функция var_export()
.
$dumper = new Nette\PhpGenerator\Dumper;
$var = ['a', 'b', 123];
echo $dumper->dump($var); // печатает ['a', 'b', 123]
Таблица совместимости
PhpGenerator 4.0 и 4.1 совместим с PHP 8.0 – 8.3