bssphp / laraveldto
Laravel 的强类型数据传输对象集成
Requires
- php: ^8.0
- ext-json: *
- bssphp/dto: ^1.4
Requires (Dev)
- bssphp/php-cs-fixer-config: ^1.2
- friendsofphp/php-cs-fixer: ^3.0
- laravel/framework: ^9.7
- orchestra/testbench: ^3.8|^4.0|^5.0|^6.0|^7.0
- phpstan/phpstan: ^0.12.99|^1.0
- phpunit/phpunit: ^9.0
README
为 Laravel 提供的强类型数据传输对象 (没有 PHP 8.0+ 的魔法)
此包扩展了 bssphp/DTO 的功能,为 Laravel 应用提供了更具体的用例。
Laravel-DTO 作为请求输入与验证以及模型属性填充之间的 中间和可重用层。
内容
安装
composer require bssphp/laraveldto
用法
所有数据对象都必须扩展 bssphp\laraveldto\AbstractModelData
类。
验证
当附加 #[ValidationRule]
时,任何给定数据都将传递给 Laravel 验证器,这样您就可以使用所有 可用的验证规则 以及内置规则实例。
use App\Models\Person; use App\Models\Project; use Illuminate\Validation\Rules\Exists; use bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\ForModel; use bssphp\laraveldto\Attributes\ValidationRule; 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; }
如果任何规则未通过,这将抛出 Illuminate\Validation\ValidationException
。
$data = new PersonData([ 'name' => 'John Doe', 'currentAge' => 25, 'language' => 'de', 'projectId' => 2, ]);
填充模型
您可以使用 #[ForModel(Model::class)]
属性将模型附加到任何 DTO。要将 DTO 属性与模型属性关联,您需要将 #[ModelAttribute()]
属性附加到每个属性。如果不向 #[ModelAttribute]
属性传递参数,DTO 将使用属性名称本身。
use App\Models\Person; use bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\ForModel; use bssphp\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 }
创建 DTO 并存储到模型
$data = new PersonData([ 'name' => 'John Doe', 'currentAge' => 25, 'language' => 'de', ]); $person = $data->toModel()->save();
Person
模型中保存的属性
{ "name": "John Doe", "current_age": 25 }
注意:您也可以将现有模型传递给 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 bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\ForModel; use bssphp\laraveldto\Attributes\ModelAttribute; use bssphp\laraveldto\Attributes\RequestAttribute; #[ForModel(Person::class)] class PersonData extends AbstractModelData { #[RequestAttribute] // The `$name` DTO property will de 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 bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\ForModel; use bssphp\laraveldto\Attributes\ModelAttribute; use bssphp\laraveldto\Attributes\RequestAttribute; use bssphp\laraveldto\Attributes\ValidatedRequestModelAttribute; use bssphp\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; // Combined usage // The `my_age` request attribute will be validated and set to the `current_age` model attribute. #[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; } }
嵌套数据
在某些情况下,您可能还希望使用单个HTTP调用创建相关模型。在这种情况下,您可以使用 #[NestedModelData(NestedData::class)]
,这将用定义的DTO的n个实例填充DTO属性。
请注意,由于不应将其设置为模型属性,因此不会将 #[ModelAttribute]
属性附加到 $address
DTO属性。
附加到嵌套DTO的所有属性将按预期正常工作。
use App\Models\Person; use bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\ForModel; use bssphp\laraveldto\Attributes\NestedModelData; use bssphp\laraveldto\Attributes\RequestAttribute; use bssphp\laraveldto\Attributes\ValidatedRequestModelAttribute; use bssphp\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 bssphp\laraveldto\AbstractModelData; use bssphp\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
#[CastToDate]
属性将尊重您从 Date::use(...)
定义的定制日期类。您还可以通过传递日期类名称作为单个参数 #[CastToDate(MyDateClass::class)]
来指定要使用的自定义日期类。
use Carbon\Carbon; use bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\Casts\CastToDate; class PersonData extends AbstractModelData { #[CastToDate] public Carbon $date; }
自定义类型转换
您可以通过简单地实现 CastInterface
接口并附加一个属性来声明自定义类型转换属性。
use Attribute; use bssphp\laraveldto\Attributes\Casts\CastInterface; #[Attribute] class MyCast implements CastInterface { public function castToType(mixed $value): mixed { return (string) $value; } }
最佳实践
请确保添加如以下所示的 @method
PHPDoc注释,以便在调用 toModel()
方法时允许IDE和静态分析器支持。
use App\Models\Person; use bssphp\laraveldto\AbstractModelData; use bssphp\laraveldto\Attributes\ForModel; use bssphp\laraveldto\Attributes\ModelAttribute; /** * @method Person toModel() */ #[ForModel(Person::class)] class PersonData extends AbstractModelData { #[ModelAttribute] public string $name; }
待办事项
- 允许数组验证规则
field.*
并映射到嵌套DTO - 从嵌套字段添加正确的验证异常错误消息
- 传递现有模型到
toModel()
方法 - 从现有模型创建DTO
- 只有当请求提供数据时才运行验证规则
测试
PHPUnit
./vendor/bin/phpunit
PHPStan
./vendor/bin/phpstan