Scaffolder
Class scaffolder. Write definition, generate simple value holders. Useful for trivial composite types used in event sourced applications - for commands, events and query definitions classes. This mostly supplements public readonly $properties.
Install / Use
/learn @grifart/ScaffolderREADME
grifart/scaffolder – The (class) scaffolder project.
It was designed to generated classes with none to simple logic. Typical usage is:
- data transfer objects (DTOs),
- events in event-sourced model,
- simple value objects (simple logic can be embedded using
#[Preserve]attribute – see below).
It is developed at gitlab.grifart.cz, automatically mirrored to GitHub and distributed over Composer packagist:grifart/scaffolder.
You can also watch introduction (in Czech) on 🎥 YouTube.
Installation
We recommend to use Composer:
composer require grifart/scaffolder
Quick start
- Create a definition file. Definition file must return a list of
ClassDefinitions. By default, its name must end with.definition.php. We commonly use just.definition.php:
<?php
use Grifart\ClassScaffolder\Capabilities;
use function Grifart\ClassScaffolder\Definition\definitionOf;
use Grifart\ClassScaffolder\Definition\Types;
return [
definitionOf(Article::class, withFields: [
'id' => 'int',
'title' => 'string',
'content' => 'string',
'tags' => Types\listOf('string'),
])
->withField('archivedAt', Types\nullable(\DateTime::class))
->with(
Capabilities\constructorWithPromotedProperties(),
Capabilities\getters(),
)
];
- Run scaffolder. You can provide the path to the definition file (or a directory which is then recursively searched for definition files) as an argument. It defaults to the current working directory if omitted.
The recommended way is to run the pre-packaged Composer binary:
composer exec scaffolder scaffold .definition.php
<details>
<summary>Alternative way: Register scaffolder as a Symfony command into you app.</summary>
Alternatively, you can register the Grifart\ClassScaffolder\Console\ScaffoldCommand into your application's DI container and run scaffolder through symfony/console. This makes it easier to access your project's services and environment in definition files. This is considered advanced usage and is not necessary in most cases.
php bin/console grifart:scaffolder:scaffold .definition.php
</details>
- Your class is ready. Scaffolder generates classes from definitions, one class per file, residing in the same directory as the definition file. By default, scaffolder makes the file read-only to prevent you from changing it accidentally.
<?php
/**
* Do not edit. This is generated file. Modify definition file instead.
*/
declare(strict_types=1);
final class Article
{
/**
* @param string[] $tags
*/
public function __construct(
private int $id,
private string $title,
private string $content,
private array $tags,
private ?\DateTime $archivedAt,
) {
}
public function getId(): int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
return $this->content;
}
/**
* @return string[]
*/
public function getTags(): array
{
return $this->tags;
}
public function getArchivedAt(): ?\DateTime
{
return $this->archivedAt;
}
}
-
Use static analysis tool such as PHPStan or Psalm to make sure that everything still works fine if you've changed any definition file.
-
Make sure that you haven't accidentally changed any generated file by adding
composer exec scaffolder check .definition.phpto your CI workflow. The command fails if any generated class differs from its definition, and thus running the scaffolder would result in losing your changes.
Definition files
A definition file must return a list of ClassDefinitions. The easiest way to create a definition is to use the definitionOf() function:
<?php
use Grifart\ClassScaffolder\Capabilities;
use Grifart\ClassScaffolder\Definition\definitionOf;
use Grifart\ClassScaffolder\Definition\Types;
return [
definitionOf(Article::class, withFields: [
'id' => 'int',
'title' => 'string',
'content' => 'string',
'tags' => Types\listOf('string'),
])
->withField('archivedAt', Types\nullable(\DateTime::class))
->with(
Capabilities\constructorWithPromotedProperties(),
Capabilities\getters(),
)
];
The definitionOf() accepts the name of the generated class and optionally a map of its fields and their types, and returns a ClassDefinition. You can further add fields and capabilities to the definition.
Fields and types
Since scaffolder is primarily designed to generate various data transfer objects, fields are first-class citizens. Every field must have a type: scaffolder has an abstraction over PHP types and provides functions to compose even the most complex of types. It adds phpdoc type annotations where necessary so that static analysis tools can perfectly understand your code.
The available types are:
-
simple types such as
'int','string','array', etc.$definition->withField('field', 'string')results in
private string $field; -
class references via
::classare resolved to the referenced class, interface or enum:$definition->withField('field', \Iterator::class)results in
private Iterator $field; -
references to other definitions are supported and resolved:
$otherDefinition = definitionOf(OtherClass::class); $definition->withField('field', $otherDefinition);results in
private OtherClass $field; -
nullability can be expressed via
nullable():$definition->withField('field', Types\nullable('string'))results in
private ?string $field; -
lists can be created via
listOf():$definition->withField('field', Types\listOf('string'))results in
/** @var string[] */ private array $field; -
key-value collections can be created via
collection():$definition->withField('field', Types\collection(Collection::class, UserId::class, User::class))results in
/** @var Collection<UserId, User> */ private Collection $field; -
any generic types can be represented via
generic():$definition->withField('field', Types\generic(\SerializedValue::class, User::class))results in
/** @var SerializedValue<User> */ private SerializedValue $field; -
complex array shapes can be described via
arrayShape():$definition->withField('field', Types\arrayShape(['key' => 'string', 'optional?' => 'int']))results in
/** @var array{key: string, optional?: int} */ private array $field; -
similarly, tuples can be created via
tuple():$definition->withField('field', Types\tuple('string', Types\nullable('int')))results in
/** @var array{string, int|null} */ private array $field; -
unions and intersections are supported as well:
$definition->withField('field', Types\union('int', 'string')) ->withField('other', Types\intersection(\Traversable::class, \Countable::class))results in
private int|string $field; private Traversable&Countable $other;
Capabilities
Fields on their own are not represented in the generated code, they just describe which fields the resulting class should contain. To add any behaviour to the class, you need to add capabilities to it. Scaffolder comes prepared with a bundle of capabilities for the most common use-cases:
-
properties()generates a private property for each field:definitionOf(Foo::class) ->withField('field', 'string') ->with(Capabilities\properties())results in:
final class Foo { private string $field; } -
initializingConstructor()generates a public constructor with property assignments. This works best when combined with theproperties()capability:definitionOf(Foo::class) ->withField('field', 'string') ->with(Capabilities\properties()) ->with(Capabilities\initializingConstructor())results in:
final class Foo { private string $field; public function __construct(string $field) { $this->field = $field; } } -
constructorWithPromotedProperties()generates a public constructor with promoted properties. This can be used instead of the preceding two capabilities in PHP 8+ code:definitionOf(Foo::class) ->withField('field', 'string') ->with(Capabilities\constructorWithPromotedProperties())results in:
final class Foo { public function __construct(private string $field) { } } -
readonlyProperties()makes properties or promoted parameters public and readonly:definitionOf(Foo::class) ->withField('field', 'string') ->with(Capabilities\constructorWithPromotedProperties()) ->with(Capabilities\readonlyProperties())results in:
final class Foo { public function __construct(public readonly string $field) { } } -
privatizedConstructor()makes the class constructor private:definitionOf(Foo::class) ->withField('field', 'st
