ThinkPHP8 验证器升级:用场景 + 自定义规则告别一堆 if 判断

2026-08-29 0 594

以前做表单提交,总是看到控制器里满屏的 if ($_POST[‘name’] == ”),或者一堆验证规则堆在一起。这次用 ThinkPHP8验证器把逻辑理清楚,真的能少写一百行废话。最关键是场景验证这个功能,同一个模型在登录和注册时用不同规则,不用再造两个类。

从一个真实的坑说起

我接手过一个老项目,里面有个“用户注册”的接口,控制器里密密麻麻写了十几次 if 判断。用户名长度、密码复杂度、邮箱格式,还有手机号规则。后来要加一个“修改个人资料”的接口,发现很多规则重叠,但又有细微差别。当时懒得重构,就复制粘贴改了一通,结果手机号规则居然漏掉了,导致用户能提交乱七八糟的号码。

所以后面我直接在 ThinkPHP 里用验证器 + 场景,把每个字段的验证规则定义好,不同场景设置不同的“启用/禁用”状态。代码清爽得不像话,关键是出错了知道去哪改。

验证器基本结构

在 ThinkPHP8 中,我们通常会在 app/validate/ 目录下新建一个验证器类。比如 User.php,里面主要定义 protected $ruleprotected $message。废话不多说,直接上代码。

<?php
namespace appvalidate;

use thinkValidate;

class User extends Validate
{
    protected $rule = [
        'username' => 'require|min:3|max:20',
        'password' => 'require|min:6|max:30',
        'email'    => 'email',
        'phone'    => 'mobile',
        'gender'   => 'in:0,1,2',
    ];

    protected $message = [
        'username.require' => '用户名必填',
        'username.min'     => '用户名最少3个字符',
        'username.max'     => '用户名最多20个字符',
        'password.require' => '密码必填',
        'password.min'     => '密码最少6位',
        'password.max'     => '密码最多30位',
        'email.email'      => '邮箱格式不对',
        'phone.mobile'     => '手机号码不合法',
        'gender.in'        => '性别取值异常',
    ];

    // 场景设置
    protected $scene = [
        'register' => ['username', 'password', 'email', 'phone'],
        'edit'     => ['username', 'email', 'gender'],
        'login'    => ['username', 'password'],
        'changePwd' => ['password'],
    ];
}
    

看到没?$scene 数组定义了不同场景下需要验证哪些字段。比如 edit 场景里没有 phone,所以编辑个人资料时就不用担心手机号了。而且我并没有为每个场景单独建类,这就防止了代码重复。

为什么场景这么好用?

比如注册场景,你要验证 username、password、email、phone。而改基本信息时,不应该强制改密码,也不该改手机号,所以场景里只放 username、email、gender。如果你以后再做一个接口“仅修改密码”,那直接定义一个 'changePwd' => ['password'],里面只检查密码规则即可。

而且你可以给某个场景指定某个字段的额外规则,写法是:

protected $scene = [
    'register' => ['username'=>'require|min:5', 'password', 'email', 'phone'],
];
    

这样在注册场景中,username 的规则就不是类里面定义的 min:3,而是 min:5,即覆盖了基本规则。注意如果没有覆盖则自动使用全局 $rule 中对 username 的规则。

控制器里怎么用?

在控制器中,你只需要用门面验证器或者依赖注入,然后调用 scene() 方法选择场景,最后 check() 一下输入数据。下面是 ThinkPHP8 控制器的典型写法:

<?php
namespace appcontroller;

use thinkfacadeValidate;
use appvalidateUser as UserValidate;

class Auth
{
    // 注册
    public function register()
    {
        $data = [
            'username' => input('username'),
            'password' => input('password'),
            'email'    => input('email'),
            'phone'    => input('phone'),
        ];

        $validate = new UserValidate();
        if (!$validate->scene('register')->check($data)) {
            return json(['code' => 1, 'msg' => $validate->getError()]);
        }

        // 验证通过,写入数据库...
        return json(['code' => 0, 'msg' => '注册成功']);
    }

    // 编辑资料
    public function edit()
    {
        $data = [
            'username' => input('username'),
            'email'    => input('email'),
            'gender'   => input('gender'),
        ];

        $validate = new UserValidate();
        if (!$validate->scene('edit')->check($data)) {
            return json(['code' => 1, 'msg' => $validate->getError()]);
        }

        // 更新资料...
        return json(['code' => 0, 'msg' => '已保存']);
    }

    // 登录
    public function login()
    {
        $data = [
            'username' => input('username'),
            'password' => input('password'),
        ];

        $validate = new UserValidate();
        if (!$validate->scene('login')->check($data)) {
            return json(['code' => 1, 'msg' => $validate->getError()]);
        }

        // 登录逻辑...
    }
}
    

你看,控制器根本不需要写各种 if 嵌套,验证器帮你搞定一切。而且因为验证器是一个独立类,以后想改规则,直接动 UserValidate,不用翻控制器代码。

自定义规则:有些验证不能用内置的

内置规则虽然多,但总有意想不到的坑。比如“验证密码是否包含字母和数字”,这得自己写。ThinkPHP8 支持两种方式:一种是给验证器类增加一个方法,比如 checkPassword,另一种是使用闭包。这里我推荐用前者,更好复用和测试。

