Generator kodu PHP

Szukasz narzędzia do generowania kodu PHP dla klas, funkcji lub całych plików?
  • Obsługuje wszystkie najnowsze funkcje PHP (takie jak haki właściwości, enumy, atrybuty itp.).
  • Pozwala na łatwą modyfikację istniejących klas
  • Wyjście zgodne ze stylem kodowania PSR-12 / PER
  • Dojrzała, stabilna i szeroko stosowana biblioteka

Instalacja

Pobierz i zainstaluj pakiet za pomocą Composera:

composer require nette/php-generator

W celu uzyskania informacji o kompatybilności z PHP, patrz tabela.

Klasy

Zacznijmy od prostego przykładu generowania klasy za pomocą 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');

// aby wygenerować kod PHP wystarczy rzutować na string lub użyć echo:
echo $class;

Wyrenderuje on taki wynik:

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

Do wygenerowania kodu możemy również użyć drukarki, którą w przeciwieństwie do echo $class, będziemy mogli dodatkowo skonfigurować:

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

Możemy dodać stałe (klasa Constant) i właściwości (klasa Property):

$class->addConstant('ID', 123)
	->setProtected() // stała widoczność
	->setType('int')
	->setFinal();

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

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

Generuje:

final protected const int ID = 123;

/** @var int[] */
private static $items = [1, 2, 3];

public ?array $list = null;

I możemy dodać metody:

$method = $class->addMethod('count')
	->addComment('Count it.')
	->setFinal()
	->setProtected()
	->setReturnType('?int') // metoda return type
	->setBody('return count($items ?: $this->items);');

$method->addParameter('items', []) // $items = []
	->setReference()           // &$items = []
	->setType('array');        // array &$items = []

Wynika z tego, że:

/**
 * Count it.
 */
final protected function count(array &$items = []): ?int
{
	return count($items ?: $this->items);
}

Parametry promowane wprowadzone przez PHP 8.0 mogą być przekazywane do konstruktora:

$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
	->setPrivate();

Skutkuje to:

public function __construct(
	public $name,
	private $args = [],
) {
}

Właściwości i klasy readonly mogą być oznaczone poprzez setReadOnly().


Jeśli dodana właściwość, stała, metoda lub parametr już istnieje, rzuca wyjątek.

Członków można usunąć używając removeProperty(), removeConstant(), removeMethod() lub removeParameter().

Możesz również dodać do klasy istniejące obiekty Method, Property lub 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);

Możesz sklonować istniejące metody, właściwości i stałe z inną nazwą używając cloneWithName():

$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);

Interface lub Trait

Można tworzyć interfejsy i cechy (klasy InterfaceTypeTraitType):

$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');

Wykorzystanie cech:

$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
	->addResolution('sayHello as protected')
	->addComment('@use MyTrait<Foo>');
echo $class;

Wynik:

class Demo
{
	use SmartObject;
	/** @use MyTrait<Foo> */
	use MyTrait {
		sayHello as protected;
	}
}

Enums

Możesz łatwo stworzyć enum, które przynosi PHP 8.1 (klasa EnumType):

$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');

echo $enum;

Wynik:

enum Suit
{
	case Clubs;
	case Diamonds;
	case Hearts;
	case Spades;
}

Możesz również zdefiniować skalarne odpowiedniki dla przypadków, aby utworzyć backed enum:

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

Możliwe jest dodanie komentarza lub atrybutów do każdego przypadku za pomocą addComment() lub addAttribute().

Klasa anonimowa

Podaj null jako nazwę i masz klasę anonimową:

$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
	->addParameter('foo');

echo '$obj = new class ($val) ' . $class . ';';

Wynik:

$obj = new class ($val) {

	public function __construct($foo)
	{
	}
};

Funkcja globalna

Kod funkcji wygeneruje klasa GlobalFunction:

$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;

// lub użyć PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);

Result:

function foo($a, $b)
{
	return $a + $b;
}

Zamknięcie

Kod zamknięcia wygeneruje klasę Closure:

$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
	->setReference();
echo $closure;

// lub użyć PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);

Wynik:

function ($a, $b) use (&$c) {
	return $a + $b;
}

Funkcja strzałki

Możesz również wydrukować zamknięcie jako funkcję strzałki za pomocą drukarki:

$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');

echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);

Wynik:

fn($a, $b) => $a + $b

Sygnatura metody i funkcji

Metody są reprezentowane przez klasę Method. Możesz ustawić widoczność, wartość zwrotną, dodać komentarze, atrybuty itp:

$method = $class->addMethod('count')
	->addComment('Count it.')
	->setFinal()
	->setProtected()
	->setReturnType('?int');

Każdy parametr jest reprezentowany przez klasę Parameter. Ponownie, możesz ustawić każdą możliwą właściwość:

$method->addParameter('items', []) // $items = []
	->setReference() // &$items = []
	->setType('array'); // array &$items = []

// function count(&$items = [])

Aby zdefiniować tzw. parametry variadics (lub również operator splat, spread, elipsa, rozpakowywanie czy trzy kropki), należy użyć setVariadic():

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

Generates:

function count(...$items)
{
}

Metoda i ciało funkcji

Ciało może być przekazane do metody setBody() jednorazowo lub sekwencyjnie (linia po linii) poprzez wielokrotne wywołanie addBody():

$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;

Result

function foo()
{
	$a = rand(10, 20);
	return $a;
}

Możesz użyć specjalnych zamienników dla poręcznego sposobu wstrzykiwania zmiennych.

Proste symbole miejsc ?

$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;

Wynik:

function foo()
{
	return substr('any string', 3);
}

Variadic placeholder ...?

$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;

Wynik:

function foo()
{
	myfunc(1, 2, 3);
}

Możesz również użyć PHP 8 nazwanych parametrów używając placeholder ...?:

$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);

// myfunc(foo: 1, bar: true);

Ucieknij od placeholder używając slash \?

$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;

Wynik:

function foo($a)
{
	return $a ? 10 : 3;
}

Drukarki i zgodność z PSR

Klasa Printer służy do generowania kodu PHP:

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

$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // to samo co: echo $class

Może generować kod dla wszystkich innych elementów, oferując metody takie jak printFunction(), printNamespace(), itp.

Dodatkowo dostępna jest klasa PsrPrinter, której dane wyjściowe są zgodne ze stylem kodowania PSR-2 / PSR-12 / PER:

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

Chcesz dostosować zachowanie do swoich potrzeb? Utwórz własną drukarkę, dziedzicząc z klasy Printer. Możesz ponownie skonfigurować te zmienne:

class MyPrinter extends Nette\PhpGenerator\Printer
{
	// długość linii, po której linia zostanie przerwana
	public int $wrapLength = 120;
	// znak wcięcia, może być zastąpiony ciągiem spacji
	public string $indentation = "\t";
	// liczba pustych linii między właściwościami
	public int $linesBetweenProperties = 0;
	// liczba pustych linii między metodami
	public int $linesBetweenMethods = 2;
	// liczba pustych linii między grupami instrukcji użycia dla klas, funkcji i stałych
	public int $linesBetweenUseTypes = 0;
	// pozycja nawiasu klamrowego otwierającego dla funkcji i metod
	public bool $bracesOnNextLine = true;
	// umieszczenie jednego parametru w jednej linii, nawet jeśli ma atrybut lub jest promowany
	public bool $singleParameterOnOneLine = false;
	// omits namespaces that do not contain any class or function
	public bool $omitEmptyNamespaces = true;
	// separator między prawym nawiasem a typem zwracanej funkcji i metody
	public string $returnTypeColon = ': ';
}

Czym i dlaczego dokładnie różnią się standardowe Printer i PsrPrinter? Dlaczego w pakiecie nie ma tylko jednej drukarki, PsrPrinter?

Standard Printer formatuje kod tak, jak robimy to we wszystkich Nette. Ponieważ Nette powstało znacznie wcześniej niż PSR, a także dlatego, że PSR przez wiele lat nie dostarczało standardów na czas, ale czasami nawet z kilkuletnim opóźnieniem od wprowadzenia nowej funkcji w PHP, spowodowało to kilka drobnych różnic w standardzie kodowania. Większą różnicą jest właśnie używanie tabulatorów zamiast spacji. Wiemy, że stosując tabulatory w naszych projektach umożliwiamy dostosowanie szerokości, co jest istotne dla osób z wadami wzroku. Przykładem drobnej różnicy jest umieszczenie nawiasu klamrowego w osobnej linii dla funkcji i metod oraz zawsze. Uważamy, że zalecenie PSR jest nielogiczne i prowadzi do zmniejszenia przejrzystości kodu.

