本文目录导读:

在PHP项目中,数据传输对象(DTO) 和视图模型(ViewModel) 都是用于在不同层之间传递数据的对象,但它们的职责和使用场景有明显区别,下面详细说明。
核心概念对比
| 特性 | DTO | ViewModel |
|---|---|---|
| 主要目的 | 在应用层之间(如Service与Controller)传输数据 | 为特定视图(页面、API响应)准备数据 |
| 关注点 | 数据完整性、类型安全、无业务逻辑 | 展示逻辑、格式化、聚合多个数据源 |
| 是否序列化 | 通常序列化为JSON/XML传输 | 直接传递给模板引擎或序列化为API响应 |
| 生命周期 | 仅存在于传输过程中 | 存在于视图渲染过程中 |
| 原始数据、数据库字段映射 | 格式化后数据、计算字段、UI状态标记 |
数据传输对象(DTO)
典型用途
- API请求/响应结构
- Service层与Controller层之间的数据交换
- 远程调用(RPC、gRPC)的数据封装
示例代码
<?php
// 1. 使用只读属性(PHP 8.2+)
class CreateUserDTO
{
public function __construct(
public readonly string $name,
public readonly string $email,
public readonly ?string $phone = null,
) {}
// 可选:从请求数据创建
public static function fromRequest(array $data): self
{
return new self(
name: $data['name'],
email: $data['email'],
phone: $data['phone'] ?? null,
);
}
}
// 2. 使用后赋值模式(兼容旧版本)
class OrderDTO
{
public string $orderId;
public float $total;
public array $items;
public function __construct(array $data = [])
{
foreach ($data as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
public function toArray(): array
{
return get_object_vars($this);
}
}
// 使用示例
$dto = new CreateUserDTO(
name: '张三',
email: 'zhangsan@example.com'
);
// Controller -> Service
$userService->createUser($dto);
关键设计原则
- 不可变性:创建后不修改(
readonly) - 无业务逻辑:只做数据验证(如类型检查)
- 明确的数据契约:每个属性都有类型声明
- 序列化友好:能轻松转为JSON或数组
视图模型(ViewModel)
典型用途
- Blade/Twig模板渲染的数据
- API响应的特定视图(如用户详情页包含积分信息)
- 聚合多个实体数据并格式化
示例代码
<?php
// 用户详情页面视图模型
class UserProfileViewModel
{
public string $displayName;
public string $formattedJoinDate;
public string $avatarUrl;
public int $postCount;
public ?string $subscriptionStatus;
// 不直接暴露实体对象
private User $user;
public function __construct(User $user, PostRepository $postRepo)
{
$this->user = $user;
// 聚合与格式化
$this->displayName = $user->getFullName() ?: $user->username;
$this->formattedJoinDate = $user->createdAt->format('Y年m月d日');
$this->avatarUrl = $this->buildAvatarUrl($user);
$this->postCount = $postRepo->countByUserId($user->id);
$this->subscriptionStatus = $this->determineStatus($user);
}
private function buildAvatarUrl(User $user): string
{
return $user->avatar
? asset('storage/avatars/' . $user->avatar)
: 'https://www.gravatar.com/avatar/' . md5($user->email);
}
private function determineStatus(User $user): ?string
{
if ($user->isExpired()) return '已过期';
if ($user->isTrial()) return '试用中';
return $user->isPremium() ? '会员' : null;
}
}
// 控制器中使用
class UserController
{
public function show(int $userId): View
{
$user = $this->userRepo->find($userId);
$viewModel = new UserProfileViewModel($user, $this->postRepo);
return view('user.profile', [
'model' => $viewModel // 直接传递ViewModel
]);
}
}
API响应场景的ViewModel
<?php
class OrderDetailViewModel
{
public function __construct(
public readonly string $orderNumber,
public readonly string $statusLabel,
public readonly array $items,
public readonly float $totalWithTax,
public readonly string $estimatedDelivery,
) {}
// 工厂方法从订单实体创建
public static function fromOrder(Order $order): self
{
$items = array_map(
fn(OrderItem $item) => [
'name' => $item->productName,
'quantity' => $item->quantity,
'price' => number_format($item->price, 2),
],
$order->items
);
return new self(
orderNumber: 'ORD-' . $order->id,
statusLabel: OrderStatusEnum::getLabel($order->status),
items: $items,
totalWithTax: $order->total * 1.1,
estimatedDelivery: $order->shippedAt?->addDays(3)->format('Y-m-d') ?? '待确认',
);
}
}
关键区别与选择指南
区别列表
-
数据来源:
- DTO: 通常单个实体或表单输入
- ViewModel: 可能聚合多个实体(User + Post + Setting)
-
数据处理:
- DTO: 保持原始数据格式
- ViewModel: 格式化(日期、货币、状态标签)
-
业务规则:
- DTO: 无业务规则
- ViewModel: 包含展示逻辑(如"是否显示VIP标记")
-
生命周期:
- DTO: 短暂存在,通常被序列化后立即废弃
- ViewModel: 生存到响应完成
选择建议
是否是跨层数据传输? → DTO
是否是为特定视图定制? → ViewModel
是否包含格式化/计算字段? → ViewModel
是否直接对应数据库表结构? → DTO
是否减少对实体的直接依赖? → 两者都可以(但ViewModel更常见)
混合使用模式
在大型项目中,常常组合使用:
<?php
// Service返回DTO
class UserService
{
public function getUser(int $id): UserDTO
{
$user = $this->repo->find($id);
return new UserDTO(
id: $user->id,
email: $user->email,
// ...
);
}
}
// Controller将DTO转为ViewModel
class UserController
{
public function show(int $id): View
{
$dto = $this->userService->getUser($id);
$viewModel = new UserProfileViewModel(
$dto,
$this->postService->getUserPosts($id)
);
return view('user.profile', $viewModel);
}
}
性能注意事项
| 场景 | 建议 |
|---|---|
| 高并发API | 直接使用DTO+JSON序列化,避免ViewModel额外开销 |
| 复杂模板渲染 | ViewModel可缓存计算结果(如权限标记) |
| 大型集合 | 使用Generator或分页,避免一次性加载所有数据到ViewModel |
| 内存敏感 | DTO使用stdClass或array(不推荐,会失去类型安全) |
- DTO = 数据管道,关注 传输(输入/输出验证、类型安全)
- ViewModel = 视图助手,关注 展示(格式化、聚合、减少模板逻辑)
- 最佳实践:Controller层接收DTO → 转换为ViewModel → 传递给视图
在小型项目中可以合并使用,但在现代PHP(特别是DDD或清晰架构)中,明确区分两者能显著提升代码的可维护性和可测试性。