grifart/scaffolder

类 scaffolder。编写定义,生成简单的值持有者。适用于在事件源应用中使用的简单复合类型 - 用于命令、事件和查询定义类。这主要补充了公共只读 $properties

0.6.7 2023-10-23 06:56 UTC

README

它被设计用来生成具有无到简单逻辑的类。典型用法是

  • 数据传输对象 (DTOs),
  • 事件源模型中的事件,
  • 简单的值对象(可以通过 #[Preserve] 属性嵌入简单逻辑 - 见下文)。

它在 gitlab.grifart.cz 开发,自动镜像到 GitHub 并通过 Composer packagist:grifart/scaffolder 分发。

您还可以在 🎥 YouTube 上观看介绍(捷克语)。

安装

我们建议使用 Composer

composer require grifart/scaffolder 

快速入门

  1. 创建定义文件。定义文件必须返回一个 ClassDefinition 的列表。默认情况下,其名称必须以 .definition.php 结尾。我们通常只使用 .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. 运行 scaffolder。您可以将定义文件的路径(或一个目录,然后递归地搜索定义文件)作为参数提供。如果省略,则默认为当前工作目录。

推荐的方法是运行预打包的 Composer 二进制文件

composer exec scaffolder scaffold .definition.php
另一种方法:将 scaffolder 注册为您的应用中的 Symfony 命令。

或者,您可以将 Grifart\ClassScaffolder\Console\ScaffoldCommand 注册到您的应用程序的 DI 容器中,并通过 symfony/console 运行 scaffolder。这使您更容易在定义文件中访问项目服务和环境。这被视为高级用法,在大多数情况下不是必需的。

php bin/console grifart:scaffolder:scaffold .definition.php
  1. 您的类已准备好。 Scaffolder 从定义生成类,每个文件一个类,位于定义文件相同的目录中。默认情况下,scaffolder 将文件设置为只读,以防止您意外更改它。
<?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. 使用静态分析工具,例如 PHPStan 或 Psalm,以确保如果您更改了任何定义文件,一切仍然正常工作。

  2. 确保您没有意外更改任何生成的文件,请将 composer exec scaffolder check .definition.php 添加到您的 CI 工作流程中。如果任何生成的类与其定义不同,则命令会失败,因此运行 scaffolder 将导致您丢失更改。

定义文件

定义文件必须返回一个 ClassDefinition 的列表。创建定义的最简单方法是通过使用 definitionOf() 函数

<?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(),
            )
    ];

definitionOf() 接受生成的类的名称,以及可选的其字段和类型的映射,并返回一个 ClassDefinition。您还可以进一步向定义中添加字段和能力。

字段和类型

由于 scaffolder 主要设计用于生成各种数据传输对象,因此字段是第一类公民。每个字段都必须有一个类型:scaffolder 对 PHP 类型有抽象,并提供函数来组合最复杂的类型。它添加必要的 phpdoc 类型注释,以便静态分析工具可以完美理解您的代码。

