用 ThinkPHP8 开发接口时,我用 30 行代码干掉了繁琐的验证器

2026-08-26 0 544

以前接手过一个老项目,控制器里的验证代码是这么写的:

public function save(Request $request)
{
    $data = $request->post();
    if (empty($data['name'])) {
        return json(['code' => 422, 'msg' => '姓名不能为空']);
    }
    if (!isset($data['age']) || $data['age'] < 0) {
        return json(['code' => 422, 'msg' => '年龄不正确']);
    }
    // ... 还有七八个字段要检查
    // 最后才真正写业务逻辑
}

这种代码短时间看着还行,但接口稍微一多,痛苦就来了。每个方法都堆着同样的验证逻辑,改一个规则要翻好几个控制器。上个月用 ThinkPHP8 起了个新项目,我决定把验证这块彻底收拾干净。

先定一个小目标

我想达到的效果是:在控制器里写接口,直接用一行代码完成参数验证,验证不通过就直接抛出异常,由全局异常处理器统一返回 JSON 错误信息。像这样:

public function save(Request $request)
{
    $data = $request->post();
    // 如果验证不过,会抛出 ValidateException
    validate(CheckRequest::class)->scene('save')->check($data);

    // 业务代码继续往下走...
}

这样每个方法只需要知道自己要验证哪些数据,至于如何返回错误,根本不关心。而且验证器本身可复用。

但 ThinkPHP8 自带的验证器抛出的是 thinkexceptionValidateException,默认异常处理虽然能捕获,但返回的错误格式并不符合我接口的约定。我需要自己处理这个异常。

第一步:写一个标准的验证器

按照 TP8 的官方文档,验证器类放在 appvalidate 目录下。我这里做一个注册接口的例子,它有两个场景:save 表示新增,update 表示更新。

<?php
declare(strict_types=1);

namespace appvalidate;

use thinkValidate;

class CheckRequest extends Validate
{
    protected $rule = [
        'name' => 'require|max:20',
        'age'  => 'require|integer|between:1,120',
        'email' => 'email',
    ];

    protected $message = [
        'name.require' => '姓名必须填写',
        'name.max'     => '姓名不能超过20个字',
        'age.require'  => '年龄必须填写',
        'age.integer'  => '年龄必须是数字',
        'age.between'  => '年龄范围在1~120之间',
        'email'        => '邮箱格式不对',
    ];

    protected $scene = [
        'save' => ['name', 'age', 'email'],
        'update' => ['age', 'email'],
    ];
}

这里我故意简单写几个字段,实际项目可以按需扩展。重点不在规则,而在后面的异常处理。

第二步:自定义一个接口异常类

我希望最终返回的 JSON 格式符合团队习惯:

{
    "code": 422,
    "message": "参数错误",
    "data": {
        "name": ["姓名必须填写"],
        "age": ["年龄必须填写"]
    }
}

所以我在 appexception 下新建了一个 ApiException 类,但它不是用来替代 TP8 自身的异常,而是用来处理验证异常转化后的数据。

<?php
declare(strict_types=1);

namespace appexception;

use Exception;

class ApiException extends Exception
{
    protected $data;

    public function __construct(string $message = "", array $data = [], int $code = 422)
    {
        parent::__construct($message, $code);
        $this->data = $data;
    }

    public function getData()
    {
        return $this->data;
    }
}

第三步:写一个公共函数,专门处理验证逻辑

我新建了一个 appcommon.php 文件(如果项目没有就自己建),里面写一个辅助函数:

<?php
// appcommon.php

use thinkfacadeValidate;
use appexceptionApiException;

if (!function_exists('api_validate')) {
    /**
     * 验证数据,失败抛 ApiException
     * @param array $data 请求参数
     * @param string $validateClass 验证器类名
     * @param string $scene 场景名
     * @return array 验证通过返回原始数据
     * @throws ApiException
     */
    function api_validate(array $data, string $validateClass, string $scene = '')
    {
        $validator = Validate::make();
        
        // 加载验证器类中的规则和场景
        if (class_exists($validateClass)) {
            $validator = new $validateClass;
        }
        
        if ($scene !== '') {
            $validator->scene($scene);
        }
        
        if (!$validator->check($data)) {
            throw new ApiException('参数错误', $validator->getError(), 422);
        }
        
        return $data;
    }
}

看到这里你可能会问:直接使用 TP8 的 validate() 不就好了?为什么要绕一层?因为 validate() 抛出的异常不是我们自定义的 ApiException,而且我想让它统一转换为上面的格式。

第四步:写全局异常处理

在 ThinkPHP8 中,自定义异常处理只需要重写 appExceptionHandle.phprender() 方法。我把它改为先处理 ApiException,其他的仍然交给父类。