拿“密码复杂度”为例,在验证器类里加一个方法:

// 验证密码要包含字母和数字
protected function checkPasswd($value, $rule, $data)
{
    if (!preg_match('/[A-Za-z]/', $value)) {
        return '密码必须包含字母';
    }
    if (!preg_match('/d/', $value)) {
        return '密码必须包含数字';
    }
    return true;
}
    

然后在规则里引用这个方法名:

'password' => 'require|min:6|max:30|checkPasswd',
    

注意:自定义方法名在验证器类中必须以 check 开头,后面的名字随意,这里就是 checkPasswd。然后验证器会自动转换成 checkPasswd 调用,传递三个参数:$value(当前字段值)、$rule(规则参数,如果规则字符串中有冒号后面的内容)、$data(整个数据集)。

如果你只想在这个规则里加个参数,比如密码长度至少 10 位,那可以写成:

'password' => 'require|checkPasswd:10',
    

然后在方法里接住参数:

protected function checkPasswd($value, $rule)
{
    $minLen = $rule ?: 8;
    if (strlen($value) < $minLen) {
        return '密码长度至少' . $minLen . '位';
    }
    return true;
}
    

很灵活有木有。

场景 + 自定义规则解决一个常见复杂案例

假设用户注册时,手机号是可选的,但如果你填写了就必须是合法的中国手机号;如果没填,则不允许给空字符串。这个规则用场景怎么设计?

我们先在验证器里为手机号单独加一个“非必填,但要合法”的规则:

protected function checkMobileOptional($value)
{
    if ($value === '' || $value === null) {
        return true; // 允许为空
    }
    if (!preg_match('/^1[3-9]d{9}$/', $value)) {
        return '手机号格式不正确';
    }
    return true;
}
    

然后全局规则中把 phone 改成这样:

'phone' => 'checkMobileOptional',
    

在注册场景中,你仍然把 phone 列入,这样如果用户填入了一个手机号,就会自动验证;如果为空,也可以跳过。这就是“可选但校验”的爽点。

如果你希望在某些场景(比如管理员后台)手机号必填,那可以这样定义另一个场景:

protected $scene = [
    'admin_add' => ['username', 'password', 'phone'=>'require|mobile'],
];
    

这里对 phone 字段使用内置的 require 和 mobile,覆盖了全局的可选规则。完美。

验证器 vs 表单请求

Laravel 有 FormRequest,ThinkPHP8 虽然没有完全等价物,但验证器类已经够用。你可以把验证器类理解为“独立校验层”,你可以在控制器中使用门面或者注入。如果你想做中间件,甚至可以在路由阶段就验证,但我觉得放在控制器里最直观。

什么时候用验证器而不是自己写逻辑?

只要你的字段超过两个,就建议上验证器。即使简单到只要非空检查,用验证器也会让你的控制器看起来更清爽。特别是团队协作的时候,后来接手的人一看验证器就知道字段规则,不用满项目找 if。而且 ThinkPHP8 验证器还支持跳出自定义错误消息,前端可以精准提示用户。

实际项目里一个坑是:当验证不通过时,getError() 返回的可能是一个数组,而不是字符串。你可以把验证器的 $dispatcher 设置为空或者用批量验证?这个问题可以这样解决,在控制器里做类型判断:

$error = $validate->getError();
if (is_array($error)) {
    $error = implode(',', $error);
}
    

其实多数情况它是字符串。但如果你开启了批量验证,就会变成数组啦。为了避免前端看到奇怪的东西,建议统一处理一下。

另一个进阶玩法:验证器里用闭包

有些规则只在某一种业务场景中出现,且不好抽象成方法。你可以在规则里直接写闭包,比如:

'username' => function($value) {
    // 不能包含敏感词
    if (strpos($value, 'admin') !== false) {
        return '用户名不允许包含admin';
    }
    return true;
},
    

但这有个问题:一旦这个规则定义后,所有场景都会生效,除非你在场景里手动覆盖取消。我认为闭包适合临时用,长期维护的话还是建议用命名方法。

配合表单令牌更完美

验证器还能检查表单令牌,防止 CSRF。你在验证器里加一个字段 '__token__' => 'token',然后表单中输出 <input type="hidden" name="__token__" value="{$Request.token}" />,控制器里正常用验证器 check 即可。这比每个控制器手动检查要省事。

很多人不知道 TP8 内置了 token 验证规则,其实有,用在验证器里就是 token。不过平时写 API 接口都用 JWT,用不到,但 Web 页面表单建议开启。

总结一下

说白了,ThinkPHP8 的验证器与场景是一个很成熟的“表单请求校验”解决方案。你只需要把字段规则集中在验证器类里,通过 scene() 控制不同场景的字段组合,再用自定义方法去覆盖那些特定业务规则。控制器和业务代码就会变得非常干净,不容易遗漏校验,也更方便后期维护。

下次写接口的时候,别再一长串 if (empty($name)) 了,试着用验证器把规则收拢起来。可能一开始你觉得多了一个类有点麻烦,但用上两个项目以后,你一定会真香。

ThinkPHP8 验证器升级:用场景 + 自定义规则告别一堆 if 判断
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP8 验证器升级:用场景 + 自定义规则告别一堆 if 判断 https://www.taomawang.com/server/thinkphp/2666.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务