SkillAgentSearch skills...

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/Scaffolder
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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

  1. 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(),
        )
];
  1. 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>
  1. 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;
    }
}
  1. Use static analysis tool such as PHPStan or Psalm to make sure that everything still works fine if you've changed any definition file.

  2. Make sure that you haven't accidentally changed any generated file by adding composer exec scaffolder check .definition.php to 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 ::class are 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 the properties() 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
    
View on GitHub
GitHub Stars10
CategoryDevelopment
Updated3mo ago
Forks0

Languages

PHP

Security Score

72/100

Audited on Dec 16, 2025

No findings