小白折腾服务器(七):自定义接口错误响应格式

上一篇是使用后置中间件自定义接口成功返回格式
这篇来写处理异常,自定义接口错误返回格式

基础小知识
laravel 处理异常的位置在 app/Exceptions 这个目录,如果新建异常类,就在这个目录
这个目录中,最重要的是 Handler.php 这个文件,如何处理渲染异常,是这个类的 rander 方法。如果你需要自定义错误输出,其实就是重写这个 rander 方法。

leo 老师在电商实战那本书里第 5.7 节 - 优雅地处理异常 里面教过,

Laravel 5.5 之后支持在异常类中定义 render () 方法,该异常被触发时系统会调用 render () 方法来输出,我们在 render () 里判断如果是 AJAX 请求则返回 JSON 格式的数据,否则就返回一个错误页面。

这就是说,我们只要新建一个 xxxException 类,让它继承自 Exception ,
在这个 xxxException 类中,定义一个构造函数,用来接收参数(如 $message, $code),
再定义一个 render 方法,处理接收的参数,组装成你想要的格式(如 $content = ['message' => $this->message, 'code' => $this->code]), 最后返回 return response()->json($content, $status);,$content 就是你自定义的异常输出内容,$status 是 http 状态码。
然后在控制器方法中,throw new xxxException('message',500)。试一下,就是你自定义的错误格式了。
可以新建多个异常类,用来输出不同的异常。
最简单的异常类:

namespace App\Exceptions;

use Exception;

class SimpleException extends Exception
{
    const HTTP_OK = 200;
    const HTTP_ERROR = 500;

    protected $data;

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

    public function render()
    {
        $content = [
            'message'   => $this->message,
            'code'      => $this->code,
            'data'      => $this->data ?? [],
            'timestamp' => time()
        ];

        $status = self::HTTP_ERROR;

        return response()->json($content, $status);
    }
}

如果不想新建异常类,只需要统一异常错误输出,就修改 app/Exceptions/Handler 中的 render 方法就好了。

    public function render($request, Exception $exception)
    {
      //  这里通常要用到 instanceof 这个运算符,先判断再处理
      //  return parent::render($request, $exception);   注释掉这行
      return  response()->json($exception->getMessage(), 500);
    }

做个小总结,自定义错误处理有两个方法(其实都一样)
1. 新建一个异常类,在 app/Exceptions/Handler 中的 render 方法将所有异常都指向你新建的异常类。
2. 直接修改 app/Exceptions/Handler 中的 render 方法,自定义异常格式。

第一种:BaseException#

BaseException 是一个很灵活的异常类,可以自定义多种参数。

  1. 在 app/Exceptions 下新建 BaseException
namespace App\Exceptions;

use Exception;

class BaseException extends Exception
{
    const HTTP_OK = 200;

    protected $data;

    protected $code;

    public function __construct($data, int $code = self::HTTP_OK, array $meta = [])
    {
        // 第一个参数是data,是因为想兼容string和array两种数据结构
        // 第二个参数默认取200,是因为公司前端框架限制,所以是status取200,错误码用code表示
        // 如果第二个参数是任意httpStatus(如200,201,204,301,422,500),就只返回httpStatus,如果是自定义错误编码,(如600101,600102),就返回httpstatus为200,返回体中包含message和code。
        // 第三个参数默认为空数组,如果在message和code之外,还需要返回数组,就传递第三个参数
        $this->data = $data;
        $this->code = $code;
        $this->meta = $meta;
//        parent::__construct($data, $code);
    }

    public function render()
    {
        $httpStatus = getHttpStatus();
        $status  = in_array($this->code, $httpStatus) ? $this->code : self::HTTP_OK;
        $content = [];
        if (is_array($this->data)) {
            $content = $this->data;
        }
        if (is_string($this->data)) {
            $content = in_array($this->code, $httpStatus)
                ? [
                    'message' => $this->data
                ]
                : [
                    'message' => $this->data,
                    'code'    => $this->code,
                    //                    'timestamp' => time()
                ];
        }

        if ($this->meta) {
            $content = array_add($content, 'meta', $this->meta);
        }

        return response($content, $status);
    }
}
  1. 在 helpers 中增加函数:(或者直接写在这个异常类中,私有调用)
    该函数是获取 Symfony 定义的所有 Http 状态码。比如 200=HTTP_OK。
    function getHttpStatus()
    {
    $objClass = new \ReflectionClass(\Symfony\Component\HttpFoundation\Response::class);
    // 此处获取类中定义的全部常量 返回的是 [key=>value,...] 的数组,key是常量名,value是常量值
    return array_values($objClass->getConstants());
    }

    3. 基础使用

    baseException($data, int $code=200, array $meta=[]);
    第 1 个参数可以为 string 或 array.
    第 2 个参数默认为 200,如果传的 code 是任意一个 httpStatus,表示返回的 http 状态码(如 404、500 等),
    如果是自定义错误码(非任意一个 httpStatus,如 1001、1002),则 http 状态码返回 200,code 码在 json 内容中返回
    第 3 个参数默认为空数组。如果传第 3 个参数,将一起返回。

