Nette PhpGenerator
- Supporta tutte le funzionalità più recenti di PHP (property hook, enum, attributi ecc.)
- Vi permette di modificare facilmente le classi esistenti
- Output conforme allo stile di codifica PSR-12 / PER
- Libreria matura, stabile e ampiamente usata
Installazione
La libreria si scarica e si installa con lo strumento Composer:
composer require nette/php-generator
Per la compatibilità con PHP vedi la tabella di compatibilità.
Classi
Cominciamo con un esempio di creazione di una classe con 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');
// generiamo il codice semplicemente convertendo in stringa oppure con echo:
echo $class;
Questo restituisce il risultato seguente:
/**
* Class description.
* Second line
*
* @property-read Nette\Forms\Form $form
*/
final class Demo extends ParentClass implements Countable
{
}
Per generare il codice potete usare anche un printer che, a differenza di echo $class, si può configurare ulteriormente:
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);
Potete aggiungere costanti (classe Constant) e proprietà (classe Property):
$class->addConstant('ID', 123)
->setProtected() // visibilità della costante
->setType('int')
->setFinal();
$class->addProperty('items', [1, 2, 3])
->setPrivate() // oppure setVisibility('private')
->setStatic()
->addComment('@var int[]');
$class->addProperty('list')
->setType('?array')
->setInitialized(); // stampa '= null'
Questo genera:
final protected const int ID = 123;
/** @var int[] */
private static $items = [1, 2, 3];
public ?array $list = null;
E potete aggiungere metodi:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int') // tipi di ritorno dei metodi
->setBody('return count($items ?: $this->items);');
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
Il risultato è:
/**
* Count it.
*/
final protected function count(array &$items = []): ?int
{
return count($items ?: $this->items);
}
I parametri promossi introdotti in PHP 8.0 si possono passare al costruttore:
$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
->setPrivate();
Il risultato è:
public function __construct(
public $name,
private $args = [],
) {
}
Le proprietà e le classi readonly si contrassegnano con la funzione setReadOnly().
Se una proprietà, una costante, un metodo o un trait aggiunto esiste già, viene lanciata un'eccezione. I parametri, al contrario, vengono sovrascritti.
I membri della classe si possono rimuovere con removeProperty(), removeConstant(),
removeMethod() oppure removeParameter().
Alla classe potete aggiungere anche oggetti Method, Property o Constant esistenti:
$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);
Potete anche clonare metodi, proprietà e costanti esistenti con un nome diverso usando cloneWithName():
$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);
Interfacce o trait
Potete creare interfacce e trait (classi InterfaceType e TraitType):
$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');
Uso di un trait:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
->addResolution('sayHello as protected')
->addComment('@use MyTrait<Foo>');
echo $class;
Il risultato è:
class Demo
{
use SmartObject;
/** @use MyTrait<Foo> */
use MyTrait {
sayHello as protected;
}
}
Enum
Gli enum introdotti in PHP 8.1 si creano facilmente così (classe EnumType):
$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');
echo $enum;
Il risultato è:
enum Suit
{
case Clubs;
case Diamonds;
case Hearts;
case Spades;
}
Potete anche definire gli equivalenti scalari e creare un backed enum:
$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');
A ogni case potete aggiungere un commento o degli attributi con addComment() oppure
addAttribute().
Classi anonime
Passate null come nome e avete una classe anonima:
$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
->addParameter('foo');
echo '$obj = new class ($val) ' . $class . ';';
Il risultato è:
$obj = new class ($val) {
public function __construct($foo)
{
}
};
Funzioni globali
Il codice delle funzioni globali lo genera la classe GlobalFunction:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;
// oppure usate PsrPrinter per un output conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);
Il risultato è:
function foo($a, $b)
{
return $a + $b;
}
Funzioni anonime
Il codice delle funzioni anonime (closure) lo genera la classe Closure:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
->setReference();
echo $closure;
// oppure usate PsrPrinter per un output conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);
Il risultato è:
function ($a, $b) use (&$c) {
return $a + $b;
}
Arrow function brevi
Con il printer potete stampare anche una arrow function breve:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');
echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);
Il risultato è:
fn($a, $b) => $a + $b;
Firme di metodi e funzioni
I metodi sono rappresentati dalla classe Method. Potete impostare la visibilità, il tipo di ritorno, aggiungere commenti, attributi ecc.:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int');
I singoli parametri sono rappresentati dalla classe Parameter. Anche qui potete impostare tutte le proprietà immaginabili:
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
// function count(array &$items = [])
Per definire i parametri variadici (noti anche come operatore splat) usate setVariadic():
$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');
Questo genera:
function count(...$items)
{
}
Corpi di metodi e funzioni
Il corpo si può passare tutto insieme al metodo setBody() oppure via via (riga per riga) chiamando ripetutamente
addBody():
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;
Il risultato è:
function foo()
{
$a = rand(10, 20);
return $a;
}
Potete usare segnaposto speciali per inserire facilmente le variabili.
Segnaposto semplici ?:
$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;
Il risultato è:
function foo()
{
return substr('any string', 3);
}
Segnaposto per i variadici ...?:
$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;
Il risultato è:
function foo()
{
myfunc(1, 2, 3);
}
Con ...?: potete usare anche i parametri con nome di PHP 8:
$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);
// myfunc(foo: 1, bar: true);
Il segnaposto si esegue l'escape con una barra rovesciata \?:
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;
Il risultato è:
function foo($a)
{
return $a ? 10 : 3;
}
Printer e conformità PSR
Per generare il codice PHP serve la classe Printer:
$class = new Nette\PhpGenerator\ClassType('Demo');
// ...
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // uguale a: echo $class
Sa generare il codice di tutti gli altri elementi e offre metodi come printFunction(),
printNamespace() ecc.
Esiste anche la classe PsrPrinter, il cui output è conforme allo stile di codifica PSR-2 / PSR-12 / PER:
$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);
Dovete personalizzare il comportamento? Createvi una vostra versione ereditando dalla classe Printer. Potete
riconfigurare queste variabili:
class MyPrinter extends Nette\PhpGenerator\Printer
{
// lunghezza della riga dopo la quale si va a capo
public int $wrapLength = 120;
// carattere di indentazione, si può sostituire con una sequenza di spazi
public string $indentation = "\t";
// numero di righe vuote tra le proprietà
public int $linesBetweenProperties = 0;
// numero di righe vuote tra i metodi
public int $linesBetweenMethods = 2;
// numero di righe vuote tra i gruppi di 'use statement' per classi, funzioni e costanti
public int $linesBetweenUseTypes = 0;
// posizione della parentesi graffa di apertura per funzioni e metodi
public bool $bracesOnNextLine = true;
// mette un singolo parametro su una riga, anche se ha un attributo o è promosso
public bool $singleParameterOnOneLine = false;
// omette i namespace che non contengono alcuna classe o funzione
public bool $omitEmptyNamespaces = true;
// mette declare(strict_types) sulla stessa riga di <?php
public bool $declareOnOpenTag = false;
// separatore tra la parentesi chiusa e il tipo di ritorno di funzioni e metodi
public string $returnTypeColon = ': ';
}
In che cosa differiscono davvero, e perché, il Printer standard e il PsrPrinter? Perché nel
pacchetto non c'è un solo printer, il PsrPrinter?
Il Printer standard formatta il codice come facciamo in tutto Nette. Poiché Nette è nato molto prima di PSR, e
anche perché gli standard PSR arrivavano spesso in ritardo (a volte anni dopo l'introduzione di una nuova funzionalità di PHP),
lo standard di codifica di Nette differisce in alcuni piccoli
dettagli. La differenza principale è l'uso delle tabulazioni invece degli spazi. Sappiamo che usare le tabulazioni nei nostri
progetti permette di personalizzare la larghezza, cosa essenziale per le persone con disabilità visive.
Un esempio di piccola differenza è mettere la parentesi graffa di apertura di funzioni e metodi sempre su una riga separata. La
raccomandazione PSR ci sembra illogica e porta a una minore leggibilità del codice.
Tipi
Ogni tipo, o tipo unione/intersezione, si può passare come stringa; per i tipi nativi potete usare anche le costanti predefinite:
use Nette\PhpGenerator\Type;
$member->setType('array'); // oppure Type::Array
$member->setType('?array'); // oppure Type::nullable(Type::Array)
$member->setType('array|string'); // oppure Type::union(Type::Array, Type::String)
$member->setType('Foo&Bar'); // oppure Type::intersection(Foo::class, Bar::class)
$member->setType(null); // rimuove il tipo
Lo stesso vale per il metodo setReturnType().
Letterali
Con Literal potete passare qualsiasi codice PHP, per esempio per i valori predefiniti di proprietà
o parametri:
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;
Risultato:
class Demo
{
public $foo = Iterator::SELF_FIRST;
public function bar($id = 1 + 2)
{
}
}
A Literal potete anche passare dei parametri e farli formattare in codice PHP valido usando i segnaposto:
new Literal('substr(?, ?)', [$a, $b]);
// genera per esempio: substr('hello', 5)
Un letterale che rappresenta la creazione di un nuovo oggetto si genera facilmente con il metodo new:
Literal::new(Demo::class, [$a, 'foo' => $b]);
// genera per esempio: new Demo(10, foo: 20)
Attributi
Gli attributi di PHP 8 si possono aggiungere a tutte le classi, i metodi, le proprietà, le costanti, gli enum, le funzioni, le closure e i parametri. Come valori dei parametri si possono usare anche i letterali.
$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;
Risultato:
#[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 hook
Con i property hook (rappresentati dalla classe PropertyHook) potete definire le operazioni get e set delle proprietà, una funzionalità introdotta in 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;
Questo genera:
class Demo
{
public string $firstName {
set(string $value) => strtolower($value);
get {
return ucfirst($this->firstName);
}
}
}
Le proprietà e i property hook possono essere abstract o final:
$class->addProperty('id')
->setType('int')
->addHook('get')
->setAbstract();
$class->addProperty('role')
->setType('string')
->addHook('set', 'strtolower($value)')
->setFinal();
Visibilità asimmetrica
PHP 8.4 introduce la visibilità asimmetrica delle proprietà. Potete impostare livelli di accesso diversi per la lettura e per la scrittura.
La visibilità si può impostare con il metodo setVisibility() con due parametri, oppure con
setPublic(), setProtected() o setPrivate() indicando nel parametro mode se la
visibilità riguarda la lettura o la scrittura della proprietà. La modalità predefinita è 'get'.
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addProperty('name')
->setType('string')
->setVisibility('public', 'private'); // public in lettura, private in scrittura
$class->addProperty('id')
->setType('int')
->setProtected('set'); // protected in scrittura
echo $class;
Questo genera:
class Demo
{
public private(set) string $name;
protected(set) int $id;
}
Namespace
Classi, trait, interfacce ed enum (d'ora in poi solo classi) si possono raggruppare in namespace rappresentati dalla classe PhpNamespace:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
// creiamo nuove classi nel namespace
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');
// oppure inseriamo nel namespace una classe o una funzione esistente
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);
Se nel namespace esiste già una classe con lo stesso nome, viene lanciata un'eccezione.
Potete definire le clausole 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');
Per semplificare il nome completo di una classe, di una funzione o di una costante in base agli alias definiti o al namespace
corrente, usate il metodo simplifyName:
echo $namespace->simplifyName('Foo\Bar'); // 'Bar', perché 'Foo' è il namespace corrente
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', grazie alla clausola use definita
Al contrario, potete convertire il nome semplificato di una classe, di una funzione o di una costante nel nome completo con il
metodo resolveName:
echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'
Risoluzione dei nomi delle classi
Quando una classe fa parte di un namespace, viene renderizzata in modo leggermente diverso: tutti i tipi (per esempio le dichiarazioni di tipo, i tipi di ritorno, il nome della classe genitore, le interfacce implementate, i trait usati e gli attributi) vengono automaticamente risolti (a meno che non lo disattiviate, vedi sotto). Questo significa che nelle definizioni dovete usare i nomi completi delle classi e nel codice risultante verranno sostituiti con gli alias (in base alle clausole use) oppure con i nomi semplificati (se sono nello stesso namespace):
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');
$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // verrà semplificato in A
->addTrait('Bar\AliasedClass'); // verrà semplificato in AliasedClass
$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // nei commenti semplifichiamo a mano
$method->addParameter('arg')
->setType('Bar\OtherClass'); // verrà tradotto in \Bar\OtherClass
echo $namespace;
// oppure usate PsrPrinter per un output conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);
Risultato:
namespace Foo;
use Bar\AliasedClass;
class Demo implements A
{
use AliasedClass;
/**
* @return D
*/
public function method(\Bar\OtherClass $arg)
{
}
}
La risoluzione automatica si può disattivare così:
$printer = new Nette\PhpGenerator\Printer; // oppure PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);
File PHP
Classi, funzioni e namespace si possono raggruppare in file PHP rappresentati dalla classe PhpFile:
$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes(); // aggiunge declare(strict_types=1)
$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');
// oppure
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');
echo $file;
// oppure usate PsrPrinter per un output conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);
Risultato:
<?php
/**
* This file is auto-generated.
*/
declare(strict_types=1);
namespace Foo;
class A
{
}
function foo()
{
}
Nel file potete inserire anche oggetti classe, funzione e namespace esistenti con il metodo add():
$file = new Nette\PhpGenerator\PhpFile;
$class = new Nette\PhpGenerator\ClassType('Demo');
$file->add($class);
Attenzione: nei file non si può aggiungere altro codice (come echo 'hello') fuori da funzioni, classi
o namespace.
Generare da elementi esistenti
Oltre a modellare classi e funzioni con l'API descritta sopra, le potete anche far generare automaticamente da quelle esistenti usando la reflection:
// crea una classe identica alla classe PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);
// crea una funzione identica alla funzione trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');
// crea una closure in base a quella fornita
$closure = Nette\PhpGenerator\Closure::from(
function (stdClass $a, $b = null) {},
);
Per impostazione predefinita i corpi di funzioni e metodi sono vuoti. Se volete caricare anche quelli, usate questo modo
(richiede l'installazione del pacchetto nikic/php-parser):
$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);
$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);
Caricare da file PHP
Potete anche caricare funzioni, classi, interfacce ed enum direttamente da una stringa che contiene codice PHP. Per esempio per
creare un oggetto ClassType:
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
<?php
class Demo
{
public $foo;
}
XX);
Quando si caricano le classi da codice PHP, i commenti su una riga fuori dai corpi dei metodi (per esempio quelli delle proprietà) vengono ignorati, perché questa libreria non ha un'API per lavorarci.
Potete anche caricare direttamente un intero file PHP, che può contenere un numero qualsiasi di classi, funzioni o perfino namespace:
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
Vengono caricati anche il commento iniziale del file e la dichiarazione strict_types. Tutto il resto del codice
globale viene invece ignorato.
Richiede l'installazione di nikic/php-parser.
Se dovete manipolare il codice globale nei file o le singole istruzioni dentro i corpi dei metodi, è meglio
usare direttamente la libreria nikic/php-parser.
Class Manipulator
La classe ClassManipulator offre strumenti per manipolare le classi.
$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);
Il metodo inheritMethod() copia nella vostra classe un metodo da una classe genitore o da un'interfaccia
implementata. Questo vi permette di ridefinire il metodo o di estenderne la firma:
$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
Il metodo inheritProperty() copia nella vostra classe una proprietà da una classe genitore. Torna utile quando
volete avere nella vostra classe la stessa proprietà, ma magari con un valore predefinito diverso:
$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');
Il metodo implement() implementa automaticamente nella vostra classe tutti i metodi e le proprietà astratte
dell'interfaccia o della classe astratta indicata:
$manipulator->implement(SomeInterface::class);
// ora la vostra classe implementa SomeInterface e contiene gli stub di tutti i suoi metodi
Dump delle variabili
La classe Dumper converte una variabile
in codice PHP analizzabile. Offre un output migliore e più chiaro della normale funzione var_export().
$dumper = new Nette\PhpGenerator\Dumper;
$var = ['a', 'b', 123];
echo $dumper->dump($var); // stampa ['a', 'b', 123]
Tabella di compatibilità
PhpGenerator 4.2 è compatibile con PHP dalla 8.1 alla 8.5.
Se state aggiornando a una versione più recente, guardate la pagina aggiornamento.