Typy

Każdy typ lub typ unii / przecięcia może być przekazany jako ciąg, możesz również użyć predefiniowanych stałych dla typów natywnych:

use Nette\PhpGenerator\Type;

$member->setType('array'); // lub 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'); // lub Type::intersection(Foo::class, Bar::class)
$member->setType(null); // usuwa typ

To samo dotyczy metody setReturnType().

Literały

Dzięki Literal możesz przekazać dowolny kod PHP do, na przykład, domyślnych wartości właściwości lub parametrów itp:

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;

Wynik:

class Demo
{
	public $foo = Iterator::SELF_FIRST;

	public function bar($id = 1 + 2)
	{
	}
}

Możesz również przekazać parametry do Literal i mieć je sformatowane w poprawny kod PHP za pomocą specjalnych placeholderów:

new Literal('substr(?, ?)', [$a, $b]);
// generuje, na przykład: substr('hello', 5);

Literał reprezentujący utworzenie nowego obiektu jest łatwo generowany przez metodę new:

Literal::new(Demo::class, [$a, 'foo' => $b]);
// generuje na przykład: new Demo(10, foo: 20)

Atrybuty

Możesz dodać atrybuty PHP 8 do wszystkich klas, metod, właściwości, stałych, przypadków enum, funkcji, domknięć i parametrów. Literały mogą być również używane jako wartości parametrów.

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

Wynik:

#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])]
class Demo
{
	#[Deprecated]
	public $list;


	#[Foo\Cached(mode: true)]
	public function count(
		#[Bar]
		$items,
	) {
	}
}

Haki nieruchomości

Można również definiować haki właściwości (reprezentowane przez klasę PropertyHook) dla operacji pobierania i ustawiania, funkcja wprowadzona w 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;

To generuje:

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

Właściwości i haki właściwości mogą być abstrakcyjne lub końcowe:

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

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

Asymetryczna widoczność

PHP 8.4 wprowadza asymetryczną widoczność właściwości. Można ustawić różne poziomy dostępu dla odczytu i zapisu.

Widoczność można ustawić za pomocą metody setVisibility() z dwoma parametrami lub za pomocą setPublic(), setProtected(), lub setPrivate() z parametrem mode, który określa, czy widoczność dotyczy pobierania czy ustawiania właściwości. Domyślnym trybem jest 'get'.

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

$class->addProperty('name')
    ->setType('string')
    ->setVisibility('public', 'private'); // public for read, private for write

$class->addProperty('id')
    ->setType('int')
    ->setProtected('set'); // protected for write

echo $class;

To generuje:

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

    protected(set) int $id;
}

Przestrzeń nazw

Klasy, cechy, interfejsy i enum (dalej klasy) mogą być grupowane w przestrzenie nazw(PhpNamespace):

$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');

// tworzyć nowe klasy w przestrzeni nazw
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');

// lub wstawić istniejącą klasę do przestrzeni nazw
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);

Jeśli klasa już istnieje, to rzuca wyjątek.

Możesz zdefiniować oświadczenia o użyciu:

// 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');

Aby uprościć w pełni kwalifikowaną nazwę klasy, funkcji lub stałej zgodnie ze zdefiniowanymi aliasami, użyj metody simplifyName:

echo $namespace->simplifyName('Foo\Bar'); // 'Bar', ponieważ 'Foo' jest bieżącą przestrzenią nazw
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', z powodu zdefiniowanej deklaracji użycia

I odwrotnie, można przekształcić uproszczoną nazwę klasy, funkcji lub stałej na w pełni kwalifikowaną, używając metody resolveName:

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

Class Names Resolving (rozwiązywanie nazw klas)

Gdy klasa jest częścią przestrzeni nazw, jest renderowana nieco inaczej: wszystkie typy (np. podpowiedzi typów, typy zwracane, nazwa klasy nadrzędnej, zaimplementowane interfejsy, używane cechy i atrybuty) są automatycznie rozwiązywane (chyba że to wyłączysz, patrz poniżej). Oznacza to, że musisz używać w pełni kwalifikowanych nazw klas w definicjach, a zostaną one zastąpione aliasami (opartymi na klauzulach użycia) lub w pełni kwalifikowanymi nazwami w wynikowym kodzie:

