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

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

基础小知识
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 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 17
kinyou

认真的人最可爱

5年前 评论

@kinyou :relaxed:蟹蟹呀

5年前 评论

学习了, 感谢

5年前 评论
guanhui07

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

5年前 评论

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

5年前 评论

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

5年前 评论

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

5年前 评论
node

file 请修正该参数 哈哈

4年前 评论

哈哈哈,细心

4年前 评论

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

4年前 评论
PHP-Coder 4年前

CodeException 的代码没贴出来耶

4年前 评论

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

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

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

4年前 评论

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

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

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

4年前 评论

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

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

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

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

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
php @ abc
文章
20
粉丝
94
喜欢
197
收藏
231
排名:107
访问:8.9 万
私信
所有博文
社区赞助商