mcpField
L
laravel-dtos

laravel-dtos

github.com/leeovery开发4338 次安装31 周前更新
代码仓库

关于 laravel-dtos

laravel-dtos 是面向 Laravel 项目的 DTO 开发技能,基于 Spatie Laravel Data 总结数据对象的组织方式。内容涵盖类型声明、显式字段映射、复杂数据转换、模型类型转换和测试工厂。

主要功能

  1. 01类型化属性与构造器属性提升
  2. 02使用 `::from()` 转换数组
  3. 03显式字段映射规范
  4. 04复杂输入的 DTO 转换器
  5. 05模型类型转换与测试工厂

适用场景

  1. 01将请求数据转换为 DTO
  2. 02映射外部 API 或 webhook 数据
  3. 03用类型表示嵌套领域数据
  4. 04为测试创建已填充的 DTO

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/leeovery/claude-laravel --skill laravel-dtos

安装后在 Claude Code 中输入/laravel-dtos确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
AddressDataFactory.php
AppServiceProvider.php
Data.php
DataTestFactory.php
EmailFormatter.php
HasTestFactory.php
TraceDataFactory.php
dto-transformers.md
helpers.php
test-factories.md

详细介绍

Laravel DTOs

切勿传递多个基本类型值。 始终将数据封装在 Data 对象中。

相关指南:

理念

DTO 提供:

  • 类型安全和 IDE 自动补全
  • 各层之间的清晰契约
  • 用于轻松生成测试数据的测试工厂
  • 验证集成
  • 将请求转换为领域对象

Spatie Laravel Data 包

使用 Spatie Laravel Data。有关包功能,请参阅官方文档。本指南介绍项目特定的模式和偏好。

使用 ::from() 处理数组

对于键名与构造函数属性名称匹配的数组,始终优先使用 ::from()。 让包根据属性类型处理类型转换。

// ✅ PREFERRED - Let package cast automatically
$data = CreateOrderData::from([
    'customerEmail' => $request->input('customer_email'),
    'deliveryDate' => $request->input('delivery_date'),  // String → CarbonImmutable
    'status' => $request->input('status'),               // String → OrderStatus enum
    'items' => $request->collect('items'),               // Array → Collection<OrderItemData>
]);

// ❌ AVOID - Manual casting in calling code
$data = new CreateOrderData(
    customerEmail: $request->input('customer_email'),
    deliveryDate: CarbonImmutable::parse($request->input('delivery_date')),
    status: OrderStatus::from($request->input('status')),
    items: OrderItemData::collect($request->input('items')),
);

优先使用 ::from() 的原因:

  • 包会根据构造函数属性类型自动进行类型转换
  • 调用代码更简洁,无需手动转换类型
  • 转换行为保持一致
  • 充分利用包的全部功能

可以使用 new 的情况:

  • 在测试工厂中,你可以控制所有值
  • 值已经是正确的类型
  • 在 DTO 构造函数内的格式化器中
避免使用大小写映射属性

不要使用 #[MapInputName] 或大小写映射属性。 在调用代码中显式映射字段名称。

// ❌ AVOID - Case mapper attributes on the class
#[MapInputName(SnakeCaseMapper::class)]
class CreateOrderData extends Data
{
    public function __construct(
        public string $customerEmail,    // Auto-maps from 'customer_email'
    ) {}
}

// ✅ PREFERRED - Explicit mapping in calling code
CreateOrderData::from([
    'customerEmail' => $request->input('customer_email'),
]);

避免使用大小写映射器的原因:

  • 显式映射更清晰、更易维护
  • 不同的 API 版本可能使用不同的字段名称
  • 转换器提供了一个集中查看所有映射的位置
  • 避免难以追踪的魔法行为
日期会自动转换类型

包会根据属性类型自动将日期字符串转换为 Carbon 或 CarbonImmutable。在包配置中设置预期的日期格式。

// config/data.php
return [
    'date_format' => 'Y-m-d H:i:s',  // Or ISO 8601: 'Y-m-d\TH:i:s.u\Z'
];
class OrderData extends Data
{
    public function __construct(
        public CarbonImmutable $createdAt,   // Automatically cast from string
        public ?CarbonImmutable $shippedAt,  // Nullable dates work too
    ) {}
}

