Nette PhpGenerator
- Umí všechny nejnovější vychytávky v PHP (jako property hooks, enumy, atributy atd.)
- Umožní vám snadno modifikovat existující třídy
- Výstupní kód je v souladu s PSR-12 / PER coding style
- Zralá, stabilní a široce používaná knihovna
Instalace
Knihovnu stáhnete a nainstalujete pomocí nástroje Composer:
composer require nette/php-generator
Kompatibilitu s PHP naleznete v tabulce.
Třídy
Začněme rovnou příkladem tvorby třídy pomocí ClassType:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class
->setFinal()
->setExtends(ParentClass::class)
->addImplement(Countable::class)
->addComment("Popis třídy.\nDruhý řádek\n")
->addComment('@property-read Nette\Forms\Form $form');
// kód jednoduše vygenerujete přetypováním na řetězec nebo použitím echo:
echo $class;
Vrátí následující výsledek:
/**
* Popis třídy
* Druhý řádek
*
* @property-read Nette\Forms\Form $form
*/
final class Demo extends ParentClass implements Countable
{
}
K vygenerování kódu můžeme také použít tzv. printer, který na rozdíl od echo $class budeme moci dále konfigurovat:
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);
Můžeme přidat konstanty (třída Constant) a proměnné (třída Property):
$class->addConstant('ID', 123)
->setProtected() // viditelnost konstant
->setType('int')
->setFinal();
$class->addProperty('items', [1, 2, 3])
->setPrivate() // nebo setVisibility('private')
->setStatic()
->addComment('@var int[]');
$class->addProperty('list')
->setType('?array')
->setInitialized(); // vypíše '= null'
Vygeneruje:
final protected const int ID = 123;
/** @var int[] */
private static $items = [1, 2, 3];
public ?array $list = null;
A můžeme přidat metody:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int') // návratové typy u metod
->setBody('return count($items ?: $this->items);');
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
Výsledkem je:
/**
* Count it.
*/
final protected function count(array &$items = []): ?int
{
return count($items ?: $this->items);
}
Propagované parametry zavedené PHP 8.0 lze předat konstruktoru:
$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
->setPrivate();
Výsledkem je:
public function __construct(
public $name,
private $args = [],
) {
}
Vlastnosti a třídy určené pouze pro čtení lze označit pomocí funkce setReadOnly().
Pokud přidaná vlastnost, konstanta, metoda nebo parametr již existují, vyhodí se výjimka.
Členy třídy lze odebrat pomocí removeProperty(), removeConstant(), removeMethod()
nebo removeParameter().
Do třídy můžete také přidat existující objekty Method, Property nebo
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);
Můžete také klonovat stávající metody, vlastnosti a konstanty pod jiným názvem pomocí
cloneWithName():
$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);
Interface nebo traita
Můžete vytvářet rozhraní a traity (třídy InterfaceType a TraitType):
$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');
Používání trait:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
->addResolution('sayHello as protected')
->addComment('@use MyTrait<Foo>');
echo $class;
Výsledek:
class Demo
{
use SmartObject;
/** @use MyTrait<Foo> */
use MyTrait {
sayHello as protected;
}
}
Enums
Výčty, které přináší PHP 8.1, můžete snadno vytvořit takto: (třída EnumType):
$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');
echo $enum;
Výsledek:
enum Suit
{
case Clubs;
case Diamonds;
case Hearts;
case Spades;
}
Můžete také definovat skalární ekvivalenty a vytvořit tak „backed“ výčet:
$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');
Ke každému case je možné přidat komentář nebo atributy pomocí
addComment() nebo addAttribute().
Anonymní třídy
Jako název předáme null a máme anonymní třídu:
$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
->addParameter('foo');
echo '$obj = new class ($val) ' . $class . ';';
Výsledek:
$obj = new class ($val) {
public function __construct($foo)
{
}
};
Globální funkce
Kód funkcí generuje třída GlobalFunction:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;
// nebo použijte PsrPrinter pro výstup v souladu s PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);
Výsledek:
function foo($a, $b)
{
return $a + $b;
}
Anonymní funkce
Kód anonymních funkcí generuje třída Closure:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
->setReference();
echo $closure;
// nebo použijte PsrPrinter pro výstup v souladu s PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);
Výsledek:
function ($a, $b) use (&$c) {
return $a + $b;
}
Zkrácené arrow funkce
Můžete také vypsat zkrácenou anonymní funkci pomocí printeru:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');
echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);
Výsledek:
fn($a, $b) => $a + $b
Signatury metod a funkcí
Metody reprezentuje třída Method. Můžete nastavit viditelnost, návratovou hodnotu, přidat komentáře, atributy atd:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int');
Jednotlivé parametry reprezentuje třídy Parameter. Opět můžete nastavit všechny myslitelné vlastnosti:
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
// function count(array &$items = [])
Pro definici tzv. variadics parametrů (nebo též splat operátor) slouží setVariadic():
$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');
Vygeneruje:
function count(...$items)
{
}
Těla metod a funkcí
Tělo lze předat najednou metodě setBody() nebo postupně (po řádcích) opakovaným voláním
addBody():
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;
Výsledek
function foo()
{
$a = rand(10, 20);
return $a;
}
Můžete použít speciální zástupné znaky pro snadné vkládání proměnných.
Jednoduché zástupné symboly ?
$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;
Výsledek
function foo()
{
return substr('any string', 3);
}
Zástupný znak pro variadic ...?
$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;
Výsledek:
function foo()
{
myfunc(1, 2, 3);
}
Můžete také použít pojmenované parametry pro PHP 8 pomocí ...?:
$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);
// myfunc(foo: 1, bar: true);
Zástupný symbol se escapuje pomocí lomítka \?
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;
Výsledek:
function foo($a)
{
return $a ? 10 : 3;
}
Printer a soulad s PSR
Ke generování PHP kódu slouží třída Printer:
$class = new Nette\PhpGenerator\ClassType('Demo');
// ...
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // totéž, jako: echo $class
Umí vygenerovat kód všech dalších prvků, nabízí metody jako printFunction(),
printNamespace(), atd.
K dispozici je také třída PsrPrinter, jejíž výstup je v souladu s PSR-2 / PSR-12 / PER coding style:
$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);
Potřebujete chování doladit na míru? Vytvořte si vlastní verzi poděděním třídy Printer. Lze
překonfigurovat tyto proměnné:
class MyPrinter extends Nette\PhpGenerator\Printer
{
// délka řádku, po které dojde k zalamování řádku
public int $wrapLength = 120;
// znak odsazení, může být nahrazen sekvencí mezer
public string $indentation = "\t";
// počet prázdných řádků mezi properties
public int $linesBetweenProperties = 0;
// počet prázdných řádků mezi metodami
public int $linesBetweenMethods = 2;
// počet prázdných řádků mezi skupinami 'use statements' pro třídy, funkce a konstanty
public int $linesBetweenUseTypes = 0;
// pozice otevírací složené závorky pro funkce a metody
public bool $bracesOnNextLine = true;
// umístěte jeden parametr na jeden řádek, i když má atribut nebo je podporován
public bool $singleParameterOnOneLine = false;
// omits namespaces that do not contain any class or function
public bool $omitEmptyNamespaces = true;
// oddělovač mezi pravou závorkou a návratovým typem funkcí a metod
public string $returnTypeColon = ': ';
}
Jak a proč se vlastně liší standardní Printer a PsrPrinter? Proč není v balíčku jen jeden
printer, a to PsrPrinter?
Standardní Printer formátuje kód tak, jak to děláme v celém Nette. Tím, že Nette vzniklo mnohem dřív,
než PSR, a také proto, že PSR dlouhé roky nedodávalo standardy včas, ale třeba až s několikaletým zpožděním od
uvedení nové featury v PHP, došlo k tomu, že kódovací
standard se v několika drobnostech liší. Větším rozdílem je jen používání tabulátorů místo mezer. Víme, že
používáním tabulátorů v našich projektech umožňujeme přizpůsobení šířky, které je pro lidi se zrakovým postižením
nezbytné. Příkladem drobné odlišnosti je umístění složené závorky na samostatném řádku u funkcí a metod a to
vždy. Doporučení PSR se nám jeví jako nelogické a vede k snížení přehlednosti kódu.
Typy
Každý typ nebo union/intersection typ lze předat jako řetězec, můžete také použít předdefinované konstanty pro nativní typy:
use Nette\PhpGenerator\Type;
$member->setType('array'); // nebo 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'); // nebo Type::intersection(Foo::class, Bar::class)
$member->setType(null); // odstraní typ
Totéž platí pro metodu setReturnType().
Literály
Pomocí Literal můžete předávat libovolný kód PHP, například pro výchozí hodnoty vlastností nebo
parametrů atd:
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;
Výsledek:
class Demo
{
public $foo = Iterator::SELF_FIRST;
public function bar($id = 1 + 2)
{
}
}
Můžete také předat parametry do Literal a nechat je zformátovat do platného kódu PHP pomocí zástupných znaků:
new Literal('substr(?, ?)', [$a, $b]);
// generuje například: substr('hello', 5);
Literál představující vytvoření nového objektu lze snadno vygenerovat pomocí metody new:
Literal::new(Demo::class, [$a, 'foo' => $b]);
// generuje například: new Demo(10, foo: 20)
Atributy
PHP 8 atributy můžete přidat do všech tříd, metod, vlastností, konstant, enumů, funkcí, closures a parametrů. Jako hodnoty parametrů lze používat i literály.
$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;
Výsledek:
#[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
Pomocí property hooks (reprezentované třídou PropertyHook) můžete definovat operace get a set pro vlastnosti, což je funkce zavedená v 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;
Vygeneruje:
class Demo
{
public string $firstName {
set(string $value) => strtolower($value);
get {
return ucfirst($this->firstName);
}
}
}
Property a property hooks mohou být abstraktní nebo finální:
$class->addProperty('id')
->setType('int')
->addHook('get')
->setAbstract();
$class->addProperty('role')
->setType('string')
->addHook('set', 'strtolower($value)')
->setFinal();
Asymetrická viditelnost
PHP 8.4 zavádí asymetrickou viditelnost pro vlastnosti. Můžete nastavit různé úrovně přístupu pro čtení a zápis.
Viditelnost lze nastavit buď pomocí metody setVisibility() se dvěma parametry, nebo pomocí
setPublic(), setProtected() nebo setPrivate() s parametrem mode, který
určuje, zda se viditelnost vztahuje ke čtení nebo zápisu vlastnosti. Výchozí režim je 'get'.
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addProperty('name')
->setType('string')
->setVisibility('public', 'private'); // public pro čtení, private pro zápis
$class->addProperty('id')
->setType('int')
->setProtected('set'); // protected pro zápis
echo $class;
Vygeneruje:
class Demo
{
public private(set) string $name;
protected(set) int $id;
}
Jmenný prostor
Třídy, vlastnosti, rozhraní a výčty (dále jen třídy) lze seskupit do jmenných prostorů reprezentovaných třídou PhpNamespace:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
// vytvoříme nové třídy v namespace
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');
// nebo vložíme existující třídu do namespace
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);
Pokud třída již existuje, vyhodí se výjimka.
Můžete definovat 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');
Chcete-li zjednodušit plně kvalifikovaný název třídy, funkce nebo konstanty podle definovaných aliasů, použijte metodu
simplifyName:
echo $namespace->simplifyName('Foo\Bar'); // 'Bar', protože 'Foo' je aktuální jmenný prostor
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', kvůli definovanému use-statement
Zjednodušený název třídy, funkce nebo konstanty můžete naopak převést na plně kvalifikovaný název pomocí metody
resolveName:
echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'
Překlady názvů tříd
Když je třída součástí jmenného prostoru, je vykreslena mírně odlišně: všechny typy (například typehinty, návratové typy, název rodičovské třídy, implementovaná rozhraní, použité vlastnosti a atributy) jsou automaticky překládány (pokud to nevypnete, viz níže). To znamená, že musíte v definicích používat úplné názvy tříd a ty budou nahrazeny za aliasy (podle klauzulí use) nebo za plně kvalifikovaná jména ve výsledném kódu:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');
$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // bude zjednodušen na A
->addTrait('Bar\AliasedClass'); // bude zjednodušen na AliasedClass
$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // v komentářích zjednodušíme manuálně
$method->addParameter('arg')
->setType('Bar\OtherClass'); // bude přeložen na \Bar\OtherClass
echo $namespace;
// nebo použijte PsrPrinter pro výstup v souladu s PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);
Výsledek:
namespace Foo;
use Bar\AliasedClass;
class Demo implements A
{
use AliasedClass;
/**
* @return D
*/
public function method(\Bar\OtherClass $arg)
{
}
}
Automatické překládání lze vypnout tímto způsobem:
$printer = new Nette\PhpGenerator\Printer; // nebo PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);
PHP soubory
Třídy, funkce a jmenné prostory lze seskupit do PHP souborů reprezentovaných třídou PhpFile:
$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes(); // přidá declare(strict_types=1)
$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');
// nebo
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');
echo $file;
// nebo použijte PsrPrinter pro výstup v souladu s PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);
Výsledek:
<?php
/**
* This file is auto-generated.
*/
declare(strict_types=1);
namespace Foo;
class A
{
}
function foo()
{
}
Upozornění: Do souborů není možné přidávat žádný další kód mimo funkce a třídy.
Generování podle existujících
Kromě toho, že třídy a funkce můžete modelovat pomocí výše popsaného API, je můžete také nechat vygenerovat automaticky podle existujících vzorů:
// vytvoří třídu stejnou jako třída PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);
// vytvoří funkci totožnou s funkcí trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');
// vytvoří closure podle uvedené
$closure = Nette\PhpGenerator\Closure::from(
function (stdClass $a, $b = null) {},
);
Těla funkcí a metod jsou ve výchozím stavu prázdná. Pokud je chcete také načíst, použijte tento způsob (vyžaduje
instalaci balíčku nikic/php-parser):
$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);
$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);
Načítání z PHP souborů
Funkce, třídy, rozhraní a enumy můžete načítat také přímo z řetězce obsahujícího PHP kód. Například takto
vytvoříme objekt ClassType:
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
<?php
class Demo
{
public $foo;
}
XX);
Při načítání tříd z kódu PHP jsou jednořádkové komentáře mimo těla metod ignorovány (např. u properties atd.), protože tato knihovna nemá API pro práci s nimi.
Můžete také načíst přímo celý soubor PHP, který může obsahovat libovolný počet tříd, funkcí nebo dokonce jmenných prostorů:
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
Načte se také úvodní komentář k souboru a deklarace strict_types. Naopak veškerý ostatní globální kód
je ignorován.
Vyžaduje se, aby byl nainstalován nikic/php-parser.
Pokud potřebujete manipulovat s globálním kódem v souborech nebo s jednotlivými příkazy v tělech
metod, je lepší použít přímo knihovnu nikic/php-parser.
Class Manipulator
Třída ClassManipulator poskytuje nástroje pro manipulaci s třídami.
$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);
Metoda inheritMethod() zkopíruje metodu z rodičovské třídy nebo implementovaného rozhraní do vaší
třídy. To vám umožní přepsat metodu nebo rozšířit její signaturu:
$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
Metoda inheritProperty() zkopíruje vlastnost z rodičovské třídy do vaší třídy. Je to užitečné, když
chcete ve své třídě mít stejnou vlastnost, ale třeba s jinou výchozí hodnotou:
$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');
Metoda implement() automaticky implementuje všechny metody a vlastnosti z daného rozhraní nebo abstraktní
třídy ve vaší třídě:
$manipulator->implement(SomeInterface::class);
// Nyní vaše třída implementuje SomeInterface a obsahuje všechny jeho metody
Výpis proměnných
Třída Dumper převede proměnnou do parsovatelného PHP kódu. Poskytuje lepší a přehlednější výstup než
standardní funkce var_export().
$dumper = new Nette\PhpGenerator\Dumper;
$var = ['a', 'b', 123];
echo $dumper->dump($var); // vypíše ['a', 'b', 123]
Tabulka kompatibility
PhpGenerator 4.1 je kompatibilní s PHP 8.0 až 8.4.