Nette PhpGenerator

Cercate uno strumento per generare codice PHP di classi, funzioni o interi file?
  • 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.

versione: 4.x