小白折騰伺服器(七):自定義介面錯誤響應格式

aen233發表於2019-03-04

上一篇是使用後置中介軟體自定義介面成功返回格式
這篇來寫處理異常,自定義介面錯誤返回格式

基礎小知識
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;
    }
}

相關文章