romanzipp / laravel-dto
Laravel 的强类型数据传输对象集成
Requires
- php: ^8.1
- ext-json: *
- romanzipp/dto: ^2.3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- laravel/framework: ^9.45
- orchestra/testbench: ^3.8|^4.0|^5.0|^6.0|^7.0
- phpstan/phpstan: ^0.12.99|^1.0
- phpunit/phpunit: ^9.0
- romanzipp/php-cs-fixer-config: ^3.0
README
为 Laravel 提供无魔法强类型数据传输对象 for Laravel
此包扩展了 romanzipp/DTO 的功能,为 Laravel 应用提供更具体的用例。
Laravel-DTO 作为请求输入与验证和模型属性填充之间的 中间和可重用层。
内容
安装
composer require romanzipp/laravel-dto
使用
所有数据对象都必须扩展 romanzipp\LaravelDTO\AbstractModelData
类。
验证
当附加 #[ValidationRule]
时,任何给定数据都将传递给 Laravel 验证器,以便您可以使用所有 可用的验证规则 以及内置规则实例。
use App\Models\Person; use App\Models\Project; use Illuminate\Validation\Rules\Exists; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ForModel; use romanzipp\LaravelDTO\Attributes\ValidationRule; use romanzipp\LaravelDTO\Attributes\ValidationChildrenRule; class PersonData extends AbstractModelData { #[ValidationRule(['required', 'string', 'min:1', 'max:255'])] public string $name; #[ModelAttribute(['sometimes', 'min:18'])] public int $currentAge; #[ValidationRule(['nullable', 'string', 'in:de,en'])] public ?string $language; #[ValidationRule(['required', 'numeric', new Exists(Project::class, 'id')])] public int $projectId; #[ValidationRule(['required', 'array', 'min:1']), ValidationChildrenRule(['string'], '*.device'), ValidationChildrenRule(['ipv4'], '*.ip')] public array $logins; }
如果任何规则未通过,这将抛出 Illuminate\Validation\ValidationException
。
$data = new PersonData([ 'name' => 'John Doe', 'currentAge' => 25, 'language' => 'de', 'projectId' => 2, 'logins' => [ ['device' => 'PC', 'ip' => '85.120.61.36'], ['device' => 'iOS', 'ip' => '85.120.61.36'], ] ]);
填充模型
您可以使用 #[ForModel(Model::class)]
属性将任何模型附加到任何 DTO。要将 DTO 属性与模型属性关联,您需要将 #[ModelAttribute()]
属性附加到每个属性。如果未向 #[ModelAttribute]
属性传递参数,则 DTO 使用属性名称本身。
use App\Models\Person; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ForModel; use romanzipp\LaravelDTO\Attributes\ModelAttribute; #[ForModel(Person::class)] class PersonData extends AbstractModelData { #[ModelAttribute] // The `$name` DTO property will populate the `name` model attribute public string $name; #[ModelAttribute('current_age')] // The `$currentAge` DTO property will populate the `current_age` model attribute public int $currentAge; public string $language; // The `$language` DTO property will be ignored } $data = new PersonData([ 'name' => 'John Doe', 'currentAge' => 25, 'language' => 'de', ]); $person = $data->toModel()->save();
Person
模型中保存的属性
注意:您还可以将现有模型传递给 toModel()
方法。
use App\Models\Person; $person = $data->toModel($person)->save();
注意:当将 没有 现有模型传递给 toModel()
方法时,DTO 中声明的默认值将被填充。如果传递模型作为参数 toModel($model)
,则默认值不会覆盖现有的模型属性。
从请求输入数据填充 DTO
当附加 #[RequestAttribute]
并通过 fromRequest(Request $request)
方法创建 DTO 实例时,所有匹配的属性都将通过输入数据填充。如果未向 #[RequestAttribute]
属性传递参数,则 DTO 使用属性名称本身。
use App\Models\Person; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ForModel; use romanzipp\LaravelDTO\Attributes\ModelAttribute; use romanzipp\LaravelDTO\Attributes\RequestAttribute; #[ForModel(Person::class)] class PersonData extends AbstractModelData { #[RequestAttribute] // The `$name` DTO property will be populated by the `name` request attribute public string $name; #[RequestAttribute('my_age')] // The `$currentAge` DTO property will be populated by `my_age` request attribute public int $currentAge; public string $language; // The `$language` DTO property will not be populated }
控制器
use App\Data\PersonData; use Illuminate\Http\Request; class TestController { public function store(Request $request) { $data = PersonData::fromRequest($request); } }
请求输入数据
{ "name": "John Doe", "my_age": 25, "language": "de" }
PersonData
DTO 实例
App\Data\PersonData^ {
+name: "John Doe"
+currentAge: 25
}
组合使用
当然,如果一起使用,所有这些属性都开始变得有意义。您可以单独附加所有属性,或者使用 #[ValidatedRequestModelAttribute]
属性,该属性结合了所有 #[RequestAttribute]
、#[ModelAttribute]
和 #[ValidationRule]
属性的功能。
以下示例中的两个属性表现完全相同。根据您的喜好使用。
use App\Models\Person; use Illuminate\Validation\Rules\Exists; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ForModel; use romanzipp\LaravelDTO\Attributes\ModelAttribute; use romanzipp\LaravelDTO\Attributes\RequestAttribute; use romanzipp\LaravelDTO\Attributes\ValidatedRequestModelAttribute; use romanzipp\LaravelDTO\Attributes\ValidationRule; #[ForModel(Person::class)] class PersonData extends AbstractModelData { // All attributes attached separately (looks disgusting doesn't it?) #[ ValidationRule(['required', 'numeric', 'min:18']), RequestAttribute('my_age'), ModelAttribute('current_age') ] public string $currentAge; // The `my_age` request attribute will be validated and set to the `current_age` model attribute. // // RequestAttribute // ValidationRule │ ModelAttribute // ┌────────────────┴──────────────┐ ┌──┴───┐ ┌─────┴─────┐ #[ValidatedRequestModelAttribute(['required', 'numeric', 'min:18'], 'my_age', 'current_age')]; public string $currentAge; }
请求输入数据
{ "my_age": 25 }
控制器
use App\Data\PersonData; use Illuminate\Http\Request; class TestController { public function index(Request $request) { $person = PersonData::fromRequest($request)->toModel()->save(); return $person->id; } }
验证数组
如果您只想验证一个数组,而不将子项目转换为另一个 DTO,您可以使用 ValidationChildrenRule
属性。
ValidationChildrenRule
属性的第一个参数是子项的验证规则。第二个参数是验证器路径,用于访问子键进行验证。
验证具有数字索引的简单数组
use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ValidationChildrenRule; class PersonData extends AbstractModelData { #[ValidationChildrenRule(['string', 'ipv4'], '*')]; public array $logins; } $data = new PersonData([ 'logins' => [ '127.0.0.1', '127.0.0.1' ] ]);
验证具有命名键的关联数组
use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ValidationChildrenRule; class PersonData extends AbstractModelData { #[ValidationChildrenRule(['string', 'ipv4'], '*.ip')]; public array $logins; } $data = new PersonData([ 'logins' => [ ['ip' => '127.0.0.1'], ['ip' => '127.0.0.1'] ] ]);
多个验证规则
use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ValidationChildrenRule; class PersonData extends AbstractModelData { #[ ValidationChildrenRule(['string', 'ipv4'], '*.ip'), ValidationChildrenRule(['string'], '*.device') ]; public array $logins; } $data = new PersonData([ 'logins' => [ ['ip' => '127.0.0.1', 'device' => 'iOS'], ['ip' => '127.0.0.1', 'device' => 'macOS'] ] ]);
将数组转换为 DTO(嵌套数据)
在某些情况下,您还希望通过单个 HTTP 请求创建相关模型。在这种情况下,您可以使用 #[NestedModelData(NestedData::class)]
,它将为定义的 DTO 填充 n 个实例。
请注意,由于不应将其设置为模型属性,因此我们不会将 #[ModelAttribute]
属性附加到 $address
DTO 属性。
附加到嵌套 DTO 的所有属性都将按预期工作。
use App\Models\Person; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ForModel; use romanzipp\LaravelDTO\Attributes\NestedModelData; use romanzipp\LaravelDTO\Attributes\RequestAttribute; use romanzipp\LaravelDTO\Attributes\ValidatedRequestModelAttribute; use romanzipp\LaravelDTO\Attributes\ValidationRule; #[ForModel(Person::class)] class PersonData extends AbstractModelData { #[ValidatedRequestModelAttribute(['required', 'string'])] public string $name; /** * @var AddressData[] */ #[NestedModelData(AddressData::class), ValidationRule(['required', 'array']), RequestAttribute] public array $adresses; }
use App\Models\Address; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ValidatedRequestModelAttribute; #[ForModel(Address::class)] class AddressData extends AbstractModelData { #[ValidatedRequestModelAttribute(['string'])] public string $street; #[ValidatedRequestModelAttribute(['nullable', 'int'])] public ?int $apartment = null; }
请求输入数据
{ "name": "John Doe", "addresses": [ { "street": "Sample Street" }, { "street": "Debugging Alley", "apartment": 43 } ] }
控制器
use App\Data\PersonData; use Illuminate\Http\Request; class TestController { public function index(Request $request) { $personData = PersonData::fromRequest($request); $person = $personData->toModel()->save(); foreach ($personData->addresses as $addressData) { // We assume the `Person` model has a has-many relation with the `Address` model $person->addresses()->save( $addressData->toModel() ); } return $person->id; } }
类型转换
类型转换将任何给定值转换为指定的类型。
内置类型转换
转换到日期
#[CastToDate]
属性将尊重您通过 Date::use(...)
定义的自定义日期类。您还可以通过传递日期类名称作为单个参数 #[CastToDate(MyDateClass::class)]
来指定要使用的自定义日期类。
use Carbon\Carbon; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\Casts\CastToDate; class PersonData extends AbstractModelData { #[CastToDate] public Carbon $date; }
自定义类型转换
您可以通过实现 CastInterface
接口并附加属性来声明自定义类型转换属性。
use Attribute; use romanzipp\LaravelDTO\Attributes\Casts\CastInterface; #[Attribute] class MyCast implements CastInterface { public function castToType(mixed $value): mixed { return (string) $value; } }
IDE 支持
请确保添加以下所示 @method
PHPDoc 注释,以允许 IDE 和静态分析器在调用 toModel()
方法时提供支持。
use App\Models\Person; use romanzipp\LaravelDTO\AbstractModelData; use romanzipp\LaravelDTO\Attributes\ForModel; use romanzipp\LaravelDTO\Attributes\ModelAttribute; /** * @method Person toModel() */ #[ForModel(Person::class)] class PersonData extends AbstractModelData { #[ModelAttribute] public string $name; }
测试
PHPUnit
./vendor/bin/phpunit
PHPStan
./vendor/bin/phpstan