Code Generation
pop-code writes PHP. You build a class, function or enum out of generator objects, and render it to
a string or a file. The same component reads in the other direction: reflection turns existing code
back into those generator objects, so you can add a method to a class that already exists and write it
out again. This is what scaffolding tools are built from — pop-kettle generates its controllers and
models this way.
composer require popphp/pop-code
Generating a Class#
ClassGenerator is the container. Give it a namespace, properties and methods, and render it:
use Pop\Code\Generator;
$class = new Generator\ClassGenerator('Widget');
$class->setNamespace(new Generator\NamespaceGenerator('App\Model'));
$class->addProperty(new Generator\PropertyGenerator('name', 'string', null, 'protected'));
$setName = new Generator\MethodGenerator('setName', 'public');
$setName->addArgument('name', null, 'string')
->setBody('$this->name = $name;')
->addReturnType('void')
->setDesc('Set the widget name.');
$class->addMethod($setName);
echo $class;
/**
* @namespace
*/
namespace App\Model;
class Widget
{
/**
* @var string
*/
protected string|null $name = null;
/**
* Set the widget name.
*
* @param string|null $name
* @return void
*/
public function setName(string|null $name = null): void
{
$this->name = $name;
}
}
InterfaceGenerator, TraitGenerator and EnumGenerator are the same shape. An interface's methods
render as semicolon-terminated signatures with no body, and its parents come from a second constructor
argument taking an array or a comma-separated string:
use Pop\Code\Generator;
$interface = new Generator\InterfaceGenerator('Arrayable');
$interface->addMethod((new Generator\MethodGenerator('toArray'))->addReturnType('array'));
echo $interface;
interface Arrayable
{
/**
* @return array
*/
public function toArray(): array;
}
A trait takes properties and methods but has no extends, implements, abstract, final or readonly
flag. An enum takes a backing type and EnumCaseGenerator cases, and can carry constants, methods and
implemented interfaces exactly as a class does. Leave the backing type off for a pure enum:
use Pop\Code\Generator;
$enum = new Generator\EnumGenerator('Status', 'string');
$enum->addCase(new Generator\EnumCaseGenerator('Active', 'active'));
$enum->addCase(new Generator\EnumCaseGenerator('Inactive', 'inactive'));
echo $enum;
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
}
FunctionGenerator covers plain functions and, with closure: true, anonymous ones — a closure with a
name renders as $name = function(...) {...};, and one with no name renders as a bare expression.
Flags cover the rest of a class declaration. setAsAbstract() and setAsFinal() are on both
ClassGenerator and MethodGenerator and are mutually exclusive — setting one clears the other.
setAsReadonly() is on ClassGenerator and PropertyGenerator. addUse() composes a trait in, and
addConstant() takes a ConstantGenerator, optionally with setTyped(true) for a PHP 8.3 typed
constant.
addUse($trait, $as) honors the alias on NamespaceGenerator; inside a class body use SomeTrait as Alias; is not valid PHP, so the alias is dropped there.
Methods, Properties and Docblocks#
addArgument($name, $value, $type, $variadic, $byRef, $attributes) is where most of the detail lives.
The second argument is the default value, and there's a trap in it: passing PHP null means "default
to null", not "no default". It emits = null and widens the type, which is why setName(string $name)
above came out as setName(string|null $name = null).
Two marker classes cover the cases a bare value cannot express. Generator\NoValue is a required
parameter with no default at all, and Generator\Literal is a raw expression emitted verbatim rather
than quoted:
use Pop\Code\Generator;
$sum = new Generator\FunctionGenerator('sum');
$sum->addArgument('numbers', new Generator\NoValue(), 'int', true)
->setBody('return array_sum($numbers);')
->addReturnType('int');
echo $sum; // function sum(int ...$numbers): int
The fourth and fifth arguments are the variadic and by-reference flags — addArgument('counter', new Generator\NoValue(), 'int', false, true) renders int &$counter. Any type position accepts a union
(int|string) or an intersection (\Countable&\Traversable); an intersection defaulted to null is
wrapped in parentheses to satisfy PHP's DNF grammar.
NoValue gives a property a bare typed declaration with no initializer —
new PropertyGenerator('name', 'string', new NoValue(), 'protected') renders protected string $name;.
Promoted constructor properties come from addPromotedArgument($name, $visibility, $value, $type, $readonly), valid only on a method named __construct:
use Pop\Code\Generator;
$class = new Generator\ClassGenerator('Point');
$constructor = new Generator\MethodGenerator('__construct');
$constructor->addPromotedArgument('x', 'public', new Generator\NoValue(), 'int')
->addPromotedArgument('y', 'public', new Generator\NoValue(), 'int')
->setBody('');
$class->addMethod($constructor);
echo $class;
class Point
{
/**
* @param int $x
* @param int $y
*/
public function __construct(public int $x, public int $y)
{
}
}
Call setBody('') for a concrete method with an empty body. MethodGenerator renders a
semicolon-terminated signature for a body that was never set.
Docblocks are generated for you from the types and the setDesc() description, but
DocblockGenerator can be built by hand and passed to setDocblock() when you want more than
@param and @return:
use Pop\Code\Generator;
$docblock = new Generator\DocblockGenerator('Adds two numbers together.');
$docblock->addParam('int', '$a', 'The first number');
$docblock->setReturn('int', 'The sum');
$docblock->setThrows('\InvalidArgumentException', 'If either number is negative');
$docblock->addTag('since', '7.0.0');
Attributes are AttributeGenerator objects, addable to a class, member, or individual parameter.
addArgument($value, $name) takes the value first and the name second, so a positional argument is a
one-argument call and a named one passes the name second:
use Pop\Code\Generator;
$table = new Generator\AttributeGenerator('Table');
$table->addArgument('products', 'name');
$class = new Generator\ClassGenerator('Product');
$class->addAttribute($table);
echo $class; // #[Table(name: 'products')] above the class declaration
Generating Files#
Pop\Code\Generator is the file container. It takes one or more code objects, adds the <?php tag,
and either renders the whole file or writes it:
use Pop\Code\Generator;
$class = new Generator\ClassGenerator('Widget');
$class->setNamespace(new Generator\NamespaceGenerator('App\Model'));
$code = new Generator($class);
echo $code->render();
$code->writeToFile(__DIR__ . '/../app/src/Model/Widget.php');
addCodeObjects() takes an array, which is how you put several functions in one file. Keyed by
namespace, it produces a multi-namespace file with each in its own braced block:
use Pop\Code\Generator;
$code = new Generator();
$code->addCodeObjects([
'App\One' => new Generator\ClassGenerator('Foo'),
'App\Two' => new Generator\ClassGenerator('Bar'),
]);
BodyGenerator renders statements with no class or function around them, for a file that is nothing
but executable code. Its createReturnConfig() turns an array into a formatted return [...], which
is exactly the shape of a Pop config file:
use Pop\Code\Generator;
$body = new Generator\BodyGenerator();
$body->createReturnConfig([
'debug' => true,
'routes' => ['home' => '/'],
]);
echo new Generator($body);
<?php
return [
'debug' => true,
'routes' => [
'home' => '/',
],
];
NamespaceGenerator::addUse() adds the imports at the top of the file, and is the one addUse() that
honors an alias:
use Pop\Code\Generator;
$namespace = new Generator\NamespaceGenerator('App\Model');
$namespace->addUse('Pop\Db\Record');
$namespace->addUse('App\Service\Mailer', 'AppMailer');
Nothing validates the code before it is written — setBody() takes a string and emits it as given, so
a body with a typo produces a file that will not parse. When a generator is part of a build step,
run php -l over what it wrote before trusting it.
Reflecting over Existing Code#
Pop\Code\Reflection reads a construct that already exists and hands back the generator object for
it. That closes the loop: you can add to a class you did not write and render the whole thing again.
use Pop\Code\Generator;
use Pop\Code\Reflection;
$class = Reflection::createClass('App\Model\Widget');
$hasName = new Generator\MethodGenerator('hasName', 'public');
$hasName->setBody('return ($this->name !== null);')
->addReturnType('bool')
->setDesc('Whether the name is set.');
$class->addMethod($hasName);
(new Generator($class))->writeToFile(__DIR__ . '/../app/src/Model/Widget.php');
The file that comes back has the original namespace, property and method intact, with hasName()
appended. Attributes on the reflected code are detected and reproduced without any extra work.
Ten factory methods cover the constructs, one per generator type:
| Method | Returns |
|---|---|
Reflection::createClass() |
Generator\ClassGenerator |
Reflection::createInterface() |
Generator\InterfaceGenerator |
Reflection::createTrait() |
Generator\TraitGenerator |
Reflection::createEnum() |
Generator\EnumGenerator |
Reflection::createNamespace() |
Generator\NamespaceGenerator |
Reflection::createFunction() |
Generator\FunctionGenerator |
Reflection::createMethod() |
Generator\MethodGenerator |
Reflection::createProperty() |
Generator\PropertyGenerator |
Reflection::createConstant() |
Generator\ConstantGenerator |
Reflection::createDocblock() |
Generator\DocblockGenerator |
Each takes a name, an object, or — for the smaller constructs — a native Reflector or a value. Built-in
code works as well as your own: Reflection::createFunction('array_map') and
Reflection::createInterface('Countable') both return usable generators.
Reflection reads a class PHP has already loaded, so the class has to be autoloadable from the script doing the reflecting.
The generator surface is much wider than one page — closures, variadics, typed constants, enum interfaces, parameter attributes and the multi-namespace file container all have more to them. See the pop-code README.
See Also#
- Console Applications — where a generator usually runs, behind a CLI command
- Applications & Bootstrap — the namespace and directory layout the generated files land in
- Config Objects — the config files
createReturnConfig()produces - pop-code README — every generator, flag and reflection factory