<?php
declare(strict_types=1);

namespace app;

use thinkdbexceptionDataNotFoundException;
use thinkdbexceptionModelNotFoundException;
use thinkexceptionHandle;
use thinkexceptionHttpException;
use thinkexceptionHttpResponseException;
use thinkexceptionValidateException;
use thinkResponse;
use Throwable;
use appexceptionApiException;

class ExceptionHandle extends Handle
{
    public function render($request, Throwable $e): Response
    {
        // 如果是自定义 ApiException
        if ($e instanceof ApiException) {
            return json([
                'code' => $e->getCode(),
                'message' => $e->getMessage(),
                'data' => $e->getData()
            ])->code(200); // 业务状态码放在 body 里,HTTP 状态码统一 200
        }

        // 如果是 TP8 的验证异常,也转成同样的结构
        if ($e instanceof ValidateException) {
            return json([
                'code' => 422,
                'message' => '参数错误',
                'data' => $e->getError()
            ])->code(200);
        }

        // 其他异常继续交给父类
        return parent::render($request, $e);
    }
}

注意我返回 HTTP 状态码固定为 200,业务状态码放在 body 里。这么做是为了让前端方便处理,不用区分 HTTP 层和业务层的不同。如果你有别的习惯,改成 422 也一样。

第五步:在控制器里使用验证

现在写注册接口:

<?php
declare(strict_types=1);

namespace appcontroller;

use thinkRequest;
use appvalidateCheckRequest;
use appexceptionApiException;

class User
{
    public function register(Request $request)
    {
        $data = $request->post();
        
        // 一行代码完成验证
        api_validate($data, CheckRequest::class, 'save');

        // 验证通过,假装创建用户
        return json([
            'code' => 200,
            'message' => '注册成功',
            'data' => ['id' => 123]
        ]);
    }
}

如果你不想用辅助函数,直接写在控制器里也行,但那样每个控制器都要重复 try-catch。我还是喜欢辅助函数的方式,一行搞定,清清爽爽。

再更进一步:把验证器参数绑定到 Request 上

用了一段时间,我发现每个方法都要先去拿 $request->post(),再手动传给 api_validate,还是有点啰嗦。干脆我在控制器基类里做了一个统一入口,通过宏或者注解?其实不用那么复杂,直接在系统公共函数里再封装一个小助手:

function request_validate(string $validateClass, string $scene = '')
{
    $request = request();
    $data = $request->post();
    // 如果需要 GET 参数,也可以 $request->get()
    return api_validate($data, $validateClass, $scene);
}

然后控制器里可以直接:

$data = request_validate(CheckRequest::class, 'save');

这一行即使从控制器里拿出来也能用,测试也好写。

踩过的一个小坑

Validate::make() 的时候,我发现如果我用自定义的验证器类,系统会自动合并内置的规则,这没什么问题。但要注意:如果验证器里定义了 scene,并且你又调用了 scene() 方法,那么验证器只会验证该场景里定义的字段。一开始我没写 scene('save'),结果所有规则全被校验了,那些非必填的字段也会报错。所以记得一定要指定场景,或者有时候我干脆不写场景,在验证器里用 $this->scene 来控制。

另外一个坑是:getError() 返回的数据结构。我一开始以为是一个字符串,后来发现它默认是一个数组,比如 ['name' => '姓名必须填写']。但如果是多个字段错误,它是一个嵌套数组。为了统一给前端,我在 ApiException 里把 data 字段直接传成了 $validator->getError()。如果以后想改成字符串,还要自己拼接一下。

这套方案好在哪里

最大的好处是:所有接口的错误返回格式都一样,前端不用对接多种结构。而且验证器和控制器完全解耦,同一个验证器可以在不同的接口场景中复用。

另外,如果想加一个“或者”规则,或者“自定义验证方法”,也是在验证器类里加,不用动控制器。项目跑了一个多月,基本没有因为接口参数问题扯皮过。

最后的一点废话

我不喜欢为了用框架而用框架,更讨厌控制器里塞一堆判断。拿到一个 TP8 项目,第一件事就是把验证模块理顺。这套东西你花半小时搭好,后面能给你省下无数个半小时。如果你现在还在用老方法写 if,不妨试试,反正代码已经很烂了,改一改也不会更差。

用 ThinkPHP8 开发接口时,我用 30 行代码干掉了繁琐的验证器
收藏 (0) 打赏

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

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

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

淘吗网 thinkphp 用 ThinkPHP8 开发接口时,我用 30 行代码干掉了繁琐的验证器 https://www.taomawang.com/server/thinkphp/2640.html

常见问题

相关文章

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

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