重寫Laravel異常處理類

如今開發先後端分離變得愈來愈流行了,後端只提供接口返回json格式的數據,即便是錯誤信息也要以json格式來返回,然而目前不管是Laravel框架仍是ThinkPHP框架,都只提供了返回json數據的方法,對異常的處理並非以json格式來返回給咱們,因此這裏就須要咱們本身來改寫。php

首先咱們在app/Exceptions目錄新建一個ExceptionHandler.php繼承自Handler.phplaravel

namespace App\Exceptions;


class ExceptionHandler extends Handler
{

}

而後咱們在bootstrap/app.php中,使用咱們自定義的異常處理類ExceptionHandler替換掉默認的Handler類shell

//改成咱們自定義的ExceptionHandler類
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\ExceptionHandler::class
);

接下來咱們就開始重寫渲染方法json

在render方法裏,咱們根據.env文件中的APP_DEBUG來判斷,若是是調試模式,咱們仍是按照默認方式來渲染錯誤,若是是非調試模式,咱們就返回JSON格式的信息bootstrap

namespace App\Exceptions;

use Exception;

class ExceptionHandler extends Handler
{
    public function render($request, Exception $exception)
    {
        if (env('APP_DEBUG')) {
            return parent::render($request, $exception);
        }
        return response()->json([
            'code' => $exception->getCode(),
            'msg'  => $exception->getMessage()
        ]);
    }
}

這樣咱們就能夠根據APP_DEBUG的值設置是否返回JSON格式的數據了,如今咱們把.env的APP_DEBUG的值設爲false來測試一下,而後咱們故意把代碼寫錯,經過postman或瀏覽器來訪問接口後端

Route::get('/', function () {
    //這是一段缺乏了分號的代碼,會報異常
    echo 'Hello World!'
});

在APP_DEBUG=true的狀況下還仍然是默認渲染,方便咱們查找錯誤排錯瀏覽器

異常類默認會把異常以日誌的形式記錄在storage/logs目錄下,而且以laravel-日期(YYYY-MM-DD)命名的形式,.log爲後綴保存錯誤日誌服務器

咱們打開這個日誌文件查看記錄的錯誤信息,咱們能夠發現錯誤信息記錄的很是詳細,除了錯誤說明以外,還記錄了調用棧,以下圖所示app

基本上紅框裏的信息就夠咱們排錯了,不須要像如今這樣記錄的這麼詳細,因此要想不記錄調用棧,咱們能夠重寫report方法框架

首先咱們看一下框架的report方法,代碼在(src/Illuminate/Foundation/Exceptions/Handler.php),我用紅框框起來的代碼就是調用棧信息,咱們在重寫這個方法時只須要徹底拷貝這個方法裏的全部代碼到咱們自定義的report方法裏,而後把紅框裏的代碼去掉便可

咱們在咱們自定義的異常處理類ExceptionHandler.php中重寫report方法

public function report(Exception $exception)
{
    if ($this->shouldntReport($exception)) {
        return;
    }

    if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
        return $this->container->call($reportCallable);
    }

    try {
        $logger = $this->container->make(LoggerInterface::class);
    } catch (Exception $ex) {
        throw $exception;
    }

    $logger->error(
        $exception->getMessage()
    );
}

而後咱們再從新請求一下接口再去查看錯誤日誌的記錄,能夠發現確實沒有記錄調用棧信息了,可是下面的信息仍是不夠,咱們無法根據下面的信息判斷錯誤發生在哪個文件和哪一行,若是能在記錄錯誤信息的時候同時記錄發生錯誤的文件和行就更好了,因此藉着修改report方法

public function report(Exception $exception)
{
    if ($this->shouldntReport($exception)) {
        return;
    }

    if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
        return $this->container->call($reportCallable);
    }

    try {
        $logger = $this->container->make(LoggerInterface::class);
    } catch (Exception $ex) {
        throw $exception;
    }

    $logger->error(
        $exception->getMessage()." at ".$exception->getFile().":".$exception->getLine()
    );
}

在代碼裏我經過exception的getFile()、getLine()方法加上了文件和行數,保存代碼再次訪問接口,查看錯誤日誌文件咱們能夠看到發生錯誤的文件和行數已經記錄下來了,有了這些信息基本咱們就能夠找到錯誤

截止到這裏實現最初的需求咱們的ExceptionHandler.php只須要有這些代碼

namespace App\Exceptions;


