dto应定义为纯php类,不继承任何基类或实现接口,属性设为public readonly(php 8.1+)或通过构造函数赋值,显式声明参数并做基础校验,禁止动态属性和批量赋值,从request提取时须用string()/integer()等安全方法强转类型。

DTO类该继承什么基类或实现什么接口
Laravel本身不强制DTO必须继承特定基类,也不提供官方DTO接口。硬套 Request 类或 FormRequest 是常见误区——它们本质是请求验证载体,不是数据传输容器,混用会导致职责混乱、测试困难、序列化异常。
推荐做法是定义纯PHP类,手动构造 + 类型声明 + 可选的简单验证逻辑:
- 不继承任何Laravel类,保持无框架依赖,方便单元测试和复用
- 所有属性设为
public readonly(PHP 8.1+)或public+ 构造函数赋值(兼容旧版) - 构造参数顺序建议与URL查询参数名一致,便于IDE自动补全和调用可读性
- 如需基础校验(如非空、范围),可在构造函数中抛出
InvalidArgumentException,而非依赖validate()
示例:
class UserListDTO
{
public function __construct(
public readonly string $search = '',
public readonly int $page = 1,
public readonly int $limit = 20,
public readonly ?string $sort = null,
) {
if ($this->page = 1');
}
if ($this->limit limit > 100) {
throw new InvalidArgumentException('limit must be between 1 and 100');
}
}
}
如何从 Request 实例安全提取并实例化DTO
不能直接用 $request->all() 丢给DTO构造函数——它会传入未过滤的全部输入(包括 _token、_method 等干扰字段),且类型不保证(所有值都是字符串)。
正确方式是显式取键、强转类型、提供默认值:
- 用
$request->string('key', 'default')、$request->integer('key', 1)等方法,比$request->input()更安全(自动类型转换 + 默认值) - 避免使用
$request->query()返回的原始数组,它不处理类型,也不触发Laravel的类型转换逻辑 - 若参数多,可封装一个静态工厂方法,把转换逻辑收拢,避免控制器里重复写
integer()调用
示例:
public function index(Request $request)
{
$dto = new UserListDTO(
search: $request->string('search', ''),
page: $request->integer('page', 1),
limit: $request->integer('limit', 20),
sort: $request->string('sort', null),
);
// ...
}
DTO要不要支持批量赋值或动态属性(如 __set)
不要。DTO的核心价值是结构清晰、意图明确、不可变(或至少显式可变)。加 __set、__get 或魔术方法会让它退化成“松散数组包装器”,失去类型提示、IDE跳转、静态分析等收益。
更严重的是:动态属性无法被PHPStan/psalm识别,会导致类型检查失效;运行时错误也更难定位(比如拼错字段名,不会报错,只是静默忽略)。
- 所有参数必须在构造函数中显式声明,哪怕默认值为
null - 如需“部分更新”语义(如PATCH场景),应另建专用DTO,而不是让GET DTO承担多种职责
- 若真有大量可选参数,考虑拆分为多个小DTO(如
UserFilterDTO+UserPaginationDTO),再组合使用
路由模型绑定或中间件里能提前解析DTO吗
不能直接在中间件或路由绑定中解析并注入DTO。Laravel的依赖注入容器不支持运行时根据 Request 上下文动态构造对象(除非你手写自定义解析器,但得不偿失)。
当前最轻量、最可控的方式仍是控制器方法参数中手动实例化。如果想进一步解耦,可用以下折中方案:
- 在控制器基类中提供
resolveDto(YourDTO::class)方法,内部统一做new YourDTO(...)和类型转换 - 用 Laravel 的
Route::bind()绑定只适用于Eloquent模型,对DTO无效;强行模拟会破坏请求生命周期,且无法访问Request实例 - 不要为了“看起来更优雅”而引入服务容器扩展或宏注册——DTO本就该是薄层,过度设计反而增加维护成本
真正容易被忽略的一点:GET参数的编码问题(如空格变成 +、中文被urlencode)。Laravel的 $request->string() 等方法已自动解码,但如果你绕过它直接操作 $_GET 或 $request->query(),就得自己调用 urldecode(),否则字段值可能损坏。











