先明确几个关键结论:ThinkPHP 的模型层数据验证,并不是“把验证规则写进模型就结束”这么简单。它实际上分为三层协同运行——模型自带验证规则、控制器中显式调用的验证、以及独立的验证器类。三者之间彼此独立,不会自动叠加,最终真正生效的,始终是最后一次启用的那一套规则。

ThinkPHP 模型层验证并不是“加个规则就能自动搞定”,而是分成三层:模型内置规则、控制器显式验证、独立验证器类——三者不会合并,最终以后一次生效的规则为准。
模型类里写 $rule 和 $message 是最常见的 ThinkPHP 验证方式
通常直接在模型中定义 protected $rule 和 protected $message,在执行 save() 时会自动触发验证,无需每次都手动调用 validate(),这是 ThinkPHP 模型验证中最常见、也最适合基础表单校验的写法:
- 字段验证规则字符串支持使用管道符分隔,例如
'name' => 'require|max:20'。不过要注意,一旦require校验失败,后面的max不会继续执行,这属于短路机制。 $message的键名必须严格采用"字段名.规则名"格式,例如'name.require' => '姓名不能为空'。如果少了点号、字段名写错,或者规则名拼写错误,就会退回系统默认提示信息。- 如果字段名本身包含下划线或采用驼峰写法,例如
user_name,那么在$message中也必须保持完全一致,不能随意写成username.require。 - 空值校验尤其要留意:
require默认使用的是Model::EXISTS_VALIDATE,也就是“字段存在时才校验”。如果表单提交时压根没有传这个字段,它就不会进入 ThinkPHP 的验证流程。
控制器里调用 $model->validate($rules) 会直接覆盖模型原有验证规则
这里很多开发者容易误解:这不是在“追加规则”,而是在“整体替换”。典型错误就是想临时增加一条 unique 唯一性验证,结果把模型里原本配置好的 email、mobile 等规则全部覆盖掉了:
- 传入的
$rules必须是完整规则数组,例如['name' => 'require', 'email' => 'email|unique:user,email']。少写一个字段,就意味着该字段在这次保存中不会被校验。 - 错误提示信息也要同步通过第二个参数传入,否则系统会直接使用默认提示,例如
['name.require' => '名称必填']。 - 场景验证写法如
validate('User.edit'),本质上是去加载对应的验证器类场景,它和当前模型类中的$rule没有直接关系,不能理解成在模型规则上继续叠加。 - 不要在控制器里连续多次调用
validate()。因为第二次调用会覆盖第一次设置的规则,最终只会保留最后一次配置的那一组验证条件。
联合唯一验证(unique)是 ThinkPHP 数据验证里最容易出错的环节
unique 规则看起来简单,但在实际项目中,字段顺序、数据表前缀、NULL 值处理、更新时排除当前 ID,这四个问题最容易导致 ThinkPHP 唯一性验证失效或误判:
- 联合唯一验证必须写成
['unique' => ['user', 'username,email']]这种形式,其中username,email的字段顺序必须与数据库联合索引的字段顺序完全一致,否则 MySQL 很可能无法正确利用索引。 - 如果你的数据表使用了前缀,例如
tp_user,那么unique的第一个参数就必须写成'tp_user'。如果仍然写'user',验证查询可能找不到真实数据表,从而导致结果异常。 - MySQL 对
UNIQUE索引中的NULL值处理相对宽松,但 ThinkPHP 的unique规则往往会把null当作普通值参与查询,这就可能把原本应当允许保存的记录错误拦截。 - 在编辑更新数据时,必须显式传入主键,例如
['id' => 123, 'username' => 'a', 'email' => 'b@x.com']。否则验证器会把当前这条记录自己也判断为重复数据;如果主键字段名不是id,规则中的对应字段也必须同步调整。
动态判断必填项(例如调用 API 决定是否校验 price)不能直接塞进模型验证规则
ThinkPHP 模型验证本质上是同步执行的,无法原生等待外部 HTTP 请求结果。如果强行把这类依赖接口返回的逻辑塞进模型规则,不仅容易导致请求阻塞、超时,还会破坏写入流程和事务一致性:
- 更稳妥的做法,是在
beforeWrite钩子中发起请求,借助think\Http或guzzlehttp/guzzle处理,并明确设置timeout => 3等超时参数。 - 拿到接口响应后再手动判断:如果某个字段此时必须填写但实际为空,就主动抛出
ValidateException,这样save()会自动终止,避免脏数据写入。 - 这类外部依赖必须配合缓存机制,例如
Cache::remember(),同时增加降级开关,例如配置'field_required_fallback' => 'loose'。否则一旦第三方服务不可用,整条写入链路都会受到影响。 - 不要轻信“运行时动态修改
$rule数组就能解决”的方案——因为验证规则往往在模型实例化时就已经完成编译,后续再改,通常并不会真正影响本次验证行为。
真正有难度的,并不是把那几行 $rule 写对,而是要彻底弄清楚 ThinkPHP 数据验证规则究竟在哪一层生效、不同层之间是谁覆盖谁,以及当验证逻辑涉及 IO、接口调用或外部依赖时,必须跳出声明式验证框架,回到命令式控制流中手动接管整个校验过程。
