laravel-dtos
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLaravel DTOs
Laravel DTOs
Never pass multiple primitive values. Always wrap data in Data objects.
Related guides:
- dto-transformers.md - Transform external data into DTOs
- test-factories.md - Create hydrated DTOs for tests
永远不要传递多个原始值,始终将数据封装在数据对象中。
相关指南:
- dto-transformers.md - 将外部数据转换为DTO
- test-factories.md - 为测试创建已填充数据的DTO
Philosophy
设计理念
DTOs provide:
- Type safety and IDE autocomplete
- Clear contracts between layers
- Test factories for easy test data generation
- Validation integration
- Transformation from requests to domain objects
DTO 具备以下优势:
- 类型安全与IDE自动补全
- 清晰的层间契约
- 测试工厂,轻松生成测试数据
- 验证集成
- 从请求到领域对象的转换能力
Spatie Laravel Data Package
Spatie Laravel Data 包
Uses Spatie Laravel Data. Refer to official docs for package features. This guide covers project-specific patterns and preferences.
使用 Spatie Laravel Data。包的功能请参考官方文档,本指南涵盖项目特定的模式与偏好。
Use ::from()
with Arrays
::from()结合数组使用 ::from()
::from()Always prefer with arrays where keys match constructor property names. Let the package handle casting based on property types.
::from()php
// ✅ 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')),
);Why prefer :
::from()- Package handles type casting automatically based on constructor property types
- Cleaner calling code without manual casting
- Consistent transformation behavior
- Leverages the full power of the package
When is acceptable:
new- In test factories where you control all values
- When values are already the correct type
- In formatters inside the DTO constructor
始终优先使用 配合与构造函数属性名匹配的数组。让包根据属性类型自动处理类型转换。
::from()php
// ✅ 推荐 - 让包自动处理类型转换
$data = CreateOrderData::from([
'customerEmail' => $request->input('customer_email'),
'deliveryDate' => $request->input('delivery_date'), // 字符串 → CarbonImmutable
'status' => $request->input('status'), // 字符串 → OrderStatus 枚举
'items' => $request->collect('items'), // 数组 → Collection<OrderItemData>
]);
// ❌ 避免 - 在调用代码中手动转换
$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构造函数内的格式化器中
Avoid Case Mapper Attributes
避免使用大小写映射器属性
Don't use or case mapper attributes. Map field names explicitly in calling code.
#[MapInputName]php
// ❌ 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'),
]);Why avoid case mappers:
- Explicit mapping is clearer and more maintainable
- Different API versions may have different field names
- Transformers provide a single place to see all mappings
- Avoids magic behavior that's hard to trace
不要使用 或大小写映射器属性,在调用代码中显式映射字段名。
#[MapInputName]php
// ❌ 避免 - 在类上使用大小写映射器属性
#[MapInputName(SnakeCaseMapper::class)]
class CreateOrderData extends Data
{
public function __construct(
public string $customerEmail, // 自动映射自 'customer_email'
) {}
}
// ✅ 推荐 - 在调用代码中显式映射
CreateOrderData::from([
'customerEmail' => $request->input('customer_email'),
]);避免大小写映射器的原因:
- 显式映射更清晰、更易于维护
- 不同API版本可能有不同的字段名
- 转换器提供了查看所有映射的统一位置
- 避免难以追踪的“魔法”行为
Date Casting is Automatic
日期转换自动完成
The package automatically casts date strings to or based on property types. Configure the expected date format in the package config.
CarbonCarbonImmutablephp
// config/data.php
return [
'date_format' => 'Y-m-d H:i:s', // Or ISO 8601: 'Y-m-d\TH:i:s.u\Z'
];php
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,
]);包会根据属性类型自动将日期字符串转换为 或 实例。可在包配置文件中设置预期的日期格式。
CarbonCarbonImmutablephp
// config/data.php
return [
'date_format' => 'Y-m-d H:i:s', // 或 ISO 8601: 'Y-m-d\TH:i:s.u\Z'
];php
class OrderData extends Data
{
public function __construct(
public CarbonImmutable $createdAt, // 自动从字符串转换
public ?CarbonImmutable $shippedAt, // 可空日期同样支持
) {}
}
// ✅ 直接传入字符串即可 - 包会处理转换
$data = OrderData::from([
'createdAt' => '2024-01-15 10:30:00',
'shippedAt' => null,
]);Basic Structure
基础结构
php
<?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);
}
}php
<?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,
) {
// 在构造函数中应用格式化器
$this->customerEmail = EmailFormatter::format($this->customerEmail);
}
}Key Patterns
核心模式
Constructor Property Promotion
构造函数属性提升
Always use promoted properties:
php
public function __construct(
public string $name,
public ?string $description,
public bool $active = true,
) {}Not:
php
public string $name;
public ?string $description;
public function __construct(string $name, ?string $description)
{
$this->name = $name;
$this->description = $description;
}始终使用属性提升语法:
php
public function __construct(
public string $name,
public ?string $description,
public bool $active = true,
) {}不要这样写:
php
public string $name;
public ?string $description;
public function __construct(string $name, ?string $description)
{
$this->name = $name;
$this->description = $description;
}Type Everything
为所有属性声明类型
php
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 DTOphp
public string $email; // 必填字符串
public ?string $phone; // 可空字符串
public CarbonImmutable $createdAt; // 不可变日期时间
public OrderStatus $status; // 枚举类型
public Collection $items; // 集合
public AddressData $address; // 嵌套DTOCollections with PHPDoc
带PHPDoc注释的集合
php
/** @var int[] */
public array $productIds;
/** @var Collection<int, OrderItemData> */
public Collection $items;php
/** @var int[] */
public array $productIds;
/** @var Collection<int, OrderItemData> */
public Collection $items;Nested Data Objects
嵌套数据对象
php
class OrderData extends Data
{
public function __construct(
public CustomerData $customer,
public ShippingAddressData $shipping,
public BillingAddressData $billing,
/** @var Collection<int, OrderItemData> */
public Collection $items,
) {}
}php
class OrderData extends Data
{
public function __construct(
public CustomerData $customer,
public ShippingAddressData $shipping,
public BillingAddressData $billing,
/** @var Collection<int, OrderItemData> */
public Collection $items,
) {}
}Formatters
格式化器
Apply formatting in the constructor:
php
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;
}Example formatter ():
app/Data/Formatters/EmailFormatter.phpphp
<?php
declare(strict_types=1);
namespace App\Data\Formatters;
class EmailFormatter
{
public static function format(string $email): string
{
return strtolower(trim($email));
}
}在构造函数中应用格式化逻辑:
php
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.phpphp
<?php
declare(strict_types=1);
namespace App\Data\Formatters;
class EmailFormatter
{
public static function format(string $email): string
{
return strtolower(trim($email));
}
}Static Factory Methods on DTOs
DTO上的静态工厂方法
For smaller applications or when starting out, add static methods directly on the DTO class. This provides factory-like behavior before complexity warrants separate transformers.
from*Method naming: - e.g., , ,
from{SourceType}fromArrayfromRequestfromModelphp
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(),
]);
}
}When to use static methods on DTO:
- Smaller applications with fewer DTOs
- Simple transformations that don't need dedicated testing
- When mapping is tightly coupled to a single DTO
When to use separate transformers:
- Multiple external sources map to the same DTO
- Complex transformation logic requiring extensive testing
- Larger applications with clear separation of concerns
对于小型应用或项目初期,可在DTO类上直接添加静态 方法。在复杂度需要独立转换器之前,这种方式能提供类似工厂的行为。
from*方法命名规则: - 例如 、、
from{SourceType}fromArrayfromRequestfromModelphp
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时
- 复杂转换逻辑需要大量测试时
- 关注点分离清晰的大型应用
Model Casts
模型转换
Cast model JSON columns to DTOs:
php
class Order extends Model
{
protected function casts(): array
{
return [
'metadata' => OrderMetadataData::class,
'status' => OrderStatus::class,
];
}
}Usage:
php
// Store
$order = Order::create([
'metadata' => $metadataData, // OrderMetadataData instance
]);
// Retrieve
$metadata = $order->metadata; // Returns OrderMetadataData instance将模型的JSON列转换为DTO:
php
class Order extends Model
{
protected function casts(): array
{
return [
'metadata' => OrderMetadataData::class,
'status' => OrderStatus::class,
];
}
}使用方式:
php
// 存储
$order = Order::create([
'metadata' => $metadataData, // OrderMetadataData实例
]);
// 读取
$metadata = $order->metadata; // 返回OrderMetadataData实例Naming Conventions
命名规范
| Type | Pattern | Examples |
|---|---|---|
| Response DTOs | | |
| Request DTOs | | |
| Nested DTOs | | |
| 类型 | 命名模式 | 示例 |
|---|---|---|
| 响应DTO | | |
| 请求DTO | | |
| 嵌套DTO | | |
Directory Structure
目录结构
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.phpapp/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.phpUsage in Controllers
在控制器中的使用
Controllers transform requests to DTOs via transformers:
php
<?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);
}
}控制器通过转换器将请求转换为DTO:
php
<?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);
}
}Usage in Actions
在动作类中的使用
Actions accept DTOs as parameters:
php
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,
]);
});
}
}动作类接受DTO作为参数:
php
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,
]);
});
}
}Transformers
转换器
For complex transformations (external APIs, webhooks, field mappings), use dedicated transformer classes.
→ Complete guide: dto-transformers.md
php
// External system data
$data = PaymentDataTransformer::fromStripePaymentIntent($webhook['data']);
// Request with version-specific field names
$data = OrderDataTransformer::fromRequest($request);Hierarchy of preference:
- - Simple cases, direct mapping
Data::from($array) - - Static method on DTO for smaller apps
Data::fromRequest() - - Complex transformations, multiple sources
Transformer::from*()
对于复杂的转换场景(外部API、Webhook、字段映射),请使用独立的转换器类。
→ 完整指南:dto-transformers.md
php
// 外部系统数据转换
$data = PaymentDataTransformer::fromStripePaymentIntent($webhook['data']);
// 带版本特定字段名的请求转换
$data = OrderDataTransformer::fromRequest($request);优先级层级:
- - 简单场景,直接映射
Data::from($array) - - 小型应用中使用DTO上的静态方法
Data::fromRequest() - - 复杂转换、多数据源场景
Transformer::from*()
Test Factories
测试工厂
Create hydrated DTOs for tests using the trait.
HasTestFactory→ Complete guide: test-factories.md
Link DTOs to factories with PHPDoc:
php
/**
* @see \Database\Factories\Data\CreateOrderDataFactory
* @method static CreateOrderDataFactory testFactory()
*/
class CreateOrderData extends Data
{
// ...
}Usage:
php
$data = CreateOrderData::testFactory()->make();
$collection = OrderItemData::testFactory()->collect(count: 5);
// With overrides
$data = CreateOrderData::testFactory()->make([
'customerEmail' => 'test@example.com',
]);使用 trait为测试创建已填充数据的DTO。
HasTestFactory→ 完整指南:test-factories.md
通过PHPDoc将DTO与工厂关联:
php
/**
* @see \Database\Factories\Data\CreateOrderDataFactory
* @method static CreateOrderDataFactory testFactory()
*/
class CreateOrderData extends Data
{
// ...
}使用方式:
php
$data = CreateOrderData::testFactory()->make();
$collection = OrderItemData::testFactory()->collect(count: 5);
// 覆盖默认值
$data = CreateOrderData::testFactory()->make([
'customerEmail' => 'test@example.com',
]);Testing DTOs
DTO测试
php
use App\Data\CreateOrderData;
it('can create DTO from array', function () {
$data = CreateOrderData::from([
'customerEmail' => 'test@example.com',
'notes' => 'Test notes',
'status' => 'pending',
]);
expect($data)
->customerEmail->toBe('test@example.com')
->notes->toBe('Test notes');
});
it('formats email in constructor', function () {
$data = new CreateOrderData(
customerEmail: ' TEST@EXAMPLE.COM ',
notes: null,
status: OrderStatus::Pending,
);
expect($data->customerEmail)->toBe('test@example.com');
});php
use App\Data\CreateOrderData;
it('can create DTO from array', function () {
$data = CreateOrderData::from([
'customerEmail' => 'test@example.com',
'notes' => 'Test notes',
'status' => 'pending',
]);
expect($data)
->customerEmail->toBe('test@example.com')
->notes->toBe('Test notes');
});
it('formats email in constructor', function () {
$data = new CreateOrderData(
customerEmail: ' TEST@EXAMPLE.COM ',
notes: null,
status: OrderStatus::Pending,
);
expect($data->customerEmail)->toBe('test@example.com');
});