Laravel 源碼核心解讀:全局異常處理

Laravel 爲用戶提供了一個基礎的全局異常攔截處理器App\Exceptions\Hander。若是沒有全局的異常錯誤攔截器,那咱們在每一個可能發生錯誤異常的業務邏輯分支中,都要使用 try ... catch,而後將執行結果返回 Controller層,再由其根據結果來構造相應的 Response,那代碼冗餘的會至關能夠。php

全局異常錯誤處理,是每一個框架都應該具有的,此次咱們就經過簡析 Laravel 的源碼和執行流程,來看一下此模式是如何被運用的。laravel

源碼解析

laravel/laravel 腳手架中有一個預約義好的異常處理器:web

app/Exceptions/Handler.phpjson

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    // 不被處理的異常錯誤
    protected $dontReport = [];

    // 認證異常時不被flashed的數據
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];
    // 上報異常至錯誤driver,如日誌文件(storage/logs/laravel.log),第三方日誌存儲分析平臺
    public function report(Exception $exception)
    {
        parent::report($exception);
    }
    // 將異常信息響應給客戶端
    public function render($request, Exception $exception)
    {
        return parent::render($request, $exception);
    }
}

當 Laravel 處理一次請求時,在啓動文件中註冊瞭如下服務:
bootstrap/app.phpbootstrap

// 綁定 http 服務提供者
$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);
// 綁定 cli 服務提供者
$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);
// 這裏將異常處理器的服務提供者綁定到了 `App\Exceptions\Handler::class`
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

然後進入請求捕獲,處理階段:
public/index.phpapp

// 使用 http 服務處理請求
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
// http 服務處理捕獲的請求 $requeset
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
Illuminate\Contracts\Http\Kernel::class 具體提供者是 App\Http\Kernel::class 繼承至 Illuminate\Foundation\Http\Kernel::class,咱們去其中看 http 服務 的 handle 方法是如何處理請求的。

請求處理階段:
Illuminate\Foundation\Http\Kernel::classhandle 方法對請求作一次處理,若是沒有異常則分發路由,若是有異常則調用 reportExceptionrenderException 方法記錄&渲染異常。框架

具體處理者則是咱們在 bootstrap/app.php 中註冊綁定的異常處理服務 Illuminate\Contracts\Debug\ExceptionHandler::classreport & render,具體的服務即綁定的 App\Exceptions\Handler::classide

public function handle($request)
{
    try {
        // 沒有異常 則進入路由分發
        $request->enableHttpMethodParameterOverride();
        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        // 捕獲異常 則 report & render
        $this->reportException($e);
        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));
        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}

//Report the exception to the exception handler.
protected function reportException(Exception $e)
{
    // 服務`Illuminate\Contracts\Debug\ExceptionHandler::class` 的 report 方法
    $this->app[ExceptionHandler::class]->report($e);
}
//Render the exception to a response.
protected function renderException($request, Exception $e)
{
    // 服務`Illuminate\Contracts\Debug\ExceptionHandler::class` 的 render 方法
    return $this->app[ExceptionHandler::class]->render($request, $e);
}

handler 方法做爲請求處理的入口,後續的路由分發,用戶業務調用(controller, model)等執行的上下文依然在此方法中,故異常也能在這一層被捕獲。函數

而後咱們就能夠在業務中經過 throw new CustomException($code, "錯誤異常描述"); 的方式將控制流程轉交給全局異常處理器,由其解析異常並構建響應實體給客戶端,這一模式在 Api服務 的開發中是效率極高的。網站

laravel 的依賴中有 symfony 這個超級棒的組件庫,symfony 爲咱們提供了詳細的 Http 異常庫,咱們能夠直接借用這些異常類(固然也能夠自定義)

clipboard.png

laravel 有提供 abort 助手函數來實現建立一個異常錯誤,但主要面向 web 網站(由於laravel主要就是用來開發後臺的嘛)的,對 Api
不太友好,並且看源碼發現只顧及了 404 這貨。

/**
 * abort(401, "你須要登陸")
 * abort(403, "你登陸了也白搭")
 * abort(404, "頁面找不到了")
 * Throw an HttpException with the given data.
 *
 * @param  int     $code
 * @param  string  $message
 * @param  array   $headers
 * @return void
 *
 * @throws \Symfony\Component\HttpKernel\Exception\HttpException
 */
public function abort($code, $message = '', array $headers = [])
{
    if ($code == 404) {
        throw new NotFoundHttpException($message);
    }

    throw new HttpException($code, $message, null, $headers);
}

即只有 404 用了具體的異常類去拋出,其餘的狀態碼都一股腦的歸爲 HttpException,這樣就不太方便咱們在全局異常處理器的 render 中根據 Exception 的具體類型來分而治之了,但 abort 也的確是爲了方便你調用具體的錯誤頁面的 resources/views/errors/{statusCode.blade.php} 的,須要對 Api 友好本身改寫吧。

使用場景

// 業務代碼 不知足直接拋出異常便可
if ("" = trim($username)) {
    throw new BadRequestHttpException("用戶名必須");
}
// 全局處理器
public function render($request, Exception $exception)
{
    if ($exception instanceof BadRequestHttpException) {
        return response()->json([
            "err" => 400,
            "msg" => $exception->getMessage()
        ]);
    }
    
    if ($exception instanceof AccessDeniedHttpException) {
        return response()->json([
            "err" => 403,
            "msg" => "unauthorized"
        ]);
    }
    
    if ($exception instanceof NotFoundHttpException) {
        return response()->json([
            "err" => 403,
            "msg" => "forbidden"
        ]);
    }
    
    if ($exception instanceof NotFoundHttpException) {
        return response()->json([
            "err" => 404,
            "msg" => "not found"
        ]);
    }
    
    if ($exception instanceof MethodNotAllowedHttpException) {
        return response()->json([
            "err" => 405,
            "msg" => "method not allowed"
        ]);
    }
    
    if ($exception instanceof MethodNotAllowedHttpException) {
        return response()->json([
            "err" => 406,
            "msg" => "你想要的數據類型我特麼給不了啊"
        ]);
    }
    
    if ($exception instanceof TooManyRequestsHttpException) {
        return response()->json([
            "err" => 429,
            "msg" => "to many request"
        ]);
    }
    
    return parent::render($request, $exception);
}
相關文章
相關標籤/搜索