// ✅ Just pass the string - package handles casting
$data = OrderData::from([
    'createdAt' => '2024-01-15 10:30:00',
    'shippedAt' => null,
]);

基本结构

<?php

declare(strict_types=1);

namespace App\Data;

use App\Enums\OrderStatus;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;

/**
 * @see \Database\Factories\Data\CreateOrderDataFactory
 */
class CreateOrderData extends Data
{
    public function __construct(
        public string $customerEmail,
        public ?string $notes,
        public ?CarbonImmutable $deliveryDate,
        public OrderStatus $status,
        /** @var Collection<int, OrderItemData> */
        public Collection $items,
        public ShippingAddressData $shippingAddress,
        public BillingAddressData $billingAddress,
    ) {
        // Apply formatters in constructor
        $this->customerEmail = EmailFormatter::format($this->customerEmail);
    }
}

关键模式

构造函数属性提升

始终使用提升属性:

public function __construct(
    public string $name,
    public ?string $description,
    public bool $active = true,
) {}

不要这样:

public string $name;
public ?string $description;

public function __construct(string $name, ?string $description)
{
    $this->name = $name;
    $this->description = $description;
}
为所有内容指定类型
public string $email;                          // Required string
public ?string $phone;                         // Nullable string
public CarbonImmutable $createdAt;             // DateTime (immutable)
public OrderStatus $status;                    // Enum
public Collection $items;                      // Collection
public AddressData $address;                   // Nested DTO
使用 PHPDoc 标注集合
/** @var int[] */
public array $productIds;

/** @var Collection<int, OrderItemData> */
public Collection $items;
嵌套 Data 对象
class OrderData extends Data
{
    public function __construct(
        public CustomerData $customer,
        public ShippingAddressData $shipping,
        public BillingAddressData $billing,
        /** @var Collection<int, OrderItemData> */
        public Collection $items,
    ) {}
}
格式化器

在构造函数中应用格式化:

public function __construct(
    public string $email,
    public ?string $phone,
    public ?string $postcode,
) {
    $this->email = EmailFormatter::format($this->email);
    $this->phone = $this->phone ? PhoneFormatter::format($this->phone) : null;
    $this->postcode = $this->postcode ? PostcodeFormatter::format($this->postcode) : null;
}

格式化器示例(app/Data/Formatters/EmailFormatter.php):

<?php

declare(strict_types=1);

namespace App\Data\Formatters;

class EmailFormatter
{
    public static function format(string $email): string
    {
        return strtolower(trim($email));
    }
}
DTO 上的静态工厂方法

对于较小的应用或刚开始时,可以直接在 DTO 类上添加静态 from* 方法。在复杂度尚未达到需要单独转换器的程度之前,这可以提供类似工厂的行为。

方法命名: from{SourceType} - 例如,fromArray、fromRequest、fromModel

class OrderData extends Data
{
    public function __construct(
        public string $customerEmail,
        public ?string $notes,
        public OrderStatus $status,
        /** @var Collection<int, OrderItemData> */
        public Collection $items,
    ) {}

    public static function fromRequest(CreateOrderRequest $request): self
    {
        return self::from([
            'customerEmail' => $request->input('customer_email'),
            'notes' => $request->input('notes'),
            'status' => $request->input('status'),
            'items' => $request->input('items'),
        ]);
    }

    public static function fromModel(Order $order): self
    {
        return self::from([
            'customerEmail' => $order->customer_email,
            'notes' => $order->notes,
            'status' => $order->status,
            'items' => $order->items->toArray(),
        ]);
    }
}

适合在 DTO 上使用静态方法的情况:

  • DTO 数量较少的小型应用
  • 无需专门测试的简单转换
  • 映射与单个 DTO 紧密耦合

适合使用单独转换器的情况:

  • 多个外部来源映射到同一个 DTO
  • 需要大量测试的复杂转换逻辑
  • 关注点分离明确的大型应用