use Exception;
use Illuminate\Support\Reflector;
use Psr\Log\LoggerInterface;

class ExceptionHandler extends Handler
{

    public function render($request, Exception $exception)
    {
        if (env('APP_DEBUG')) {
            return parent::render($request, $exception);
        }
        return response()->json([
            'code' => $exception->getCode(),
            'msg'  => $exception->getMessage()
        ]);
    }

    public function report(Exception $exception)
    {
        if ($this->shouldntReport($exception)) {
            return;
        }

        if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
            return $this->container->call($reportCallable);
        }

        try {
            $logger = $this->container->make(LoggerInterface::class);
        } catch (Exception $ex) {
            throw $exception;
        }

        $logger->error(
            $exception->getMessage()." at ".$exception->getFile().":".$exception->getLine()
        );
    }
}

而後還不夠,咱們發現剛剛咱們把服務器端的錯誤信息以JSON格式返回給客戶端了,這是不容許的,咱們應該只把一些客戶端錯誤返回給客戶端,好比密碼不足六位、身份證不合法諸如此類,而服務端出現錯誤時咱們只返回給客戶端一個模糊的信息便可,好比「服務器錯誤」,把真實的服務器錯誤信息記錄在日誌裏面方便開發人員排查錯誤

因此咱們須要定義一個客戶端異常專門用戶返回客戶端錯誤,使用以下命令在app/Exceptions目錄下生成一個ClientException.php文件

php artisan make:exception ClientException

修改成構造方法爲以下代碼

namespace App\Exceptions;

use Exception;

class ClientException extends Exception
{
    public function __construct($code, $msg)
    {
        parent::__construct($msg, $code);
    }
}

接着咱們繼續修改ExceptionHandler.php

namespace App\Exceptions;


use Exception;
use Illuminate\Support\Reflector;
use Psr\Log\LoggerInterface;

class ExceptionHandler extends Handler
{
    /**
     * @var int 錯誤碼
     */
    protected $code;
    /**
     * @var string 錯誤信息
     */
    protected $message;

    protected $dontReport = [
        ClientException::class
    ];

    public function render($request, Exception $exception)
    {
        if ($exception instanceof ClientException) {
            $this->code = $exception->getCode();
            $this->message = $exception->getMessage();
        } else {
            if (env('APP_DEBUG')) {
                return parent::render($request, $exception);
            }
            
            $this->code = 500;
            $this->message = '服務器錯誤';
        }
        
        return response()->json([
            'code' => $this->code,
            'msg'  => $this->message
        ]);
    }

    public function report(Exception $exception)
    {
        if ($this->shouldntReport($exception)) {
            return;
        }

        if (Reflector::isCallable($reportCallable = [$exception, 'report'])) {
            return $this->container->call($reportCallable);
        }

        try {
            $logger = $this->container->make(LoggerInterface::class);
        } catch (Exception $ex) {
            throw $exception;
        }

        $logger->error(
            $exception->getMessage()." at ".$exception->getFile().":".$exception->getLine()
        );
    }
}

對於上面的修改作一下說明,laravel的$dontReport屬性的異常類都不會被上報,由於客戶端錯誤信息咱們不須要記錄,因此將其添加到$dontReport屬性裏,而且在render方法裏把異常大概分爲了兩大類,一大類就是客戶端異常,另外一大類就是服務器異常,咱們把服務器異常統一code爲500,錯誤信息爲服務器錯誤,將真實的錯誤信息記錄在了錯誤日誌裏,避免把服務器信息暴露給了客戶端。

如今咱們來測試咱們重寫異常的結果

假如咱們想返回客戶端異常,好比沒有權限,這類客戶端異常在錯誤日誌裏都不會產生記錄,咱們自己也不須要記錄

Route::get('/', function () {
    throw new \App\Exceptions\ClientException(403, '你沒有權限');
});

對於服務器端的錯誤,如少些了分號,客戶端就只會知道服務器的某個接口出了問題,可是不清楚具體問題是什麼

Route::get('/', function () {
    echo 'Hello World!'
});

可是真實的錯誤信息會記錄在錯誤日誌裏,咱們仍舊能夠經過錯誤日誌來修改咱們服務端的錯誤

咱們還能夠在render方法中加入告警代碼,若是是服務端錯誤就給管理員發送郵件。

至此,咱們的重寫Laravel異常處理類就算完成啦,但願對正在準備使用Laravel作先後端分離項目的你有所幫助。

相關文章
相關標籤/搜索