3.1 参数传 string

throw new BaseException('都是好孩子');

Status: 200 OK 
{
    "message": "都是好孩子"
}

3.2 参数传 string,code (自定义错误码,非 httpStatus)

throw new BaseException('都是好孩子',1001);

Status: 200 OK 
{
    "message": "都是好孩子",
    "code": 1001
}

3.3 参数传 string,code(httpStatus)

throw new BaseException('都是好孩子', 404);

Status: 404 Not Found
{
    "message": "都是好孩子"
}

3.4 参数传 array

throw new BaseException(['msg' => '都是好孩子', 'code' => '123']);

Status: 200 OK
{
    "msg": "都是好孩子",
    "code": "123"
}

3.5 参数传 array,code(httpStatus)

throw new BaseException(['msg' => '都是好孩子', 'code' => '123'], 403);

Status: 403 Forbidden
{
    "msg": "都是好孩子",
    "code": "123"
}

3.6 参数传 string,code(httpStatus),array

throw new BaseException('都是好孩子', 422, ['msg' => '天是蓝的', 'code' => '24678']);

Status: 422 Unprocessable Entity
{
    "message": "都是好孩子",
    "meta": {
        "msg": "天是蓝的",
        "code": "24678"
    }
}

3.7 参数传 string,code(自定义错误码,非 httpStatus),array

throw new BaseException('都是好孩子', 4567, ['msg' => '天是蓝的', 'code' => '24678']);

Status: 200 OK
{
    "message": "都是好孩子",
    "code": 4567,
    "meta": {
        "msg": "天是蓝的",
        "code":24678"  
    }
}

3.8 参数传 array,code(自定义错误码,非 httpStatus),array

throw new BaseException(['msg' => '都是好孩子', 'code' => '123'], 1234, ['msg' => '天是蓝的', 'code' => '24678']);

Status: 200 OK
{
    "msg": "都是好孩子",
    "code": "123",
    "meta": {
        "msg": "天是蓝的",
        "code": "24678"
    }
}

3.9 参数传 array,code(自定义错误码,非 httpStatus),array

throw new BaseException(['msg' => '都是好孩子', 'code' => '123'], 500, ['msg' => '天是蓝的', 'code' => '24678']);

Status: 500 Internal Server Error
{
    "msg": "都是好孩子",
    "code": "123",
    "meta": {
        "msg": "天是蓝的",
        "code": "24678"
    }
}
参数校验异常#

使用 laravel 内置的 ValidationException

1. 在 app/Exceptions/Handler 中添加

 public function render($request, Exception $exception)
    {
        //-------新加这4行---------
        if ($exception instanceof ValidationException) {
            $message = current(current(array_values($exception->errors())));
            throw new BaseException($message, 4022); // 不加4022,会返回httpStatus=422;加4022是因为返回前端统一httpStatus为200,就在422中加了0
        }
        //------------------------

        return parent::render($request, $exception);
    }

2. 基础使用

    //控制器中,不需要额外抛出异常
    public function index(Request $request)
    {
        Validator::make($request->all(), [
            'file' => 'bail|required|file'
        ], [
            'file.required' => '请上传文件'
        ])->validate();
    }

输出:

    //handler中不加4022
    Status: 422 Unprocessable Entity
    {
        "message": "请上传文件"
    }

    //handler中加4022
    Status: 200 OK
    {
        "message": "请上传文件",
        "code": 4022,
    }
处理其他异常#

同参数校验异常,如处理 FatalThrowableError (定义错误码为 5678),然后故意写个语法错误。
同样不需要自己抛错,也不会出现报错大黑框

Status: 200 OK
{
    "message": "Parse error: syntax error, unexpected 'dd' (T_STRING)",
    "code": 5678
}
CodeException#

为了项目统一规范,需统一管理 code 错误码,所以建立 CodeException。

  1. 在 app/Exceptions 下新建 error.php。返回错误信息数组。
    <?php
    return [
    1001 =>'门前大桥下',
    1002 =>'游过一群鸭'
    ];

2 . 在 helpers 中增加函数:
该函数是获取该 errorCode 相对应的 errorMessage。

function getErrorMessage($code)
{
    $err = require_once __DIR__.'/../app/Exceptions/error.php';
    return $err[$code];
}

3 . 在 app/Exceptions 下新建 CodeException

4 . 基础使用

throw new CodeException(1001);

// 返回
{
    "message": "门前大桥下",
    "code": 1001
}

有附加错误信息数组

throw new CodeException(1001,['info'=>'门前大桥下','text'=>'游过一群鸭']);

//返回
{
    "message": "门前大桥下",
    "code": 1001,
    "data": {
        "info": "门前大桥下",
        "text": "游过一群鸭"
    }
}

第二种 修改 Handler#

