Генератор PHP коду

Шукаєте інструмент для генерації PHP коду класів, функцій чи повних файлів?
  • Підтримує всі найновіші можливості PHP (такі як property hooks, enum, атрибути тощо)
  • Дозволяє легко модифікувати існуючі класи
  • Вихідний код відповідає стилю кодування 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("Опис класу.\nДругий рядок\n")
	->addComment('@property-read Nette\Forms\Form $form');

// код легко згенерувати, перетворивши на рядок або використавши echo:
echo $class;

Повертає наступний результат:

/**
 * Опис класу
 * Другий рядок
 *
 * @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;
	}
}

Enums

Переліки (enums), які з'явилися в 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;
}

Ви також можете визначити скалярні еквіваленти та створити таким чином “backed” перелік:

$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');

До кожного case можна додати коментар або #атрибути за допомогою 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;
}

Скорочені стрілочні функції

Ви також можете вивести скорочену анонімну функцію за допомогою printer:

$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(array &$items = [])

Для визначення так званих variadics параметрів (або також splat оператора) служить setVariadic():

$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');

Згенерує:

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);
}

Плейсхолдер для variadic ...?

$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;
}

Printer та відповідність PSR

Для генерації PHP коду служить клас Printer:

$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;
	// кількість порожніх рядків між групами 'use statements' для класів, функцій та констант
	public int $linesBetweenUseTypes = 0;
	// позиція відкриваючої фігурної дужки для функцій та методів
	public bool $bracesOnNextLine = true;
	// розміщуйте один параметр на одному рядку, навіть якщо він має атрибут або підтримується
	public bool $singleParameterOnOneLine = false;
	// пропускає простори імен, які не містять жодного класу чи функції
	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 ви можете додати до всіх класів, методів, властивостей, констант, enum, функцій, closures та параметрів. Як значення параметрів можна використовувати також #літерали.

$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,
	) {
	}
}

Property Hooks

За допомогою property hooks (представлених класом PropertyHook) ви можете визначити операції get та set для властивостей, що є функцією, введеною в PHP 8.4:

$class = new Nette\PhpGenerator\ClassType('Demo');
$prop = $class->addProperty('firstName')
    ->setType('string');

$prop->addHook('set', 'strtolower($value)')
    ->addParameter('value')
	    ->setType('string');

$prop->addHook('get')
	->setBody('return ucfirst($this->firstName);');

echo $class;

Згенерує:

class Demo
{
    public string $firstName {
        set(string $value) => strtolower($value);
        get {
            return ucfirst($this->firstName);
        }
    }
}

Властивості та property hooks можуть бути абстрактними або фінальними:

$class->addProperty('id')
    ->setType('int')
    ->addHook('get')
        ->setAbstract();

$class->addProperty('role')
    ->setType('string')
    ->addHook('set', 'strtolower($value)')
        ->setFinal();

Асиметрична видимість

PHP 8.4 вводить асиметричну видимість для властивостей. Ви можете встановити різні рівні доступу для читання та запису.

Видимість можна встановити або за допомогою методу setVisibility() з двома параметрами, або за допомогою setPublic(), setProtected() чи setPrivate() з параметром mode, який визначає, чи стосується видимість читання чи запису властивості. Режим за замовчуванням — 'get'.

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

$class->addProperty('name')
    ->setType('string')
    ->setVisibility('public', 'private'); // public для читання, private для запису

$class->addProperty('id')
    ->setType('int')
    ->setProtected('set'); // protected для запису

echo $class;

Згенерує:

class Demo
{
    public private(set) string $name;

    protected(set) int $id;
}

Простір імен

Класи, властивості, інтерфейси та переліки (далі — класи) можна групувати в простори імен, представлені класом 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', через визначений use-statement

Спрощене ім'я класу, функції або константи можна, навпаки, перетворити на повне кваліфіковане ім'я за допомогою методу resolveName:

echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'

Перетворення імен класів

Коли клас є частиною простору імен, він відображається дещо інакше: всі типи (наприклад, typehint, типи повернення, ім'я батьківського класу, реалізовані інтерфейси, використані властивості та атрибути) автоматично перетворюються (якщо ви це не вимкнете, див. нижче). Це означає, що ви повинні використовувати повні імена класів у визначеннях, і вони будуть замінені на псевдоніми (відповідно до операторів 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()
{
}

Увага: До файлів неможливо додавати будь-який інший код поза функціями та класами.

Генерація на основі існуючих

Окрім того, що класи та функції можна моделювати за допомогою описаного вище API, їх також можна генерувати автоматично на основі існуючих шаблонів:

// створить клас, ідентичний класу PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);

// створить функцію, ідентичну функції trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');

// створить closure відповідно до зазначеної
$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 файлів

Функції, класи, інтерфейси та enum можна завантажувати також безпосередньо з рядка, що містить 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.

Class Manipulator

Клас ClassManipulator надає інструменти для маніпулювання класами.

$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);

Метод inheritMethod() копіює метод з батьківського класу або реалізованого інтерфейсу до вашого класу. Це дозволяє вам перевизначити метод або розширити його сигнатуру:

$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
  ```

Метод `inheritProperty()` копіює властивість з батьківського класу до вашого класу. Це корисно, коли ви хочете мати у своєму класі таку саму властивість, але, наприклад, з іншим значенням за замовчуванням:

```php
$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');

Метод implement() автоматично реалізує всі методи та властивості з даного інтерфейсу або абстрактного класу у вашому класі:

$manipulator->implement(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.1 сумісний з PHP 8.0 до 8.4.

версія: 4.0