[Go to site: main page, start]

Nette PhpGenerator

Ищете инструмент для порождения 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("Class description.\nSecond line\n")
	->addComment('@property-read Nette\Forms\Form $form');

// код порождаем просто приведением к строке или через echo:
echo $class;

Это вернёт такой результат:

/**
 * Class description.
 * Second line
 *
 * @property-read Nette\Forms\Form $form
 */
final class Demo extends ParentClass implements Countable
{
}

Для порождения кода можно использовать и принтер, который, в отличие от 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;
}

Можно определить и скалярные соответствия и создать backed enum:

$enum = new Nette\PhpGenerator\EnumType('Suit');
$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(array &$items = [])

Чтобы определить параметры с переменным числом аргументов (известные как оператор распаковки), используйте 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);
}

Подстановка для переменного числа аргументов ...?:

$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

Для порождения 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 statement' для классов, функций и констант
	public int $linesBetweenUseTypes = 0;
	// положение открывающей фигурной скобки у функций и методов
	public bool $bracesOnNextLine = true;
	// размещать единственный параметр на одной строке, даже если у него есть атрибут или он продвинутый
	public bool $singleParameterOnOneLine = false;
	// опускает пространства имён, которые не содержат ни классов, ни функций
	public bool $omitEmptyNamespaces = true;
	// размещает declare(strict_types) на той же строке, что и <?php
	public bool $declareOnOpenTag = false;
	// разделитель между правой скобкой и возвращаемым типом функций и методов
	public string $returnTypeColon = ': ';
}

Чем же на самом деле различаются стандартный Printer и PsrPrinter и почему? Почему в пакете нет только одного принтера, PsrPrinter?

Стандартный Printer форматирует код так, как мы делаем это во всей Nette. Поскольку Nette возникла намного раньше PSR, а стандарты PSR к тому же часто выходили с опозданием (иногда через годы после появления новой возможности PHP), стандарт кодирования Nette отличается несколькими мелочами. Главное отличие – использование табуляций вместо пробелов. Мы знаем, что использование табуляций в наших проектах позволяет настраивать ширину, что принципиально важно для людей с нарушениями зрения. Пример мелкого отличия – размещение открывающей фигурной скобки у функций и методов всегда на отдельной строке. Рекомендация PSR кажется нам нелогичной и ведёт к ухудшению понятности кода.

Типы

Любой тип или объединение либо пересечение типов можно передать строкой; для нативных типов можно использовать и заранее определённые константы:

use Nette\PhpGenerator\Type;

$member->setType('array'); // либо Type::Array
$member->setType('?array'); // либо Type::nullable(Type::Array)
$member->setType('array|string'); // либо 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,
	) {
	}
}

Хуки свойств

С помощью хуков свойств (представленных классом 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);
        }
    }
}

Свойства и хуки свойств могут быть абстрактными или финальными:

$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

И наоборот, упрощённое имя класса, функции или константы можно преобразовать обратно в полное методом 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()
{
}

В файл можно вставлять и существующие объекты классов, функций и пространств имён методом add():

$file = new Nette\PhpGenerator\PhpFile;
$class = new Nette\PhpGenerator\ClassType('Demo');
$file->add($class);

Обратите внимание: никакой дополнительный код (например, echo 'hello') в файлы вне функций, классов или пространств имён добавить нельзя.

Порождение из существующих элементов

Кроме моделирования классов и функций через описанный выше 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-файлов

Функции, классы, интерфейсы и перечисления можно загрузить и прямо из строки с 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');

Метод 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.2 совместим с PHP от 8.1 до 8.5.

Если вы переходите на более новую версию, посмотрите страницу обновления.

версия: 4.x