复杂表单验证应写在 form request 而非控制器,因其支持 rules()、messages()、authorize() 且自动触发;控制器仅负责业务流转。

验证逻辑该写在控制器还是 Form Request?
复杂表单验证别塞进控制器——90% 的臃肿都源于这里。控制器只该做「业务流转」,不该承担字段校验、错误组装、消息映射这些事。
Form Request 是 Laravel 官方推荐的解耦方案,它天然支持三件事:rules()、messages()、authorize(),而且一定义就自动触发,不用手动调用 validate()。
- 简单场景(比如仅 2–3 个字段、无权限判断、无自定义提示)可直接在控制器用
$request->validate() - 只要涉及
unique、confirmed、嵌套数组(如meta.tags.*)、文件上传(avatar)、或需要复用(比如多个接口共用同一套注册校验),就必须抽成Form Request - 生成命令固定为:
php artisan make:request StoreUserRequest,类会落在app/Http/Requests/
rules() 里规则太多怎么分层?
别把所有规则堆在一个 return 数组里。Laravel 允许你在 rules() 方法内做条件分支和动态拼接,这对多步骤表单、角色差异化校验特别有用。
- 用
$this->user()或$this->route('id')获取上下文,区分「新建」和「编辑」:编辑时跳过unique或放宽password规则 - 对数组字段(如
roles.*)用Rule::forEach()或array+required组合,避免写死索引 - 敏感字段(如
email、phone)可封装成私有方法:emailRules(),便于测试和复用 - 避免重复写
required|string|max:255这类组合,提取成常量或 trait 中的静态方法
错误消息太散乱,怎么统一维护?
硬编码在 messages() 方法里或控制器的第三参数数组中,等于给自己埋雷:改一处漏十处,国际化更无从谈起。
真正可持续的做法是全部收口到语言文件:lang/en/validation.php 和 lang/zh/validation.php。
- 字段别名统一写在
'attributes'键下:'email' => '邮箱地址',后续所有:attribute占位符自动替换 - 规则级提示优先走
'required' => ':attribute 不能为空'这种泛化写法,比逐条写'email.required'更省力 - 特殊提示(如
unique:users要说“该邮箱已被注册”)才单独加键:'unique' => [ 'users' => ':attribute 已被注册' ] - Form Request 中的
messages()只保留极少数无法用语言文件覆盖的动态提示(比如带变量的日期范围错误)
第三方规则(如 spatie/laravel-validation-rules)怎么不破坏结构?
引入 spatie/laravel-validation-rules 后,别直接往 rules() 数组里塞 'country_code' => 'country_code' 就完事——这会让团队新人看不懂规则来源,也难定位问题。
- 安装后无需额外配置,但要在
rules()中显式 use 对应规则类,比如:use Spatie\ValidationRules\Rules\CountryCode; - 规则实例化更清晰:
'country_code' => ['required', new CountryCode()],一眼看出是外部扩展 - 自定义规则(如年龄校验)建议独立成
app/Rules/AgeAfter.php,并在rules()中同样用new AgeAfter('18')调用,保持风格一致 - 注意兼容性:Spatie 包部分规则(如
Authorized)依赖策略,若没配好Gate会静默失败,务必在本地跑通再上线
最常被忽略的是验证上下文的丢失——比如在 StoreUserRequest 里调用 $this->isJson() 判断请求类型,却忘了在 messages() 或自定义规则里同步处理 JSON vs 表单的提示差异。多模态场景下,错误消息也得「多模态」。