可用的类型有

  • 简单类型,例如 'int''string''array' 等。

    $definition->withField('field', 'string')

    结果

    private string $field;
  • 类引用通过 ::class 解析到引用的类、接口或枚举

    $definition->withField('field', \Iterator::class)

    结果

    private Iterator $field;
  • 支持并解析对其他定义的引用

    $otherDefinition = definitionOf(OtherClass::class);
    $definition->withField('field', $otherDefinition);

    结果

    private OtherClass $field;
  • 可以使用 nullable() 表达 可空性

    $definition->withField('field', Types\nullable('string'))

    结果

    private ?string $field;
  • 可以使用 listOf() 创建 列表

    $definition->withField('field', Types\listOf('string'))

    结果

    /** @var string[] */
    private array $field;
  • 可以使用 collection() 创建 键值集合

    $definition->withField('field', Types\collection(Collection::class, UserId::class, User::class))

    结果

    /** @var Collection<UserId, User> */
    private Collection $field;
  • 可以使用 generic() 表示 任何泛型类型

    $definition->withField('field', Types\generic(\SerializedValue::class, User::class))

    结果

    /** @var SerializedValue<User> */
    private SerializedValue $field;
  • 可以使用 arrayShape() 描述 复杂数组形状

    $definition->withField('field', Types\arrayShape(['key' => 'string', 'optional?' => 'int']))

    结果

    /** @var array{key: string, optional?: int} */
    private array $field;
  • 同样,可以使用 tuple() 创建 元组

    $definition->withField('field', Types\tuple('string', Types\nullable('int')))

    结果

    /** @var array{string, int|null} */
    private array $field;
  • 也支持 联合和交集

    $definition->withField('field', Types\union('int', 'string'))
               ->withField('other', Types\intersection(\Traversable::class, \Countable::class))

    结果

    private int|string $field;
    private Traversable&Countable $other;

功能

生成的代码中不表示字段本身,它们仅描述结果类应包含的字段。要向类添加任何行为,您需要向其添加功能。Scaffolder 预先准备好了一组用于最常见用例的功能

  • properties() 为每个字段生成一个私有属性

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\properties())

    结果

    final class Foo
    {
        private string $field;
    }
  • initializingConstructor() 生成一个带有属性赋值的公共构造函数。这最好与 properties() 功能结合使用

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\properties())
        ->with(Capabilities\initializingConstructor())

    结果

    final class Foo
    {
        private string $field;
    
        public function __construct(string $field)
        {
            $this->field = $field;
        }
    }
  • constructorWithPromotedProperties() 生成一个带有提升属性的公共构造函数。在 PHP 8+ 代码中,这可以替代前面的两个功能

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())

    结果

    final class Foo
    {
        public function __construct(private string $field)
        {
        }
    }
  • readonlyProperties() 将属性或提升参数设置为公共和只读

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\readonlyProperties())

    结果

    final class Foo
    {
        public function __construct(public readonly string $field)
        {
        }
    }
  • privatizedConstructor() 将类构造函数设置为私有

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\privatizedConstructor())

    结果

    final class Foo
    {
        private function __construct(private string $field)
        {
        }
    }
  • namedConstructor($name) 创建一个公共静态命名构造函数

    definitionOf(FooEvent::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\privatizedConstructor())
        ->with(Capabilities\namedConstructor('occurred'))

    结果

    final class FooEvent
    {
        private function __construct(private string $field)
        {
        }
    
        public static function occurred(string $field): self
        {
            return new self($field);
        }
    }
  • getters() 为所有字段生成公共获取器

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\getters())

    结果

    final class Foo
    {
        public function __construct(private string $field)
        {
        }
    
        public function getField(): string
        {
            return $this->field;
        }
    }
  • setters() 为所有字段生成公共设置器

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\setters())

    结果

    final class Foo
    {
        public function __construct(private string $field)
        {
        }
    
        public function setField(string $field): void
        {
            $this->field = $field;
        }
    }
  • immutableSetters() 为所有字段生成公共以...结束的设置器

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\getters())

    结果

    final class Foo
    {
        public function __construct(private string $field)
        {
        }
    
        public function withField(string $field): self
        {
            $self = clone $this;
            $self->field = $field;
            return $self;
        }
    }
  • implementedInterface() 向生成的类添加 implements 子句

    definitionOf(Foo::class)
        ->withField('field', 'string')
        ->with(Capabilities\implementedInterface(\IteratorAggregate::class))

    结果

    final class Foo implements IteratorAggregate
    {
    }

    ⚠️ 请注意,scaffolder 并不检查您的类是否实际上满足给定的接口。您可以使用 preservedAnnotatedMethods() 功能(见下文)提供实现。

添加和保留逻辑

