Generador de código PHP
- Admite todas las características más recientes de PHP (como property hooks, enums, atributos, etc.)
- Le permite modificar fácilmente clases existentes
- El código de salida cumple con el estilo de codificación PSR-12 / PER
- Biblioteca madura, estable y ampliamente utilizada
Instalación
Descargue e instale la biblioteca usando Composer:
composer require nette/php-generator
Puede encontrar la compatibilidad con PHP en la tabla.
Clases
Comencemos directamente con un ejemplo de creación de una clase usando ClassType:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class
->setFinal()
->setExtends(ParentClass::class)
->addImplement(Countable::class)
->addComment("Descripción de la clase.\nSegunda línea\n")
->addComment('@property-read Nette\Forms\Form $form');
// simplemente genere el código convirtiéndolo a cadena o usando echo:
echo $class;
Devuelve el siguiente resultado:
/**
* Descripción de la clase
* Segunda línea
*
* @property-read Nette\Forms\Form $form
*/
final class Demo extends ParentClass implements Countable
{
}
También podemos usar el llamado printer para generar el código, que a diferencia de echo $class
, podremos configurar más:
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);
Podemos agregar constantes (clase Constant) y propiedades (clase Property):
$class->addConstant('ID', 123)
->setProtected() // visibilidad de constantes
->setType('int')
->setFinal();
$class->addProperty('items', [1, 2, 3])
->setPrivate() // o setVisibility('private')
->setStatic()
->addComment('@var int[]');
$class->addProperty('list')
->setType('?array')
->setInitialized(); // imprime '= null'
Genera:
final protected const int ID = 123;
/** @var int[] */
private static $items = [1, 2, 3];
public ?array $list = null;
Y podemos agregar métodos:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int') // tipos de retorno en métodos
->setBody('return count($items ?: $this->items);');
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
El resultado es:
/**
* Count it.
*/
final protected function count(array &$items = []): ?int
{
return count($items ?: $this->items);
}
Los parámetros promocionados introducidos en PHP 8.0 se pueden pasar al constructor:
$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
->setPrivate();
El resultado es:
public function __construct(
public $name,
private $args = [],
) {
}
Las propiedades y clases de solo lectura se pueden marcar usando la función setReadOnly()
.
Si la propiedad, constante, método o parámetro agregado ya existe, se lanza una excepción.
Los miembros de la clase se pueden eliminar usando removeProperty()
, removeConstant()
,
removeMethod()
o removeParameter()
.
También puede agregar objetos Method
, Property
o Constant
existentes a la clase:
$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);
También puede clonar métodos, propiedades y constantes existentes con un nombre diferente usando
cloneWithName()
:
$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);
Interfaz o trait
Puede crear interfaces y traits (clases InterfaceType y TraitType):
$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');
Uso de traits:
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
->addResolution('sayHello as protected')
->addComment('@use MyTrait<Foo>');
echo $class;
Resultado:
class Demo
{
use SmartObject;
/** @use MyTrait<Foo> */
use MyTrait {
sayHello as protected;
}
}
Enums
Los enums, introducidos en PHP 8.1, se pueden crear fácilmente así: (clase EnumType):
$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');
echo $enum;
Resultado:
enum Suit
{
case Clubs;
case Diamonds;
case Hearts;
case Spades;
}
También puede definir equivalentes escalares y crear así un enum “backed”:
$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');
A cada case se le puede agregar un comentario o atributos usando
addComment()
o addAttribute()
.
Clases anónimas
Pasamos null
como nombre y tenemos una clase anónima:
$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
->addParameter('foo');
echo '$obj = new class ($val) ' . $class . ';';
Resultado:
$obj = new class ($val) {
public function __construct($foo)
{
}
};
Funciones globales
El código de las funciones lo genera la clase GlobalFunction:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;
// o use PsrPrinter para salida conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);
Resultado:
function foo($a, $b)
{
return $a + $b;
}
Funciones anónimas
El código de las funciones anónimas lo genera la clase Closure:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
->setReference();
echo $closure;
// o use PsrPrinter para salida conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);
Resultado:
function ($a, $b) use (&$c) {
return $a + $b;
}
Funciones flecha abreviadas
También puede imprimir una función anónima abreviada usando el printer:
$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');
echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);
Resultado:
fn($a, $b) => $a + $b
Firmas de métodos y funciones
Los métodos los representa la clase Method. Puede establecer la visibilidad, el tipo de retorno, agregar comentarios, atributos, etc:
$method = $class->addMethod('count')
->addComment('Count it.')
->setFinal()
->setProtected()
->setReturnType('?int');
Los parámetros individuales los representa la clase Parameter. Nuevamente, puede establecer todas las propiedades imaginables:
$method->addParameter('items', []) // $items = []
->setReference() // &$items = []
->setType('array'); // array &$items = []
// function count(array &$items = [])
Para definir los llamados parámetros variádicos (o también operador splat) sirve setVariadic()
:
$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');
Genera:
function count(...$items)
{
}
Cuerpos de métodos y funciones
El cuerpo se puede pasar de una vez al método setBody()
o gradualmente (línea por línea) llamando
repetidamente a addBody()
:
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;
Resultado
function foo()
{
$a = rand(10, 20);
return $a;
}
Puede usar placeholders especiales para insertar variables fácilmente.
Placeholders simples ?
$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;
Resultado
function foo()
{
return substr('any string', 3);
}
Placeholder para variadic ...?
$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;
Resultado:
function foo()
{
myfunc(1, 2, 3);
}
También puede usar parámetros con nombre para PHP 8 usando ...?:
$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);
// myfunc(foo: 1, bar: true);
El placeholder se escapa con una barra invertida \?
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;
Resultado:
function foo($a)
{
return $a ? 10 : 3;
}
Printer y conformidad con PSR
Para generar código PHP sirve la clase Printer:
$class = new Nette\PhpGenerator\ClassType('Demo');
// ...
$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // lo mismo que: echo $class
Puede generar código de todos los demás elementos, ofrece métodos como printFunction()
,
printNamespace()
, etc.
También está disponible la clase PsrPrinter
, cuya salida cumple con el estilo de codificación PSR-2 / PSR-12
/ PER:
$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);
¿Necesita ajustar el comportamiento a medida? Cree su propia versión heredando la clase Printer
. Se pueden
reconfigurar estas variables:
class MyPrinter extends Nette\PhpGenerator\Printer
{
// longitud de línea tras la cual se produce el salto de línea
public int $wrapLength = 120;
// carácter de indentación, puede ser reemplazado por una secuencia de espacios
public string $indentation = "\t";
// número de líneas vacías entre propiedades
public int $linesBetweenProperties = 0;
// número de líneas vacías entre métodos
public int $linesBetweenMethods = 2;
// número de líneas vacías entre grupos de 'use statements' para clases, funciones y constantes
public int $linesBetweenUseTypes = 0;
// posición de la llave de apertura para funciones y métodos
public bool $bracesOnNextLine = true;
// colocar un parámetro por línea, incluso si tiene un atributo o es promocionado
public bool $singleParameterOnOneLine = false;
// omite espacios de nombres que no contienen ninguna clase o función
public bool $omitEmptyNamespaces = true;
// separador entre el paréntesis derecho y el tipo de retorno de funciones y métodos
public string $returnTypeColon = ': ';
}
¿Cómo y por qué difieren realmente el Printer
estándar y el PsrPrinter
? ¿Por qué no hay solo un
printer en el paquete, es decir, PsrPrinter
?
El Printer
estándar formatea el código como lo hacemos en todo Nette. Dado que Nette se creó mucho antes que
PSR, y también porque PSR durante muchos años no entregó estándares a tiempo, sino tal vez con varios años de retraso desde
la introducción de una nueva característica en PHP, sucedió que el estándar de codificación difiere en algunos pequeños detalles.
La mayor diferencia es solo el uso de tabuladores en lugar de espacios. Sabemos que al usar tabuladores en nuestros proyectos,
permitimos la personalización del ancho, lo cual es necesario para personas con
discapacidad visual. Un ejemplo de una pequeña diferencia es la colocación de la llave de apertura en una línea separada
para funciones y métodos, y siempre. La recomendación de PSR nos parece ilógica y conduce a una reducción de la claridad del código.
Tipos
Cada tipo o tipo unión/intersección se puede pasar como una cadena, también puede usar constantes predefinidas para tipos nativos:
use Nette\PhpGenerator\Type;
$member->setType('array'); // o Type::Array;
$member->setType('?array'); // o Type::nullable(Type::Array);
$member->setType('array|string'); // o Type::union(Type::Array, Type::String)
$member->setType('Foo&Bar'); // o Type::intersection(Foo::class, Bar::class)
$member->setType(null); // elimina el tipo
Lo mismo se aplica al método setReturnType()
.
Literales
Con Literal
puede pasar cualquier código PHP, por ejemplo, para valores predeterminados de propiedades
o parámetros, etc:
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;
Resultado:
class Demo
{
public $foo = Iterator::SELF_FIRST;
public function bar($id = 1 + 2)
{
}
}
También puede pasar parámetros a Literal
y dejar que se formateen en código PHP válido usando placeholders:
new Literal('substr(?, ?)', [$a, $b]);
// genera por ejemplo: substr('hello', 5);
Un literal que representa la creación de un nuevo objeto se puede generar fácilmente usando el método new
:
Literal::new(Demo::class, [$a, 'foo' => $b]);
// genera por ejemplo: new Demo(10, foo: 20)
Atributos
Los atributos de PHP 8 se pueden agregar a todas las clases, métodos, propiedades, constantes, enums, funciones, closures y parámetros. También se pueden usar literales como valores de parámetros.
$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;
Resultado:
#[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
Con los property hooks (representados por la clase PropertyHook) puede definir operaciones get y set para propiedades, una característica introducida en 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;
Genera:
class Demo
{
public string $firstName {
set(string $value) => strtolower($value);
get {
return ucfirst($this->firstName);
}
}
}
Las propiedades y los property hooks pueden ser abstractos o finales:
$class->addProperty('id')
->setType('int')
->addHook('get')
->setAbstract();
$class->addProperty('role')
->setType('string')
->addHook('set', 'strtolower($value)')
->setFinal();
Visibilidad asimétrica
PHP 8.4 introduce la visibilidad asimétrica para propiedades. Puede establecer diferentes niveles de acceso para lectura y escritura.
La visibilidad se puede establecer ya sea usando el método setVisibility()
con dos parámetros, o usando
setPublic()
, setProtected()
o setPrivate()
con el parámetro mode
, que
especifica si la visibilidad se aplica a la lectura o escritura de la propiedad. El modo predeterminado es
'get'
.
$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addProperty('name')
->setType('string')
->setVisibility('public', 'private'); // public para lectura, private para escritura
$class->addProperty('id')
->setType('int')
->setProtected('set'); // protected para escritura
echo $class;
Genera:
class Demo
{
public private(set) string $name;
protected(set) int $id;
}
Espacio de nombres
Las clases, propiedades, interfaces y enums (en adelante, clases) se pueden agrupar en espacios de nombres representados por la clase PhpNamespace:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
// creamos nuevas clases en el namespace
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');
// o insertamos una clase existente en el namespace
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);
Si la clase ya existe, se lanza una excepción.
Puede definir cláusulas 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');
Para simplificar el nombre completamente calificado de una clase, función o constante según los alias definidos, use el
método simplifyName
:
echo $namespace->simplifyName('Foo\Bar'); // 'Bar', porque 'Foo' es el espacio de nombres actual
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range', debido al use-statement definido
Por el contrario, puede convertir el nombre simplificado de una clase, función o constante a su nombre completamente
calificado usando el método resolveName
:
echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'
Resolución de nombres de clases
Cuando una clase forma parte de un espacio de nombres, se renderiza de forma ligeramente diferente: todos los tipos (por ejemplo, typehints, tipos de retorno, nombre de la clase padre, interfaces implementadas, propiedades usadas y atributos) se resuelven automáticamente (a menos que lo desactive, ver más abajo). Esto significa que debe usar nombres de clase completos en las definiciones y se reemplazarán por alias (según las cláusulas use) o por nombres completamente calificados en el código resultante:
$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');
$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // se simplificará a A
->addTrait('Bar\AliasedClass'); // se simplificará a AliasedClass
$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // en comentarios simplificamos manualmente
$method->addParameter('arg')
->setType('Bar\OtherClass'); // se resolverá a \Bar\OtherClass
echo $namespace;
// o use PsrPrinter para salida conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);
Resultado:
namespace Foo;
use Bar\AliasedClass;
class Demo implements A
{
use AliasedClass;
/**
* @return D
*/
public function method(\Bar\OtherClass $arg)
{
}
}
La resolución automática se puede desactivar de esta manera:
$printer = new Nette\PhpGenerator\Printer; // o PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);
Archivos PHP
Las clases, funciones y espacios de nombres se pueden agrupar en archivos PHP representados por la clase PhpFile:
$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes(); // agrega declare(strict_types=1)
$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');
// o
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');
echo $file;
// o use PsrPrinter para salida conforme a PSR-2 / PSR-12 / PER
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);
Resultado:
<?php
/**
* This file is auto-generated.
*/
declare(strict_types=1);
namespace Foo;
class A
{
}
function foo()
{
}
Advertencia: No es posible agregar ningún otro código fuera de funciones y clases a los archivos.
Generación a partir de existentes
Además de poder modelar clases y funciones usando la API descrita anteriormente, también puede hacer que se generen automáticamente a partir de patrones existentes:
// crea una clase igual que la clase PDO
$class = Nette\PhpGenerator\ClassType::from(PDO::class);
// crea una función idéntica a la función trim()
$function = Nette\PhpGenerator\GlobalFunction::from('trim');
// crea un closure según el indicado
$closure = Nette\PhpGenerator\Closure::from(
function (stdClass $a, $b = null) {},
);
Los cuerpos de funciones y métodos están vacíos de forma predeterminada. Si también desea cargarlos, use este método
(requiere la instalación del paquete nikic/php-parser
):
$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);
$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);
Carga desde archivos PHP
También puede cargar funciones, clases, interfaces y enums directamente desde una cadena que contenga código PHP. Por
ejemplo, así creamos un objeto ClassType
:
$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
<?php
class Demo
{
public $foo;
}
XX);
Al cargar clases desde código PHP, los comentarios de una sola línea fuera de los cuerpos de los métodos se ignoran (por ejemplo, en propiedades, etc.), ya que esta biblioteca no tiene una API para trabajar con ellos.
También puede cargar directamente un archivo PHP completo, que puede contener cualquier número de clases, funciones o incluso espacios de nombres:
$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));
También se cargan el comentario introductorio del archivo y la declaración strict_types
. Por el contrario, todo
el demás código global se ignora.
Se requiere que esté instalado nikic/php-parser
.
Si necesita manipular código global en archivos o sentencias individuales en cuerpos de métodos, es mejor usar
directamente la biblioteca nikic/php-parser
.
Class Manipulator
La clase ClassManipulator proporciona herramientas para manipular clases.
$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);
El método inheritMethod()
copia un método de la clase padre o interfaz implementada a su clase. Esto le permite
sobrescribir el método o extender su firma:
$method = $manipulator->inheritMethod('bar');
$method->setBody('...');
El método inheritProperty()
copia una propiedad de la clase padre a su clase. Es útil cuando desea tener la
misma propiedad en su clase, pero quizás con un valor predeterminado diferente:
$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');
El método implement()
implementa automáticamente todos los métodos y propiedades de la interfaz o clase
abstracta dada en su clase:
$manipulator->implement(SomeInterface::class);
// Ahora su clase implementa SomeInterface y contiene todos sus métodos
Volcado de variables
La clase Dumper
convierte una variable en código PHP analizable. Proporciona una salida mejor y más clara que la
función estándar var_export()
.
$dumper = new Nette\PhpGenerator\Dumper;
$var = ['a', 'b', 123];
echo $dumper->dump($var); // imprime ['a', 'b', 123]
Tabla de compatibilidad
PhpGenerator 4.1 es compatible con PHP 8.0 a 8.4.