必须写在独立的验证器类里,控制器仅调用validate()传入实例;验证器支持复用、测试、场景,避免耦合与维护困难。

验证规则写在控制器里还是验证器类里
必须写在独立的验证器类里,控制器里直接调用 validate() 传入验证器实例。ThinkPHP 的验证器类能复用、可测试、支持场景(scene),而把规则硬塞进控制器会导致逻辑耦合、调试困难、后续改表单字段时容易漏改验证。
常见错误现象:validate(['name' => 'require|alphaNum']) 这种字符串规则写法在复杂场景下完全不可维护——比如要校验手机号是否已存在、密码和确认密码是否一致、上传文件类型+大小联动判断,字符串规则根本写不下去。
- 验证器类路径统一放在
app\validate\下,类名首字母大写,如UserFormValidate - 继承
think\Validate,重写rule和message属性,或在__construct()里动态注册 - 自定义规则函数必须定义在验证器类内部(
protected function checkPasswordConfirm($value, $rule, $data)),不能丢到全局函数里,否则框架找不到
怎么写一个带数据库查询的自定义验证规则
比如“邮箱不能重复”,不能靠 unique 规则硬配表名字段,因为实际业务中常要加额外条件(如只查 status=1 的用户),或跨库/跨模型查询。这时候得手写回调验证。
核心是利用验证方法的第 3 个参数 $data——它包含全部提交数据,可从中取其他字段参与判断;同时要用 $this->fail() 主动报错,不能 return false。
- 在验证器类中定义方法:
protected function checkEmailUnique($value, $rule, $data) { $exists = User::where('email', $value)->where('status', 1)->find(); if ($exists) { $this->fail('该邮箱已被注册'); } } - 规则数组里写:
'email' => 'require|email|checkEmailUnique',注意函数名不用加function:前缀 - 别在规则里直接 new 模型——会破坏依赖注入和事务上下文;应通过容器获取:
app(User::class)->where(...)
表单嵌套数组(如多地址、多商品)怎么验证
ThinkPHP 默认不支持深层点号路径(如 addresses.0.name)的规则定义,直接写 'addresses.0.name' => 'require' 会失效。必须用数组方式声明字段,并配合 each 规则或自定义循环验证。
性能影响明显:对每个子项都执行一次完整验证流程,如果嵌套层级深、子项多,容易触发 N+1 查询。得提前在验证前做数据预处理。
- 字段定义写成数组:
'addresses' => 'array',再加'addresses.*.name' => 'require|length:2,20'(5.1+ 支持通配符,但需开启strict模式) - 更稳的方式是自定义规则:
protected function checkAddresses($value, $rule, $data) { foreach ($value as $k => $item) { $result = $this->batch()->check($item, ['name' => 'require', 'phone' => 'mobile']); if (!$result) { $this->fail("第".($k+1)."条地址信息有误:".implode(',', $this->getError())); break; } } } - 别忘了在控制器里关掉默认批量验证:
$validate->batch(false),否则嵌套数组会被当成单个值跳过
验证失败后如何保留原始表单数据(含文件)
ThinkPHP 默认验证失败返回 JSON 或跳转,但复杂表单往往需要原样回显——尤其是用户刚选好头像、填完 5 个地址又因邮箱格式错被清空,体验极差。关键不是“怎么回显”,而是“怎么让文件不丢”。
文件无法通过 HTTP GET 回传,也不能存 session(大小和生命周期受限)。唯一可靠做法是前端用 JS 缓存 file input 的 files 对象,验证失败后手动还原 DOM,后端只负责返回错误字段和消息。
- 后端验证失败时,用
$validate->getError()提取具体字段错误,组装成['email' => '邮箱格式不正确']格式返回 JSON - 前端拿到后,遍历错误字段,用
document.querySelector('[name="email"]').nextElementSibling.textContent = msg显示提示 - 文件 input 不做任何服务端干预,JS 在提交前用
FormData.append('avatar', input.files[0])手动附带,验证失败也不清空 input.value - 别信网上“把文件 base64 后存 hidden input”的方案——大文件直接卡死浏览器,且无意义增加传输量
真正难的不是写规则,是让验证逻辑能随着表单结构变化自动适配,而不是每次加个字段就去翻三个地方改验证、改提示、改前端校验。嵌套、异步上传、条件显示字段——这些才是压垮验证设计的细节。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