模型转换

将模型 JSON 列转换为 DTO:

class Order extends Model
{
    protected function casts(): array
    {
        return [
            'metadata' => OrderMetadataData::class,
            'status' => OrderStatus::class,
        ];
    }
}

用法:

// Store
$order = Order::create([
    'metadata' => $metadataData,  // OrderMetadataData instance
]);

// Retrieve
$metadata = $order->metadata;  // Returns OrderMetadataData instance

命名约定

类型模式示例
响应 DTO{Entity}DataOrderData、UserData、ProductData
请求 DTO{Action}{Entity}DataCreateOrderData、UpdateUserData
嵌套 DTO{Descriptor}{Entity}DataShippingAddressData、OrderMetadataData

目录结构

app/Data/
├── CreateOrderData.php
├── UpdateOrderData.php
├── OrderData.php
├── Concerns/
│   └── HasTestFactory.php
├── Formatters/
│   ├── EmailFormatter.php
│   ├── PhoneFormatter.php
│   └── PostcodeFormatter.php
└── Transformers/
    ├── PaymentDataTransformer.php
    ├── Web/
    │   └── OrderDataTransformer.php
    └── Api/
        └── V1/
            └── OrderDataTransformer.php

在控制器中使用

控制器通过转换器将请求转换为 DTO:

<?php

declare(strict_types=1);

namespace App\Http\Web\Controllers;

use App\Actions\Order\CreateOrderAction;
use App\Data\Transformers\Web\OrderDataTransformer;
use App\Http\Web\Requests\CreateOrderRequest;
use App\Http\Web\Resources\OrderResource;

class OrderController extends Controller
{
    public function store(
        CreateOrderRequest $request,
        CreateOrderAction $action
    ): OrderResource {
        $order = $action(
            user(),
            OrderDataTransformer::fromRequest($request)
        );

        return OrderResource::make($order);
    }
}

在 Action 中使用

Action 接受 DTO 作为参数:

class CreateOrderAction
{
    public function __invoke(User $user, CreateOrderData $data): Order
    {
        return DB::transaction(function () use ($user, $data) {
            return $user->orders()->create([
                'customer_email' => $data->customerEmail,
                'notes' => $data->notes,
                'status' => $data->status,
            ]);
        });
    }
}

转换器

对于复杂的转换(外部 API、Webhook、字段映射),请使用专用的转换器类。

→ 完整指南:dto-transformers.md

// External system data
$data = PaymentDataTransformer::fromStripePaymentIntent($webhook['data']);

// Request with version-specific field names
$data = OrderDataTransformer::fromRequest($request);

优先级顺序:

  1. Data::from($array) - 简单场景,直接映射
  2. Data::fromRequest() - DTO 上的静态方法,适用于较小的应用
  3. Transformer::from*() - 复杂转换,涉及多个来源

测试工厂

使用 HasTestFactory trait 创建已填充数据的 DTO 以用于测试。

→ 完整指南:test-factories.md

使用 PHPDoc 将 DTO 关联到工厂:

/**
 * @see \Database\Factories\Data\CreateOrderDataFactory
 * @method static CreateOrderDataFactory testFactory()
 */
class CreateOrderData extends Data
{
    // ...
}

用法:

$data = CreateOrderData::testFactory()->make();
$collection = OrderItemData::testFactory()->collect(count: 5);

// With overrides
$data = CreateOrderData::testFactory()->make([
    'customerEmail' => '[email protected]',
]);

测试 DTO

use App\Data\CreateOrderData;

it('can create DTO from array', function () {
    $data = CreateOrderData::from([
        'customerEmail' => '[email protected]',
        'notes' => 'Test notes',
        'status' => 'pending',
    ]);

    expect($data)
        ->customerEmail->toBe('[email protected]')
        ->notes->toBe('Test notes');
});

it('formats email in constructor', function () {
    $data = new CreateOrderData(
        customerEmail: '  [email protected]  ',
        notes: null,
        status: OrderStatus::Pending,
    );

    expect($data->customerEmail)->toBe('[email protected]');
});

评论

支持 Markdown