推荐在验证器类中定义protected公共方法作为自定义规则,方法名小写即规则名,返回布尔值;联动校验用scene+append添加字段并写对应方法;提示信息通过rule数组、$message属性或方法内返回字符串控制;复用时须每次new实例或调用reset()避免状态污染。

验证器里怎么写自定义规则函数
ThinkPHP 的 Validate 类支持通过 rule 数组注册闭包或方法名,但直接写闭包在复用和调试时容易出问题——比如报错堆栈不清晰、无法被 IDE 提示、热更新后不生效。
推荐统一在验证器类里定义公共方法,方法名即为规则名(小写),并用 protected 修饰。框架会自动识别并调用:
protected function checkMobile($value, $rule, $data, $field)
{
return preg_match('/^1[3-9]\d{9}$/', $value) === 1;
}注意三点:
- 方法必须返回布尔值,不能 throw 异常,验证失败靠返回 false 触发错误提示
- 第二个参数 $rule 是你配置时传的附加值(如 ['checkMobile' => '必须是中国大陆手机号'] 中的字符串)
- $data 是整个待验证数据数组,可用于跨字段校验(比如“结束时间必须大于开始时间”)
多个字段联动校验怎么做
单字段规则解决不了字段间依赖关系,比如密码与确认密码一致、金额不能为负且需小于余额。这时候得用 scene + extend 或直接在 check() 前手动补逻辑。
更稳妥的做法是:在验证器的 scene 定义中开启 only 字段白名单后,额外调用一个校验方法:
立即学习“PHP免费学习笔记(深入)”;
public function sceneRegister()
{
return $this->only(['password', 'confirm_password', 'email'])
->append('password', 'confirmPassword');
}
<p>protected function confirmPassword($value, $rule, $data, $field)
{
return $value === ($data['password'] ?? '');
}关键点:
- append() 是把字段加进当前场景,但不参与默认规则,只用于传参给自定义方法
- 联动校验逻辑尽量写在方法体内,别塞进闭包,否则单元测试难覆盖
- 如果涉及数据库查询(如检查用户名是否已存在),记得在方法里用 Db::name(...),别依赖外部注入,避免验证器变“服务类”
验证失败时提示信息不准确怎么办
ThinkPHP 默认提示是 “username 格式错误”,但用户根本不知道什么叫“格式错误”。真正有效的提示必须带业务语义,比如“手机号只能是 11 位中国大陆号码”。
有三个地方可以控制提示:
- 在 rule 数组里为每个规则配独立提示:['mobile' => ['require', 'checkMobile' => '手机号格式不正确']]
- 使用 message 属性批量设置:protected $message = ['checkMobile' => '请输入正确的手机号'];
- 动态提示(比如根据地区码切换):在自定义方法里返回字符串而非布尔值,框架会把它当错误信息处理
注意:$message 数组的 key 必须和规则名完全一致(包括大小写),否则静默失效;动态提示仅对方法有效,对内置规则(如 number)不生效。
验证器复用时容易忽略的上下文污染
验证器实例不是无状态的——$this->data、$this->error、$this->scene 都是可变属性。如果在多处共用同一个验证器对象(比如 app('validate') 单例),第二次调用 check() 可能残留上次的 error 或错误字段。
安全做法只有两种:
- 每次都 new 一个新实例:validate(new UserValidate())
- 显式重置状态:$v->reset()->check($data)(5.1+ 支持,低版本需手动 $v->error = [])
- 别在控制器里把验证器当属性长期持有,尤其在 Swoole 或 Hyperf 等长生命周期环境中,这是内存泄漏+数据错乱的高发区
还有个隐藏坑:验证器里调用模型的 save() 或事务操作,会导致验证和业务逻辑耦合。验证就该只管“数据对不对”,不该管“存不存得进去”。



















