ThinkPHP 验证器里 `unique` 规则那个隐蔽的"表名陷阱":我帮用户改了个昵称,结果全库用户名跟着崩了
上周有个老用户反馈,说改昵称时系统提示"用户名已被占用"。我第一反应是缓存没清,让他等十分钟再试。结果他截图过来——占用的那个用户名,是他自己三年前注册的小号。
查了半天,问题出在验证规则上。先给大家看看我当初写的"经典错误示范":
// 错误写法:看似没毛病,实则埋雷
$validate = Validate::rule([
'nickname' => 'require|unique:user', // 注意这里
'username' => 'require|unique:user',
]);
这段代码跑了一年多,为啥突然出事?因为 `unique:user` 这个简写,ThinkPHP 默认去查的是 当前请求对应的数据表,而不是你字面写的 `user` 表。更坑的是,如果控制器里前面某处用 `model('User')` 查过数据,验证器可能直接复用了那个查询实例的表名设定——而我前阵子给 `User` 模型加了个全局的 `soft_delete` 查询范围,连带把表别名也改了。
最终结果是:`nickname` 的 `unique` 校验,实际查的是 `user` 表没错,但 `username` 那条因为前面代码的"污染",偷偷去校验了另一张关联表的同名字段。用户的小号在那张关联表里有记录,于是自己的用户名被自己的小号"占用"了。
正确的写法应该显式指定完整参数,别让框架"猜":
// 正确写法:把表名、字段、排除条件都写死,不留模糊空间
$validate = Validate::rule([
'nickname' => 'require|unique:user,nickname,' . $userId,
'username' => 'require|unique:user,username,' . $userId . ',id',
]);
或者更保险一点,直接闭包自定义:
'username' => ['require', function ($value, $data) use ($userId) {
$exists = Db::name('user')
->where('username', $value)
->where('id', '', $userId)
->find();
return $exists ? '用户名已被占用' : true;
}]
这个坑最阴险的地方在于:它不会报错。验证规则语法完全合法,校验逻辑也"正常执行"了,只是查错了表。如果不是用户刚好有个同名小号,我可能到现在都没发现全库有批用户的用户名校验是飘在别处的。
现在我的规矩是:所有 `unique` 规则必须带全四个参数,闭包校验优先。框架给的"便利"有时候是糖衣,有时候是炮弹,这次我算尝出味了。
你们有没有被验证器的"智能推断"坑过?欢迎交流,让我知道我不是一个人。