Scaffolder 每次运行时都会重新生成您的类。如果您对生成的类进行了任何更改,则下一次运行 scaffolder 时将丢失这些更改。(Scaffolder 通过使生成的文件为只读来防止这种情况,但这可以很容易地解决。)然而,DTO 也可以包含一些简单的逻辑,例如连接姓名的首字母和末尾字母。

考虑以下定义

return [
    definitionOf(Name::class, withFields: [
        'firstName' => 'string',
        'lastName' => 'string',
    ])
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\getters()),
];

它将生成以下类

<?php

/**
 * Do not edit. This is generated file. Modify definition file instead.
 */

declare(strict_types=1);

final class Name
{
    public function __construct(
        private string $firstName,
        private string $lastName,
    ) {
    }
    
    
    public function getFirstName(): string{
        return $this->firstName;
    }


    public function getLastName(): string{
        return $this->lastName;
    }
}

我们想要添加一个 getFullName() 方法,并在 scaffolder 下次运行时保留它。技巧是用 #[Preserve] 属性标记该方法

#[\Grifart\ClassScaffolder\Preserve]
public function getFullName(): string
{
    return $this->firstName . ' ' . $this->lastName;
}

并将 preservedAnnotatedMethods() 功能添加到定义中

$definition->with(Capabilities\preservedAnnotatedMethods())

下次您运行 scaffolder 时,只要 getFullName() 方法具有 #[Preserve] 属性,它就会保持完整。

或者,您可以使用 preservedMethod($methodName) 功能,该功能仅保留在功能函数中显式列出的方法。

⚠️ 方法保留功能最好与 preservedUseStatements() 功能一起使用,该功能确保保留了原始文件中的所有 use 语句。

自定义功能

功能是一个非常简单的接口,因此您可以轻松地创建和使用自己的

use Grifart\ClassScaffolder\Capabilities\Capability;
use Grifart\ClassScaffolder\ClassInNamespace;
use Grifart\ClassScaffolder\Definition\ClassDefinition;

final class GetFullNameMethod implements Capability
{
    public function applyTo(
        ClassDefinition $definition, // lets you access the list of defined fields
        ClassInNamespace $draft,     // this is the prescription of the newly generated class
        ?ClassInNamespace $current,  // this describes the original class if it already exists
    ): void
    {
        $draft->getClassType()->addMethod('getFullName')
            ->setReturnType('string')
            ->addBody('return $this->firstName . " " . $this->lastName;');
    }
}

ℹ️ 提示:如果您只需要单一用途的功能,您可以将其定义为匿名类。例如

->with(new class implements Capability {
  function applyTo() { /* the transformation */ }
});

不要重复自己

由于定义文件是一个普通的 PHP 文件,因此您可以使用任何语言结构来利用它。我们通常定义函数来预配置功能,甚至为重复模式定义字段

use Grifart\ClassScaffolder\Capabilities;
use Grifart\ClassScaffolder\Definition\ClassDefinition;
use function Grifart\ClassScaffolder\Definition\definitionOf;

function valueObject(string $className): ClassDefinition
{
    return definitionOf($className)
        ->with(Capabilities\constructorWithPromotedProperties())
        ->with(Capabilities\getters());
}

然后可以在定义文件中轻松重用这些函数

return [
    $tag = valueObject(Tag::class)
        ->withField('name', 'string'),

    valueObject(Article::class)
        ->withField('id', 'int')
        ->withField('title', 'string')
        ->withField('content', 'string')
        ->withField('tags', listOf($tag))
        ->withField('archivedAt', nullable(\DateTime::class)),
];

⚠️ Scaffolder 依赖于 Composer 自动加载器。为了能够在定义文件中访问您的函数,您应该将它们添加到 composer.json 中的 files 自动加载部分,或者将它们包裹在由 Composer 自动自动加载的静态类中。如果您有自己的自定义自动加载器,请将此库作为命令注册到您的应用程序中。然后它将使用您的自定义环境。