grifart / scaffolder
类 scaffolder。编写定义,生成简单的值持有者。适用于在事件源应用中使用的简单复合类型 - 用于命令、事件和查询定义类。这主要补充了公共只读 $properties
Requires
- php: ^8.0
- ext-tokenizer: *
- nette/finder: ^2.5||^3.0
- nette/php-generator: ^4.0.1
- nette/utils: ^3.0||^4.0
- symfony/console: ^6.0
- symfony/filesystem: ^6.0
Requires (Dev)
- nette/tester: ^2.0.0
- nikic/php-parser: ^4.10
- php-parallel-lint/php-console-color: ^1.0
- php-parallel-lint/php-parallel-lint: ^1.2
- phpstan/phpstan: ^1.0
- tracy/tracy: ^2.7.5
Suggests
- nikic/php-parser: To be able to use Preserve attribute.
- tracy/tracy: For more detailed error messages.
Replaces
- grifart/class-scaffolder: 0.6.7
This package is auto-updated.
Last update: 2024-09-24 12:06:28 UTC
README
它被设计用来生成具有无到简单逻辑的类。典型用法是
- 数据传输对象 (DTOs),
- 事件源模型中的事件,
- 简单的值对象(可以通过
#[Preserve]
属性嵌入简单逻辑 - 见下文)。
它在 gitlab.grifart.cz 开发,自动镜像到 GitHub 并通过 Composer packagist:grifart/scaffolder 分发。
您还可以在 🎥 YouTube 上观看介绍(捷克语)。
安装
我们建议使用 Composer
composer require grifart/scaffolder
快速入门
- 创建定义文件。定义文件必须返回一个
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(), ) ];
- 运行 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
- 您的类已准备好。 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; } }
-
使用静态分析工具,例如 PHPStan 或 Psalm,以确保如果您更改了任何定义文件,一切仍然正常工作。
-
确保您没有意外更改任何生成的文件,请将
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 自动自动加载的静态类中。如果您有自己的自定义自动加载器,请将此库作为命令注册到您的应用程序中。然后它将使用您的自定义环境。