PHP Code Generator

  • Вам нужно сгенерировать PHP-код для классов, функций, PHP-файлов и т.д.?
  • Поддерживает все новейшие возможности PHP, такие как перечисления, атрибуты и т.д.
  • Позволяет легко модифицировать существующие классы
  • Выходные данные соответствуют стандарту PSR-12
  • Высокоразвитая, стабильная и широко используемая библиотека

Установка

Загрузите и установите пакет с помощью 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
{
	use Nette\SmartObject;
}

Мы также можем использовать класс Printer для генерации кода, который, в отличие от echo $class, мы сможем дополнительно настроить:

$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);

Можно добавлять константы (Constant) и свойства (Property):

$class->addConstant('ID', 123)
	->setProtected() // constant visibility
	->setFinal();

$class->addProperty('items', [1, 2, 3])
	->setPrivate() // или setVisibility('private')
	->setStatic()
	->addComment('@var int[]');

$class->addProperty('list')
	->setType('?array')
	->setInitialized(); // prints '= null'

Это сгенерирует:

final protected const 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') // method return type
	->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 = [],
) {
}

Свойства, доступные только для чтения, введенные в PHP 8.1, могут быть помечены с помощью 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);

Интерфейс или трейты

Вы можете создавать интерфейсы и трейты:

$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 {
		sayHello as protected;
	}
}

Перечисления

Вы можете легко создавать перечисления, которые появились в PHP 8.1:

$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
// 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
// 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) используйте setVariadics():

$method = $class->addMethod('count');
$method->setVariadics(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;
}

Принтеры и соответствие ПСР

Код PHP генерируется объектами Printer. Существует PsrPrinter, чей вывод соответствует PSR-2 и PSR-12 и использует пробелы для отступов, и Printer, который использует табуляцию для отступов.

$class = new Nette\PhpGenerator\ClassType('Demo');
// ...

$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class); // 4 spaces indentation

Нужно настроить поведение принтера? Создайте свой собственный, унаследовав класс 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 string $returnTypeColon = ': ';
}

Типы

Каждый тип или тип объединения/пересечения может быть передан как строка. Вы также можете использовать предопределенные константы для собственных типов:

use Nette\PhpGenerator\Type;

$member->setType('array'); // или Type::Array;
$member->setType('array|string'); // или Type::union('array', '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);

Атрибуты

Вы можете добавлять атрибуты PHP 8 ко всем классам, методам, свойствам, константам, перечислениям, функциям, замыканиям и параметрам. Литералы также можно использовать в качестве значений параметров.

$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addAttribute('Deprecated');

$class->addProperty('list')
	->addAttribute('WithArguments', [1, 2]);

$method = $class->addMethod('count')
	->addAttribute('Foo\Cached', ['mode' => true]);

$method->addParameter('items')
	->addAttribute('Bar');

echo $class;

Результат:

#[Deprecated]
class Demo
{
	#[WithArguments(1, 2)]
	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->simplifyName('Foo\D')); // в комментариях упрощаем вручную
$method->addParameter('arg')
	->setType('Bar\OtherClass'); // это разрешится до \Bar\OtherClass

echo $namespace;

// или использовать PsrPrinter для вывода, совместимого с PSR-2 / PSR-12
// 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
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);

Результат:

<?php

/**
 * This file is auto-generated.
 */

declare(strict_types=1);

namespace Foo;

class A
{
}

function foo()
{
}

Генерация с использованием Reflection

Другой распространенный случай использования – создание класса или функции на основе существующих:

// создает класс, идентичный классу 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-файла, который ещё не загружен, или строки PHP-кода:

$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
	<?php

	class Demo
	{
		public $foo;
	}
	XX);

Загрузка всего файла PHP, который может содержать несколько классов или даже несколько пространств имён:

$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));

Для этого необходимо установить nikic/php-parser.

Дампинг переменных

Класс Dumper возвращает разобранное строковое представление переменной в формате PHP. Обеспечивает более качественный и четкий вывод, чем родная функция var_export().

$dumper = new Nette\PhpGenerator\Dumper;

$var = ['a', 'b', 123];

echo $dumper->dump($var); // prints ['a', 'b', 123]

Таблица совместимости

PhpGenerator 4.0 совместим с PHP 8.0 – 8.2