public function render($request, Exception $exception)
    {
        if ($exception instanceof BaseException
            || $exception instanceof HttpException
            || $exception instanceof ValidationException
            || $exception instanceof QueryException
            || $exception instanceof ModelNotFoundException
            || $exception instanceof ErrorException
        ) {
            return $this->error($exception);
        }

        return parent::render($request, $exception);
    }

    /**
     * 自定义错误输出
     *
     * @param $exception
     *
     * @return \Illuminate\Http\JsonResponse
     */
    private function error($exception)
    {
        $statusCode = 500;
        if ($exception instanceof ValidationException) {
            $code    = $exception->status;
            $message = current(current(array_values($exception->errors())));
        } else {
            $code    = $exception->getCode() ?: ($exception->statusCode ?? 404);
            $message = $exception->getMessage();
        }

        $response = [
            'id'      => md5(uniqid()),
            'code'    => $code,
            'status'  => $statusCode,
            'message' => $message,
            'error'   => 'ERROR',
        ];

        if ($exception instanceof BaseException && $exception->getData()) {
            $response['data'] = $exception->getData();
        }

        iuLog('error', 'Response Error: ', $response);
        iuLog(PHP_EOL);

        return response()->json($response, $statusCode, [], 320);
    }

这里也保留了 BaseException,但是写的要简单些,因为大部分逻辑写在 Handler 里面,这里只留了获取 StatusCode 和 获取数组信息。使用和上面一样。

namespace App\Exceptions;

use Exception;

class BaseException extends Exception
{
    protected $data;

    /**
     * Error constructor.
     *
     * @param string $message
     * @param int    $code
     * @param array  $data
     */
    public function __construct($message = "", $code = 500, $data = [])
    {
        $this->data = $data;
        parent::__construct($message, $code);
    }

    public function getStatusCode()
    {
        $objClass = new \ReflectionClass(\Symfony\Component\HttpFoundation\Response::class);
        // 此处获取类中定义的全部常量 返回的是 [key=>value,...] 的数组
        // key是常量名 value是常量值
        // dd($objClass->getConstants());
        $httpStatus = array_values($objClass->getConstants());

        return in_array($this->code, $httpStatus) ? $this->code : 500;
    }

    public function getData()
    {
        return $this->data;
    }
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 6年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 17
kinyou

认真的人最可爱

6年前 评论

学习了, 感谢

6年前 评论
guanhui07

返回的字段 信息 和我差不多

6年前 评论

你用的是 dingo / api 吗?我这里测试不成功。

6年前 评论

@十七岁程序员想当歌手 没有用 dingo,就是 laravel 直接 orm 查询返回 和使用 aipresoure 两种

6年前 评论

@十七岁程序员想当歌手 https://mikecoder.cn/post/145/ 把 ding/api 的异常改下就可以

6年前 评论
node

file 请修正该参数 哈哈

5年前 评论

哈哈哈,细心

5年前 评论

那个 第一种:BaseException 1. 里是不是少了个 protected $meta。。还有想请教一下就是在 xxxException 类的 render () 方法是怎么被调用的。。还像没看到有调用 render () 方法呀... 谢谢

5年前 评论
PHP-Coder 5年前

CodeException 的代码没贴出来耶

5年前 评论

file
这里的不加 4022 应该返回的 200 的状态码吧,这个不是你在 BaseException 里面 render 方法里面定义的吗?怎么会变成 422 默认的参数验证的状态码呢?

5年前 评论
aen233 (楼主) 5年前

我在想那些错误应该按照异常来处理,比如:代码错误、网络超时
还有比如:没有权限、活动结束、库存不足。这些应该是按照异常来处理,还是正常返回

5年前 评论

抛出异常的时候,http 状态码到底是 200 还是其他状态

5年前 评论
aen233 (楼主) 5年前
aen233 (楼主) 5年前
勇敢的心 (作者) 5年前
aen233 (楼主) 5年前
勇敢的心 (作者) 5年前
aen233 (楼主) 5年前
勇敢的心 (作者) 5年前
aen233 (楼主) 5年前
勇敢的心 (作者) 5年前
aen233 (楼主) 5年前
勇敢的心 (作者) 5年前

感觉这个异常类也可以用来返回正确的响应啊

5年前 评论

想法一致就来搜了,蓝后看完了所有文章发现是个宝藏程序员。不知有没有个人网站之类的,翻了一下你的 github 也没有找到,想交个朋友~~

5年前 评论
aen233 (楼主) 5年前

好像发现要给问题,如果使用了作者的自定义返回成功返回格式,和这里的异常统一处理。当 httpStatus 为 200 时,成功返回格式化会对异常返回做格式处理。需要过滤一下:

        /* @var Response $response */
        // 对状态码为 200 且包含异常的返回值兼容
        if($response->exception ?? false) {
            return $response;
        }
4年前 评论

php @ abc
文章
20
粉丝
94
喜欢
198
收藏
232
排名:106
访问:8.9 万
私信
所有博文
社区赞助商