romanzipp/laravel-dto 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

romanzipp/laravel-dto

Composer 安装命令:

composer require romanzipp/laravel-dto

包简介

A strongly typed Data Transfer Object integration for Laravel

README 文档

README

Latest Stable Version Total Downloads License GitHub Build Status

A strongly typed Data Transfer Object for Laravel without magic for PHP 8.0+

This package extends the functionality of romanzipp/DTO to provide more narrow usecases for Laravel applications.

Laravel-DTO serves as an intermediate and reusable layer between request input & validation and model attribute population.

Contents

Installation

composer require romanzipp/laravel-dto

Usage

All data objects must extend the romanzipp\LaravelDTO\AbstractModelData class.

Validation

When attaching the #[ValidationRule] any given data will be passed to the Laravel Validator so you can make use of all available validation rules and even built-in rules instances.

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;
}

This will throw a Illuminate\Validation\ValidationException if any rule does not pass.

$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'],
    ]
]);

Hydrate Models

You can attach a model to any DTO using the #[ForModel(Model::class)] attribute. To associate DTO properties with Model attributes, you need to attach the #[ModelAttribute()] attribute to each property. If no parameter is passed to the #[ModelAttribute] attribute, DTO uses the property name itself.

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();

Attributes saved in Person model

name current_age
John Doe 25

Note: You can also pass an existing model to the toModel() method.

use App\Models\Person;

$person = $data->toModel($person)->save();

Note: When passing no existing model to the toModel() method, default values declared in the DTO will be populated. If a model is passed as argument toModel($model) default values will not override existing model attributes.

Populate DTO from request input data

When attaching the #[RequestAttribute] and creating a DTO instance via the fromRequest(Request $request) method all matching attributes will be populated by the input data. If no parameter is passed to the #[RequestAttribute] attribute, DTO uses the property name itself.

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
}

The controller

use App\Data\PersonData;
use Illuminate\Http\Request;

class TestController
{
    public function store(Request $request)
    {
        $data = PersonData::fromRequest($request);
    }
}

Request input data

{
  "name": "John Doe",
  "my_age": 25,
  "language": "de"
}

The PersonData DTO instance

App\Data\PersonData^ {
  +name: "John Doe"
  +currentAge: 25
}

Combined usage

Of course all those attributes start to make sense if used together. You can attach all attributes separately of make use of the #[ValidatedRequestModelAttribute] attribute which combines the functionality of all #[RequestAttribute], #[ModelAttribute] and #[ValidationRule] attributes.

Both properties in the following example behave exactly the same. Use as you prefer.

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;
}

Request input data

{
  "my_age": 25
}

The controller

use App\Data\PersonData;
use Illuminate\Http\Request;

class TestController
{
    public function index(Request $request)
    {
        $person = PersonData::fromRequest($request)->toModel()->save();

        return $person->id;
    }
}

Validate arrays

If you only want to validate an array without casting the children items to another DTO, you can make use of the ValidationChildrenRule attribute.

The first parameter to the ValidationChildrenRule attribute is the validation rule for the children items. The second parameter is the validator path to access the children key to validate.

Validate a simple array with numeric indexes

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'
    ]
]);

Validate associative arrays with named keys

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']
    ]
]);

Multiple validation rules

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']
    ]
]);

Cast arrays to DTOs (Nested data)

In some cases you also want to create realted models with a single HTTP call. In this case you can make use of the #[NestedModelData(NestedData::class)] which will populate the DTO property with n instances of the defined DTO.

Note that we will not attach an #[ModelAttribute] attribute to the $address DTO property since it should not be set to a model attribute.

All attributes attached to the nested DTO will just work as expected.

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;
}

Request input data

{
  "name": "John Doe",
  "addresses": [
    {
      "street": "Sample Street"
    },
    {
      "street": "Debugging Alley",
      "apartment": 43
    }
  ]
}

The controller

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;
    }
}

Type Casting

Type casts will convert any given value to a specified type.

Built-in type casts

CastToDate

The #[CastToDate] attribute will respect your customly defined date class from Date::use(...). You can also specify a custom date class to be used by passing the date class name as single argument #[CastToDate(MyDateClass::class)].

use Carbon\Carbon;
use romanzipp\LaravelDTO\AbstractModelData;
use romanzipp\LaravelDTO\Attributes\Casts\CastToDate;

class PersonData extends AbstractModelData
{
    #[CastToDate]
    public Carbon $date;
}

Custom type casts

You can declare custom type cast attributes by simply implementing the CastInterface interface and attaching an attribute.

use Attribute;
use romanzipp\LaravelDTO\Attributes\Casts\CastInterface;

#[Attribute]
class MyCast implements CastInterface
{
    public function castToType(mixed $value): mixed
    {
        return (string) $value;
    }
}

IDE Support

Make sure to add a @method PHPDoc comment like shown below to allow IDE and static analyzer support when calling the toModel() method.

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;
}

Testing

PHPUnit

./vendor/bin/phpunit

PHPStan

./vendor/bin/phpstan

Authors

romanzipp/laravel-dto 适用场景与选型建议

romanzipp/laravel-dto 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 5.42k 次下载、GitHub Stars 达 11, 最近一次更新时间为 2022 年 04 月 06 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 romanzipp/laravel-dto 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 romanzipp/laravel-dto 我们能提供哪些服务?
定制开发 / 二次开发

基于 romanzipp/laravel-dto 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 5.42k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 11
  • 点击次数: 12
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 11
  • Watchers: 1
  • Forks: 2
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2022-04-06