honeystone / laravel-dto-tools
Laravel 的 DTO 工具集。
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0
- spatie/laravel-package-tools: ^1.16.4
Requires (Dev)
- nunomaduro/collision: ^8.0
- nunomaduro/larastan: ^2.9
- nunomaduro/phpinsights: ^2.11
- orchestra/testbench: ^9.2
- pestphp/pest: ^2.0
- pestphp/pest-plugin-laravel: ^2.4
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^1.2
- phpstan/phpstan-phpunit: ^1.4
- phpunit/phpunit: ^10.0
- spatie/laravel-ray: ^1.37
This package is auto-updated.
Last update: 2024-09-14 18:29:37 UTC
README
DTO 工具包是一个用于为您的原生 PHP 数据传输对象(DTO)提供额外功能和便利的包。此包的主要动机是移除在数据在 DTO 之间移动过程中产生的许多样板代码。例如,将蛇形模型属性转换为驼峰式以供您的表示层消费,或将用户输入的数值字符串(当然是在验证之后)转换为整数。
功能包括属性转换和变异、序列化、补丁数据处理、关系以及模型和集合转换。
支持我们
我们致力于提供由 Honeystone 团队维护的高质量开源包。如果您想支持我们的努力,只需使用我们的包,推荐它们并贡献。
如果您在项目中需要任何帮助,或需要任何定制开发,请联系我们。
安装
composer require honeystone/laravel-dto-tools
使用以下命令发布配置文件:
php artisan vendor:publish --tag=honeystone-dto-tools-config
用法
此包对现有 DTO 的修改非常少。至少,您的 DTO 需要实现 Transferable
合同并使用 HasTransferableData
特性,如下所示
<?php declare(strict_types=1); namespace App\Domains\Articles\Data; use App\Domains\Articles\Data\Enums\Status; use Honeystone\DtoTools\Concerns\HasTransferableData; use Honeystone\DtoTools\Contracts\Transferable; final readonly class ArticleData implements Transferable { use HasTransferableData; public function __construct( public string $title, public ?string $description, public Status $status, public string $modified, ) { } }
如你所见,这仍然只是一个普通的 readonly
PHP 对象。您也可以正常实例化它,但是,如果您想使用属性转换,您需要使用静态的 make()
方法
$data = ArticleData::make( title: $source['title'], description: $source['description'] === '' ? null : $source['description'], status: $source['status'] === 'published' ? Status::PUBLISHED : Status::DRAFT, modified: Carbon::make($source['modified'])->toIso8601String(), );
这真的很乱,所以我们将在下一节中清理它。
属性转换
转换器用于在实例化 DTO 之前拦截和转换/变异值。它们使用 PHP 属性实现。在以下示例中,我们将确保我们的空描述转换为 null
,将状态转换为枚举,并将修改时间表示为 iso-8601 字符串
use App\Domains\Articles\Data\Enums\Status; use Honeystone\DtoTools\Casters\DateTimeCaster; use Honeystone\DtoTools\Casters\EnumCaster; use Honeystone\DtoTools\Casters\NullCaster; final readonly class ArticleData implements Transferable { use HasTransferableData; public function __construct( public string $title, #[NullCaster('')] public ?string $description, #[EnumCaster(Status::class)] public Status $status, #[DateTimeCaster] public string $modified, ) { } }
我们的数据现在可以稍微宽松一些,但我们仍然从 DTO 的类型安全性中受益
$data = ArticleData::make( title: $source['title'], description: $source['description'], status: $source['status'], modified: $source['modified'], ); //or even cleaner $data = ArticleData::make(Arr::only($source, 'title', 'description', 'status', 'modified'));
以下提供了许多转换器,或者您可以创建自己的
#[DateTimeCaster('Y-m-d')] //takes a format string, or defaults to iso-8601 #[EnumCaster(Status::class, State::class)] //takes one or more enum class strings #[NullCaster('', '-')] //takes one or more values that should be converted to null #[ScalarCaster('bool', 'null')] //takes one or more accepted types, if no types match it will cast to the last type
要实现自己的转换器,只需创建一个实现 Honeystone\DtoTools\Casters\Contracts\CastsValues
的 Attribute
。
<?php declare(strict_types=1); namespace App\Support\Data\Casters; use Attribute; use Honeystone\DtoTools\Casters\Contracts\CastsValues; #[Attribute] final readonly class MyCaster implements CastsValues { public function cast(mixed $value): mixed { //... } }
序列化
有两种主要的序列化方法可用,getAttributes()
和 toArray()
。区别在于 getAttributes()
将简单地提供一个包含属性值的数组,而 toArray()
将递归地将 Transferable
和 Arrayable
属性转换为数组。
还有 getRelationships()
和 toStorableArray()
,但我们稍后再谈。
属性转换
有时您需要对进入 DTO 的所有参数进行重大更改。为此,您可以在 DTO 中添加一个静态的 transformIncoming()
方法
final readonly class SomeData implements Transferable { use HasTransferableData; public function __construct( public string $foo, public string $bar, ) { } protected static function transformIncoming(array $parameters): array { return array_map(static fn (string $value): string => "🔥 $value 🔥", $parameters); } }
echo SomeData::make(['foo', 'bar'])->getAttributes(); //['foo' => '🔥 foo 🔥', 'bar' => '🔥 bar 🔥']
对于 DTO 序列化,还可以实现 transformOutgoing()
方法
final readonly class SomeData implements Transferable { use HasTransferableData public function __construct( public string $foo, public string $bar, ) { } protected static function transformOutgoing(array $parameters): array { return array_map(static fn (string $value): string => "🔥 $value 🔥", $parameters); } }
echo SomeData::make(['foo', 'bar'])->getAttributes(); //['foo' => 'foo', 'bar' => 'bar'] echo SomeData::make(['foo', 'bar'])->toArray(); //['foo' => '🔥 foo 🔥', 'bar' => '🔥 bar 🔥']
在 Laravel 中,一个非常常见的用例是将蛇形属性转换为驼峰式。因此,提供了 CreatableFromSnake
和 SerializesToSnake
特性
use Honeystone\DtoTools\Concerns\CreatableFromSnake; use Honeystone\DtoTools\Concerns\SerializesToSnake; final readonly class SomeData implements Transferable { use HasTransferableData, CreatableFromSnake, SerializesToSnake; public function __construct( public string $someProperty, ) { } }
echo SomeData::make(some_property: 'value')->getAttributes(); //['someProperty' => 'value'] echo SomeData::make(some_property: 'value')->toArray(); //['some_property' => 'value']
可存储数据
我们已经了解了 Transferable
数据,这对于在服务层传递完整数据非常好,但如果您需要处理部分数据(即补丁)怎么办?这时就出现了 Storable
。
<?php declare(strict_types=1); namespace App\Domains\Articles\Data; use App\Domains\Articles\Data\Enums\Status; use Honeystone\DtoTools\Concerns\HasStorableData; use Honeystone\DtoTools\Contracts\Storable; final class ArticlePatchData implements Storable { use HasStorableData; public function __construct( public readonly string $title, public readonly ?string $description, public readonly Status $status, public readonly string $modified, ) { } }
基本的 Storable
与 Transferable
非常相似,除了类本身不能是 readonly
。 Storable
需要一点状态才能运行,因此我们用 readonly
标记我们的属性。
默认情况下,可存储对象不在修补模式。为此,我们需要添加 Patch
类属性
<?php use Honeystone\DtoTools\Attributes\Patch; #[Patch] final class ArticlePatchData implements Storable { use HasStorableData; public function __construct( //... ) { } }
我们可以使用 isPatching()
方法检查可存储对象是否处于修补模式。
当可存储对象处于修补模式时,使用 toStorableArray()
方法将自动排除 null
值进行序列化
echo SomeData::make(foo: 'value', bar: null)->toStorableArray(); //['foo' => 'value'] echo SomeData::make(foo: 'value', bar: null)->toArray(); //['foo' => 'value', 'bar => null]
您也可以使用 isStorable()
方法检查单个属性是否可以存储
$data = echo SomeData::make(foo: 'value', bar: null); $data->isStorable('foo'); //true $data->isStorable('bar'); //false
有时,null
是一个有效的值并且应该被存储。在这些情况下,您可以使用 force()
方法将这些属性标记为可存储
$data = echo SomeData::make(foo: 'value', bar: null); $data->force('bar'); $data->isStorable('bar'); //true
如果您需要强制属性列表,请使用 getForced()
。
可存储关系
偶尔,当将数据传输到您的服务层时,您需要表示关系结构的更改。您可以通过在您的 DTO 上设置一个简单的属性来实现这一点,例如
<?php #[Patch] final class ArticlePatchData implements Storable { use HasStorableData; /** * @param array<int>|null $tags */ public function __construct( //... public ?array $tags = null, ) { } }
尽管这种方法有几个问题:没有真正的类型安全,您不能随意添加或删除标签,您必须提供所有标签,并且不能有任何元数据(例如顺序)。也许您可以将它升级为 DTO 数组,但有一个更简单的方法
<?php use Honeystone\DtoTools\Attributes\ToMany; #[Patch] #[ToMany(['tags' => 'int|empty'])] final class ArticlePatchData implements Storable { use HasStorableData; public function __construct( //... ) { } }
使用 ToMany
类属性,我们可以声明一个名为 'tags' 的多对多关系,该关系可以为空,或为整数。
我们现在可以添加、删除和替换相关的标签
$data = ArticlePatchData::make(...); $data->relationships()->addToManyRelation('foo', 123, ['priority' => 5]);
关系使用相关 ID 存储。这可以是整数或字符串。您还可以提供一个 Transferable
或 Storable
,库将使用其 getKey()
方法来确定 ID。还可以作为数组或 Transferable
提供附加元数据
$data = ArticlePatchData::make(...); $data->relationships()->addToManyRelation('foo', TagData::make(...), TagMetaData::make(...));
以下关系类 Attributes
受支持
#[ToOne(['foo' => 'int|string|null'])] #[ToMany(['bar' => 'int|string|empty'])]
以下关系方法可用
$data->relationships()->hasToOne('foo'); $data->relationships()->getOneRelated('foo'); $data->relationships()->setOneRelated('foo', 123); $data->relationships()->unsetOneRelated('foo'); $data->relationships()->hasToMany('bar'); $data->relationships()->getManyRelated('bar'); //plus any additions, minus any removals $data->relationships()->getManyAdditions('bar'); $data->relationships()->getManyRemovals('bar'); $data->relationships()->addToManyRelation('bar', 123, []); //param 3 optional $data->relationships()->removeToManyRelation('bar', 123); $data->relationships()->replaceToMany('bar', 123, []); //param 3 optional, clears addition and removals $data->relationships()->resetToMany('bar'); //clears addition and removals $data->relationships()->getMetaData('bar'); $data->relationships()->setMetaData('bar', []);
序列化关系包含在 toStorableArray()
中,或者您可以使用 getRelationships()
仅获取关系。
模型转换
将 Model
转换为 DTO 并不罕见。但它们将有所不同。DTO 的数据应更具体和情境化。这可能导致大量样板代码来处理转换。本包包含一个 abstract
ModelTransformer
以帮助清理。
以下是最基本的示例
<?php declare(strict_types=1); namespace App\Domains\Articles\Data\Transformers; use App\Domains\Articles\Data\ArticleData; use Honeystone\DtoTools\Transformers\ModelTransformer; final class ArticleTransformer extends ModelTransformer { protected string $dataClass = ArticleData::class; }
您现在可以调用转换器的 transform()
和 transformCollection()
方法,将您的 Model
转换为 Transferable
。
$transformer = app(ArticleTransformer::class); $transformer->transform(Article::first()); $transformer->transformCollection(Article::all());
内部此示例将使用模型的自定义 toArray()
方法。
我们可以使用 $only
属性具体指定要包含在转换中的字段
final class ArticleTransformer extends ModelTransformer { protected string $dataClass = ArticleData::class; protected array $only = [ 'title', 'description', 'status', 'modified', ]; }
如果需要执行更复杂的操作,我们可以创建一个 map()
方法
final class ArticleTransformer extends ModelTransformer { protected string $dataClass = ArticleData::class; protected function map(Model $model): array { return [ 'title' => Str::title($model->title), 'status' => in_array($model->status, ['published', 'active']) ? 'published' : 'draft', ...$model->only('description', 'modified'), ]; } }
我们还可以将附加参数传递给 map 方法
$transformer->transform(Article::first(), foo: 'bar'); $transformer->transformCollection(Article::all(), foo: 'bar', bar: 'baz');
override()
和 exclude()
方法可以链接起来,以便在转换时进行即时更改
$transformer ->exclude('foo', 'bar') ->override(['status' => 'preview']) ->transform(Article::first()); //transform() or transformCollection()
那很好,但关系怎么办?
final class ArticleTransformer extends ModelTransformer { protected string $dataClass = ArticleData::class; protected function map(Model $model): array { return [ 'title' => Str::title($model->title), 'status' => in_array($model->status, ['published', 'active']) ? 'published' : 'draft', ...$model->only('description', 'modified'), 'tags' => $this->includeRelated('tags', $model->tags), 'category' => $this->requireRelated('category', $model->category), ]; } }
includeRelated()
和 requireRelated()
方法将使用本包配置文件中的转换映射,根据 ModelTransformer
转换 Model
。
requireRelated()
方法如果关系尚未加载,将抛出 Honeystone\DtoTools\Exceptions\RequiredRelationNotLoadedException
。
您可以传递附加参数给这些方法,这些参数将传递给相应的 ModelTransformer
的 map()
方法。
任何排除或覆盖也可以作为附加参数包含
final class ArticleTransformer extends ModelTransformer { protected string $dataClass = ArticleData::class; protected function map(Model $model): array { return [ //... 'category' => $this->requireRelated( 'category', $model->category, exclude: ['foo', 'bar'], override: ['status' => 'preview'], bar: 'baz', //passed onto map() method ), ]; } }
就是这样!如果您发现此包有用,我们非常乐意听到您的意见。
变更日志
更改列表可在 CHANGELOG.md 文件中找到。