$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');

$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // to się uprości do A
	->addTrait('Bar\AliasedClass'); // uprości się do AliasedClass

$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo')); // w komentarzach uprościć ręcznie
$method->addParameter('arg')
	->setType('Bar\OtherClass'); // zostanie rozwiązany do klasy \Bar\Bar\Class

echo $namespace;

// lub użyć PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);

Wynik:

namespace Foo;

use Bar\AliasedClass;

class Demo implements A
{
	use AliasedClass;

	/**
	 * @return D
	 */
	public function method(\Bar\OtherClass $arg)
	{
	}
}

Auto-resolving można wyłączyć w ten sposób:

$printer = new Nette\PhpGenerator\Printer; // lub PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);

Pliki PHP.

Klasy, funkcje i przestrzenie nazw mogą być pogrupowane w pliki PHP reprezentowane przez klasę PhpFile:

$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes(); // dodaje declare(strict_types=1)

$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');

// lub
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');

echo $file;

// lub użyj PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);

Wynik:

<?php

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

declare(strict_types=1);

namespace Foo;

class A
{
}

function foo()
{
}

Uwaga: Do plików nie można dodawać żadnego dodatkowego kodu poza funkcjami i klasami.

Generowanie według istniejących

Oprócz możliwości modelowania klas i funkcji za pomocą opisanego powyżej API, można również zlecić ich automatyczne generowanie według istniejących:

// tworzy klasę identyczną z klasą PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);

// tworzy funkcję identyczną z trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');

// tworzy zamknięcie identyczne jak
$closure = Nette\PhpGenerator\Closure::from(
	function (stdClass $a, $b = null) {},
);

Ciała funkcji i metod są domyślnie puste. Jeśli chcesz je również załadować, użyj tego sposobu (wymaga on zainstalowania nikic/php-parser ):

$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);

$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);

Ładowanie z pliku PHP

Można też ładować funkcje, klasy, interfejsy i enumy bezpośrednio z ciągu kodu PHP. Na przykład tworzymy obiekt ClassType w ten sposób:

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

	class Demo
	{
		public $foo;
	}
	XX);

Podczas ładowania klas z kodu PHP, komentarze jednolinijkowe poza ciałami metod są ignorowane (np. dla właściwości itp.), ponieważ ta biblioteka nie posiada API do pracy z nimi.

Możesz również załadować bezpośrednio cały plik PHP, który może zawierać dowolną liczbę klas, funkcji, a nawet wiele przestrzeni nazw:

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

Ładowany jest również początkowy komentarz do pliku oraz deklaracja strict_types. Z drugiej strony, wszystkie inne globalne kody są ignorowane.

Wymaga to zainstalowania nikic/php-parser.

Jeśli musisz manipulować globalnym kodem w plikach lub pojedynczymi stwierdzeniami w ciałach metod, lepiej jest użyć bezpośrednio biblioteki nikic/php-parser.

Class Manipulator

Klasa ClassManipulator zapewnia narzędzia do manipulowania klasami.

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

Metoda inheritMethod() kopiuje metodę z klasy nadrzędnej lub zaimplementowanego interfejsu do klasy użytkownika. Pozwala to na nadpisanie metody lub rozszerzenie jej sygnatury:

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

Metoda inheritProperty() kopiuje właściwość z klasy nadrzędnej do klasy użytkownika. Jest to przydatne, gdy chcesz mieć tę samą właściwość w swojej klasie, ale prawdopodobnie z inną wartością domyślną:

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

Metoda implement() automatycznie implementuje wszystkie metody i właściwości z danego interfejsu lub klasy abstrakcyjnej:

$manipulator->implement(SomeInterface::class);
// Teraz twoja klasa implementuje interfejs SomeInterface i zawiera wszystkie jego metody

Zrzutka zmiennych

Dumper zwraca parsowalną reprezentację zmiennej w PHP. Zapewnia lepsze i bardziej przejrzyste wyjście niż funkcja natywna var_export().

$dumper = new Nette\PhpGenerator\Dumper;

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

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

Tabela kompatybilności

PhpGenerator 4.1 jest kompatybilny z PHP 8.0 do 8.4.

wersja: 4.0