romanzipp/laravel-dto

Laravel 的强类型数据传输对象集成

1.0.0 2024-03-05 08:33 UTC

README

Latest Stable Version Total Downloads License GitHub Build Status

为 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

作者