krak / struct-gen
Requires
- php: ^7.2
- composer-plugin-api: ^1.1 | ^2.0
- krak/lex: ^1.0
- nikic/php-parser: ^4.3
Requires (Dev)
- composer/composer: ^2.0
- phpunit/phpunit: ^8.0 || ^9.0
- symfony/finder: ^5.1
- symfony/var-dumper: ^5.0
- vimeo/psalm: ^3.8
Suggests
- symfony/finder: Support glob based patterns for directory traversal.
README
使用简单的PHP类定义struct,包含带类型定义的属性。包含StructTrait,让库为您生成所有模板代码,以便制作不可变的值对象,这些对象与IDE和静态分析工作得很好。
安装
使用composer安装 krak/struct-gen
用法
给定一些使用特殊特质 {className}Struct
(其中{className}是实际的类名)的类定义
<?php namespace App\Catalog; final class Product { use ProductStruct; /** @var int */ private $id; /** @var ?string */ private $code; /** @var Category[] */ private $categories; } final class Category { use CategoryStruct; /** @var int */ private $id; /** @var string */ private $name; }
运行 composer struct-gen:generate
,然后这些特质会被填充方法以允许类似以下API的操作
<?php $product = new App\Catalog\Product(1, null, []); $product->id(); $product->code(); $product = $product->withCategories([ App\Catalog\Category::fromValidatedArray(['id' => 1, 'name' => 'Nike']) ]); $product->toArray(); // ['id' => 1, 'code' => null, [['id' => 1', 'name' => 'Nike']]]
生成的Struct特质提供了制作不可变值对象所需的所有模板代码。
<?php namespace App\Catalog; trait ProductStruct { /** @param Category[] $categories */ public function __construct(int $id, ?string $code, array $categories) { $this->id = $id; $this->code = $code; $this->categories = $categories; } public static function fromValidatedArray(array $data) : self { return new self($data['id'], $data['code'], \array_map(function (array $value) : Category { return Category::fromValidatedArray($value); }, $data['categories'])); } public function toArray() : array { return ['id' => $this->id, 'code' => $this->code, 'categories' => \array_map(function (Category $value) : array { return $value->toArray(); }, $this->categories)]; } public function id() : int { return $this->id; } public function code() : ?string { return $this->code; } /** @return Category[] */ public function categories() : array { return $this->categories; } public function withId(int $id) : self { $self = clone $this; $self->id = $id; return $self; } public function withCode(?string $code) : self { $self = clone $this; $self->code = $code; return $self; } /** @param Category[] $categories */ public function withCategories(array $categories) : self { $self = clone $this; $self->categories = $categories; return $self; } } trait CategoryStruct { public function __construct(int $id, string $name) { $this->id = $id; $this->name = $name; } public static function fromValidatedArray(array $data) : self { return new self($data['id'], $data['name']); } public function toArray() : array { return ['id' => $this->id, 'name' => $this->name]; } public function id() : int { return $this->id; } public function name() : string { return $this->name; } public function withId(int $id) : self { $self = clone $this; $self->id = $id; return $self; } public function withName(string $name) : self { $self = clone $this; $self->name = $name; return $self; } }
配置路径
要配置要生成struct的路径,您可以轻松地将路径传递给 struct-gen:generate
命令。默认情况下,它查找 ./src
文件夹。
您还可以在composer.json中配置要搜索的路径,这样您就不必每次运行命令时都列出路径。
{ "extra": { "struct-gen": { "paths": ["src/*/DTO", "lib/DTO"] } } }
生成文件与内联生成
默认情况下,struct-gen将生成的struct保存到与原始类文件内联的php文件中。在此格式中,生成的struct应提交到您的仓库中。
但是,您可以配置struct-gen将所有生成的struct保存到一个单独的文件中,并自动让composer将该文件注册为类映射。
要这样做,只需更新您的composer json配置如下
{ "extra": { "struct-gen": { "generated-path": ".generated-structs.php" } } }
该文件可以提交到您的仓库,或在CI管道上运行以确保struct的最新版本可用。
生成器
struct是从一组实现CreateStructStatements接口的生成器生成的。每个生成器负责构建最终struct的一部分。
构造函数生成
基于类中的所有属性生成构造函数。如果系统检测到已定义了构造函数,则不会向struct特质添加构造函数。
从验证数组构造函数生成
静态构造函数接受一个假定有效的数组并将其转换为对象表示形式。这可以看作是 toArray
的逆操作。术语 validated
的意思是,此操作在非验证用户数据上调用是不安全的。
此构造函数可以处理嵌套struct和struct集合。库天真地假设任何类型为类的属性都必须实现 fromValidatedArray
函数。所以如果您的struct包含没有该函数的对象,则在调用 fromValidatedArray
时会收到错误。
获取器生成
所有获取器都是基于属性生成的,不使用 get
前缀。它们只是将属性名作为函数调用。
Wither生成
所有struct默认为不可变,因此,如果您想更改struct的值,可以使用 with{propName}
约定来设置值并返回一个具有更改值的新的实例。
转换为数组
将struct转换为数组表示形式。这也适用于嵌套struct和struct集合。
生成struct选项
在 use {className}Struct
之上,您可以指定用于该特定类的选项,这些选项可以通过 @struct-gen
文档块标签影响生成过程。
格式为 @struct-gen {option-name} ?{option-value}
。其中值是可选的,可以是简单的字符串、逗号分隔的列表或 JSON 字符串。
以下是一个示例
<?php class Acme { /** @struct-gen generate getters,withers */ use AcmeStruct; // ... }
您可以有多个 struct-gen 标签,具有不同的选项名称和值,所有这些都会合并在一起。
生成
生成选项允许您控制特定结构使用的生成器。如果您只想使用获取器和设置器,可以相应地指定。
您可以使用以下生成器名称列表:constructor,from-validated-array,to-array,getters,withers
持续集成设置
如果您直接在仓库中提交 struct gen 变更,那么您需要确保在将任何提交合并到主分支之前,struct-gen 已正确运行和测试。
在这种情况下,您应该在 CI 测试管道中运行 struct gen 时使用 --fail-on-changes
标志。如果检测到任何更改,它将以退出代码 1 失败,在大多数 CI 管道中这将导致构建失败,确保提交补丁的开发者已使用最新更改进行了测试,并且没有修改任何生成的文件。
示例
- composer struct-gen:generate --fail-on-changes
如果您通过 generatedPath
选项将结构生成到外部文件,并且在该文件中忽略 CVS,那么您需要确保在运行 composer install 之前运行 struct-gen。
- composer struct-gen:generate -vv # some point later - composer install --no-dev -o
为什么使用静态生成?
大多数 DTO 或结构的库对 IDE 不友好,并且通过运行时魔法和反射提供对结构的有用方法的访问。这些方法不仅会有轻微的性能损失,而且难以在同时与 IDE 和静态分析工具良好协作的情况下实现类型安全。
开发
运行 composer
composer install
Psalm
./vendor/bin/psalm
PHPUnit
./vendor/bin/phpunit
测试 Composer 插件
composer 插件目前正在另一个本地仓库中手动仔细测试,该仓库需要本地使用 struct-gen 包。我通常会手动测试所有功能。
路线图
- 从非验证数组创建
- 改进 Psalm 对类型定义的支持
- 插件系统
- 从 Open Api 3 生成
- 导出到 Open API 3