Generator kodu PHP
- Obsługuje wszystkie najnowsze funkcje PHP (takie jak property hooks, enumy, atrybuty itp.)
- Umożliwia łatwą modyfikację istniejących klas
- Kod wyjściowy jest zgodny ze stylem kodowania PSR-12 / PER
- Dojrzała, stabilna i szeroko stosowana biblioteka
Instalacja
Bibliotekę pobierzesz i zainstalujesz za pomocą narzędzia Composer:
composer require nette/php-generator
Kompatybilność z PHP znajdziesz w tabeli.
Klasy
Zacznijmy od razu od przykładu tworzenia klasy za pomocą ClassType:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class
->setFinal()
->setExtends(ParentClass::class)
->addImplement(Countable::class)
->addComment("Opis klasy.\nDruga linia\n")
->addComment('@property-read Nette\Forms\Form $form');
// kod można łatwo wygenerować przez rzutowanie na ciąg znaków lub użycie echo:
echo $class;
Zwraca następujący wynik:
/**
* Opis klasy
* Druga linia
*
* @property-read Nette\Forms\Form $form
*/
final class Demo extends ParentClass implements Countable
{
}
Do wygenerowania kodu możemy również użyć tzw. printera, który w przeciwieństwie do echo $class
będziemy
mogli dalej konfigurować:
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);
Możemy dodać stałe (klasa Constant) i zmienne (klasa Property):
$class->addConstant('ID', 123)
->setProtected() // widoczność stałych
->setType('int')
->setFinal();
$class->addProperty('items', [1, 2, 3])
->setPrivate() // lub setVisibility('private')
->setStatic()
->addComment('@var int[]');
$class->addProperty('list')
->setType('?array')
->setInitialized(); // wypisze '= null'
Wygeneruje:
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('Policz to.')
->setFinal()
->setProtected()
->setReturnType('?int') // typy zwracane w metodach
->setBody('return count($items ?: $this->items);');
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
Wynikiem jest:
/**
* Policz to.
*/
final protected function count(array &$items = []): ?int
{
return count($items ?: $this->items);
}
Promowane parametry wprowadzone w PHP 8.0 można przekazać do konstruktora:
$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
->setPrivate();
Wynikiem jest:
public function __construct(
public $name,
private $args = [],
) {
}
Właściwości i klasy przeznaczone tylko do odczytu można oznaczyć za pomocą funkcji setReadOnly()
.
Jeśli dodawana właściwość, stała, metoda lub parametr już istnieją, zostanie rzucony wyjątek.
Członków klasy można usunąć za pomocą removeProperty()
, removeConstant()
,
removeMethod()
lub removeParameter()
.
Do klasy można również dodać 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żna również klonować istniejące metody, właściwości i stałe pod inną nazwą za pomocą
cloneWithName()
:
$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);
Interfejs lub Trait
Można tworzyć interfejsy i traity (klasy InterfaceType i TraitType):
$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');
Używanie traitów:
$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;
}
}
Enumy
Wyliczenia, które wprowadza PHP 8.1, można łatwo utworzyć w ten sposób: (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żna również zdefiniować ekwiwalenty skalarne i utworzyć w ten sposób “backed” wyliczenie:
$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');
Do każdego case można dodać komentarz lub atrybuty za pomocą addComment()
lub addAttribute()
.
Klasy anonimowe
Jako nazwę przekażemy null
i mamy 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)
{
}
};
Funkcje globalne
Kod funkcji generuje klasa GlobalFunction:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;
// lub użyj PsrPrinter dla wyjścia zgodnego z PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);
Wynik:
function foo($a, $b)
{
return $a + $b;
}
Funkcje anonimowe
Kod funkcji anonimowych generuje klasa 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żyj 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;
}
Skrócone funkcje strzałkowe
Można również wypisać skróconą funkcję anonimową za pomocą printera:
$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
Sygnatury metod i funkcji
Metody reprezentuje klasa Method. Można ustawić widoczność, wartość zwracaną, dodać komentarze, atrybuty itp.:
$method = $class->addMethod('count')
->addComment('Policz to.')
->setFinal()
->setProtected()
->setReturnType('?int');
Poszczególne parametry reprezentuje klasa Parameter. Ponownie można ustawić wszystkie możliwe właściwości:
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
// function count(array &$items = [])
Do definicji tzw. parametrów variadics (lub też operatora splat) służy setVariadic()
:
$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');
Wygeneruje:
function count(...$items)
{
}
Ciała metod i funkcji
Ciało można przekazać naraz metodzie setBody()
lub stopniowo (linia po linii) przez wielokrotne wywołanie
addBody()
:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;
Wynik
function foo()
{
$a = rand(10, 20);
return $a;
}
Można użyć specjalnych symboli zastępczych do łatwego wstawiania zmiennych.
Proste symbole zastępcze ?
$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);
}
Symbol zastępczy dla variadic ...?
$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;
Wynik:
function foo()
{
myfunc(1, 2, 3);
}
Można również użyć nazwanych parametrów dla PHP 8 za pomocą ...?:
$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);
// myfunc(foo: 1, bar: true);
Symbol zastępczy escapuje się za pomocą ukośnika \?
$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;
}
Printer i zgodność z PSR
Do generowania kodu PHP służy klasa Printer:
$class = new Nette\PhpGenerator\ClassType('Demo');
// ...
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // to samo, co: echo $class
Potrafi wygenerować kod wszystkich innych elementów, oferuje metody takie jak printFunction()
,
printNamespace()
, itd.
Dostępna jest również klasa PsrPrinter
, której wyjście jest zgodne ze stylem kodowania PSR-2 / PSR-12
/ PER:
$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);
Potrzebujesz dostosować zachowanie do własnych potrzeb? Utwórz własną wersję, dziedzicząc po klasie
Printer
. Można przekonfigurować następujące zmienne:
class MyPrinter extends Nette\PhpGenerator\Printer
{
// długość linii, po której następuje zawijanie wiersza
public int $wrapLength = 120;
// znak wcięcia, może być zastąpiony sekwencją 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 'use statements' dla klas, funkcji i stałych
public int $linesBetweenUseTypes = 0;
// pozycja otwierającego nawiasu klamrowego dla funkcji i metod
public bool $bracesOnNextLine = true;
// umieść jeden parametr w jednej linii, nawet jeśli ma atrybut lub jest promowany
public bool $singleParameterOnOneLine = false;
// pomija przestrzenie nazw, które nie zawierają żadnej klasy ani funkcji
public bool $omitEmptyNamespaces = true;
// separator między prawym nawiasem a typem zwracanym funkcji i metod
public string $returnTypeColon = ': ';
}
Jak i dlaczego właściwie różnią się standardowy Printer
i PsrPrinter
? Dlaczego w pakiecie nie
ma tylko jednego printera, a mianowicie PsrPrinter
?
Standardowy Printer
formatuje kod tak, jak to robimy w całym Nette. Ponieważ Nette powstało znacznie wcześniej
niż PSR, a także dlatego, że PSR przez długie lata nie dostarczało standardów na czas, ale na przykład z kilkuletnim
opóźnieniem od wprowadzenia nowej funkcji w PHP, doszło do tego, że standard kodowania różni się w kilku drobnych szczegółach.
Większą różnicą jest tylko używanie tabulatorów zamiast spacji. Wiemy, że używanie tabulatorów w naszych projektach
umożliwia dostosowanie szerokości, co jest niezbędne dla osób z wadami wzroku.
Przykładem drobnej różnicy jest umieszczenie nawiasu klamrowego na osobnej linii w przypadku funkcji i metod, i to zawsze.
Zalecenie PSR wydaje nam się nielogiczne i prowadzi do zmniejszenia czytelności kodu.
Typy
Każdy typ lub typ union/intersection można przekazać jako ciąg znaków, można 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
Za pomocą Literal
można przekazywać dowolny kod PHP, na przykład dla 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żna również przekazać parametry do Literal
i pozwolić je sformatować do poprawnego kodu PHP za pomocą symboli zastępczych:
new Literal('substr(?, ?)', [$a, $b]);
// generuje na przykład: substr('hello', 5);
Literał reprezentujący utworzenie nowego obiektu można łatwo wygenerować za pomocą metody new
:
Literal::new(Demo::class, [$a, 'foo' => $b]);
// generuje na przykład: new Demo(10, foo: 20)
Atrybuty
Atrybuty PHP 8 można dodać do wszystkich klas, metod, właściwości, stałych, enumów, funkcji, closures i parametrów. Jako wartości parametrów można używać również literaly.
$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,
) {
}
}
Property Hooks
Za pomocą property hooks (reprezentowanych przez klasę PropertyHook) można zdefiniować operacje get i set dla właściwości, co jest funkcją wprowadzoną 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;
Wygeneruje:
class Demo
{
public string $firstName {
set(string $value) => strtolower($value);
get {
return ucfirst($this->firstName);
}
}
}
Właściwości i property hooks mogą być abstrakcyjne lub finalne:
$class->addProperty('id')
->setType('int')
->addHook('get')
->setAbstract();
$class->addProperty('role')
->setType('string')
->addHook('set', 'strtolower($value)')
->setFinal();
Widoczność asymetryczna
PHP 8.4 wprowadza widoczność asymetryczną dla właściwości. Można ustawić różne poziomy dostępu dla odczytu i zapisu.
Widoczność można ustawić albo za pomocą metody setVisibility()
z dwoma parametrami, albo za pomocą
setPublic()
, setProtected()
lub setPrivate()
z parametrem mode
, który
określa, czy widoczność dotyczy odczytu czy zapisu właściwości. Domyślny tryb to 'get'
.
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addProperty('name')
->setType('string')
->setVisibility('public', 'private'); // public dla odczytu, private dla zapisu
$class->addProperty('id')
->setType('int')
->setProtected('set'); // protected dla zapisu
echo $class;
Wygeneruje:
class Demo
{
public private(set) string $name;
protected(set) int $id;
}
Przestrzeń nazw
Klasy, właściwości, interfejsy i wyliczenia (dalej zwane klasami) można grupować w przestrzenie nazw reprezentowane przez klasę PhpNamespace:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
// tworzymy nowe klasy w przestrzeni nazw
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');
// lub wstawiamy istniejącą klasę do przestrzeni nazw
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);
Jeśli klasa już istnieje, zostanie rzucony wyjątek.
Można zdefiniować klauzule 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');
Aby uprościć w pełni kwalifikowaną nazwę klasy, funkcji lub stałej zgodnie z zdefiniowanymi aliasami, użyj metody
simplifyName
:
echo $namespace->simplifyName('Foo\Bar'); // 'Bar', ponieważ 'Foo' to bieżąca przestrzeń nazw
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', z powodu zdefiniowanego use-statement
Uproszczoną nazwę klasy, funkcji lub stałej można na odwrót przekształcić na w pełni kwalifikowaną nazwę za pomocą
metody resolveName
:
echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'
Tłumaczenie nazw klas
Gdy klasa jest częścią przestrzeni nazw, jest renderowana nieco inaczej: wszystkie typy (na przykład typehinty, typy zwracane, nazwa klasy nadrzędnej, implementowane interfejsy, używane właściwości i atrybuty) są automatycznie tłumaczone (jeśli tego nie wyłączysz, patrz poniżej). Oznacza to, że w definicjach musisz używać pełnych nazw klas, a te zostaną zastąpione aliasami (zgodnie z klauzulami use) 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') // zostanie uproszczone do A
->addTrait('Bar\AliasedClass'); // zostanie uproszczone do AliasedClass
$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // w komentarzach upraszczamy ręcznie
$method->addParameter('arg')
->setType('Bar\OtherClass'); // zostanie przetłumaczone na \Bar\OtherClass
echo $namespace;
// lub użyj 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)
{
}
}
Automatyczne tłumaczenie 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 można grupować w pliki PHP reprezentowane przez klasę PhpFile:
$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('Ten plik jest generowany automatycznie.');
$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
/**
* Ten plik jest generowany automatycznie.
*/
declare(strict_types=1);
namespace Foo;
class A
{
}
function foo()
{
}
Uwaga: Do plików nie można dodawać żadnego innego kodu poza funkcjami i klasami.
Generowanie na podstawie istniejących
Oprócz tego, że klasy i funkcje można modelować za pomocą opisanego powyżej API, można je również wygenerować automatycznie na podstawie istniejących wzorców:
// tworzy klasę taką samą jak klasa PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);
// tworzy funkcję identyczną z funkcją trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');
// tworzy closure na podstawie podanej
$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 instalacji
pakietu nikic/php-parser
):
$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);
$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);
Wczytywanie z plików PHP
Funkcje, klasy, interfejsy i enumy można wczytywać również bezpośrednio z ciągu znaków zawierającego kod PHP. Na
przykład w ten sposób utworzymy obiekt ClassType
:
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
<?php
class Demo
{
public $foo;
}
XX);
Podczas wczytywania klas z kodu PHP, jednoliniowe komentarze poza ciałami metod są ignorowane (np. przy właściwościach itp.), ponieważ ta biblioteka nie ma API do pracy z nimi.
Można również wczytać bezpośrednio cały plik PHP, który może zawierać dowolną liczbę klas, funkcji lub nawet przestrzeni nazw:
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
Wczytany zostanie również komentarz początkowy pliku i deklaracja strict_types
. Natomiast cały pozostały kod
globalny jest ignorowany.
Wymagane jest zainstalowanie nikic/php-parser
.
Jeśli potrzebujesz manipulować globalnym kodem w plikach lub poszczególnymi instrukcjami w ciałach metod,
lepiej użyć bezpośrednio biblioteki nikic/php-parser
.
Class Manipulator
Klasa ClassManipulator dostarcza narzędzi do manipulacji klasami.
$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);
Metoda inheritMethod()
kopiuje metodę z klasy nadrzędnej lub implementowanego interfejsu do Twojej klasy.
Pozwala to nadpisać metodę lub rozszerzyć jej sygnaturę:
$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
```
Metoda `inheritProperty()` kopiuje właściwość z klasy nadrzędnej do Twojej klasy. Jest to przydatne, gdy chcesz mieć w swojej klasie tę samą właściwość, ale na przykład z inną wartością domyślną:
```php
$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 w Twojej klasie:
$manipulator->implement(SomeInterface::class);
// Teraz Twoja klasa implementuje SomeInterface i zawiera wszystkie jego metody
Zrzut zmiennych
Klasa Dumper
konwertuje zmienną na parsowalny kod PHP. Dostarcza lepsze i bardziej przejrzyste wyjście niż
standardowa funkcja var_export()
.
$dumper = new Nette\PhpGenerator\Dumper;
$var = ['a', 'b', 123];
echo $dumper->dump($var); // wypisze ['a', 'b', 123]
Tabela kompatybilności
PhpGenerator 4.1 jest kompatybilny z PHP 8.0 do 8.4.