预处理逻辑应放在 formrequest 的 prepareforvalidation() 中,因其专属于具体业务场景,能安全执行 trim、转小写、手机号标准化等操作,确保验证、存储与 old() 回显数据一致,且不违反单一职责原则。

预处理逻辑该放 Form Request 里,而不是中间件或控制器中。 这不是风格偏好,而是职责边界问题:中间件管“能不能进”,控制器管“怎么处理业务”,而数据预处理——比如 trim、转小写、过滤空格、标准化手机号、补默认值——属于“进门前把数据理干净”,正好是 FormRequest 的核心职责。
为什么不能放在中间件里
中间件面向所有请求,缺乏上下文。你无法在中间件里知道当前请求对应的是「注册用户」还是「更新地址」,也就没法安全地对 email 字段做 strtolower()(某些系统要求邮箱大小写敏感),更没法决定是否该把空字符串 '' 转成 null。强行塞进去会导致:
- 中间件里出现大量
if ($request->is('api/users') && $request->isMethod('post'))这类脆弱判断 - 参数预处理逻辑和权限校验、日志等混在一起,违反单一职责
- API 和 Web 路由共用同一中间件时,预处理行为可能冲突(比如 API 需要原样保留空格,Web 表单需要 trim)
为什么不能直接写在控制器里
控制器是业务入口,不是数据清洗站。把 $request->input('phone') 包一层 preg_replace('/[^0-9]/', '', ...) 写死在 store() 方法里,会带来三个实际麻烦:
- 相同清洗逻辑(如手机号标准化)在多个控制器方法中重复出现,改一处漏十处
- 单元测试时必须 mock 请求对象并手动构造脏数据,验证成本高
- 表单提交失败重定向后,
old()函数回显的是原始未处理值,用户体验割裂(用户看到自己输的+86 138-1234-5678,后端却存了13812345678,但页面仍显示带符号版本)
Form Request 的 prepareForValidation() 是正解
FormRequest 提供了 prepareForValidation() 这个钩子,它在 authorize() 之后、rules() 之前执行,且只作用于当前请求类型。这才是预处理的黄金位置:
- 天然绑定具体业务场景(如
StoreUserRequest只管注册) - 清洗后的值会自动进入验证流程和后续控制器,
$request->validated()拿到的就是干净数据 - 重定向时
old()也基于清洗后值生成(Laravel 10+ 默认启用)
示例:
class StoreUserRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$this->merge([
'email' => strtolower($this->email ?? ''),
'phone' => preg_replace('/[^0-9]/', '', $this->phone ?? ''),
'bio' => $this->bio === '' ? null : trim($this->bio),
]);
}
public function rules(): array
{
return [
'email' => 'required|email|unique:users',
'phone' => 'required|digits_between:10,15',
];
}
}
注意:$this->merge() 修改的是请求实例内部数据,不影响原始输入,也不会污染其他请求。
边界情况:全局性预处理怎么办
极少数需求确实需要跨所有请求统一处理,比如强制将所有字符串字段 trim。这时才考虑中间件,但必须加防护:
- 只对
application/x-www-form-urlencoded和multipart/form-data请求生效,跳过 JSON API - 用
$request->hasSession()或路由命名空间判断是否为 Web 流量 - 绝不修改数组型字段(如
tags[]),避免破坏结构 - 优先用
Kernel.php中的$middlewareGroups['web']而非全局中间件,缩小影响范围
真正难的不是“怎么做”,而是判断“该不该做”——绝大多数所谓“全局预处理”,其实是没想清楚业务边界,最后都回归到一个个具体的 FormRequest 里去解决。











