Laravel核心代碼學習--異常處理

異常處理是編程中十分重要但也最容易被人忽視的語言特性,它爲開發者提供了處理程序運行時錯誤的機制,對於程序設計來講正確的異常處理可以防止泄露程序自身細節給用戶,給開發者提供完整的錯誤回溯堆棧,同時也能提升程序的健壯性。php

這篇文章咱們來簡單梳理一下Laravel中提供的異常處理能力,而後講一些在開發中使用異常處理的實踐,如何使用自定義異常、如何擴展Laravel的異常處理能力。laravel

註冊異常Handler

這裏又要回到咱們說過不少次的Kernel處理請求前的bootstrap階段,在bootstrap階段的Illuminate\Foundation\Bootstrap\HandleExceptions 部分中Laravel設置了系統異常處理行爲並註冊了全局的異常處理器:git

class HandleExceptions
{
    public function bootstrap(Application $app)
    {
        $this->app = $app;

        error_reporting(-1);

        set_error_handler([$this, 'handleError']);

        set_exception_handler([$this, 'handleException']);

        register_shutdown_function([$this, 'handleShutdown']);

        if (! $app->environment('testing')) {
            ini_set('display_errors', 'Off');
        }
    }
    
    
    public function handleError($level, $message, $file = '', $line = 0, $context = [])
    {
        if (error_reporting() & $level) {
            throw new ErrorException($message, 0, $level, $file, $line);
        }
    }
}
複製代碼

set_exception_handler([$this, 'handleException'])HandleExceptionshandleException方法註冊爲程序的全局處理器方法:github

public function handleException($e)
{
    if (! $e instanceof Exception) {
    	$e = new FatalThrowableError($e);
    }

    $this->getExceptionHandler()->report($e);

    if ($this->app->runningInConsole()) {
    	$this->renderForConsole($e);
    } else {
    	$this->renderHttpResponse($e);
    }
}

protected function getExceptionHandler()
{
	return $this->app->make(ExceptionHandler::class);
}

// 渲染CLI請求的異常響應
protected function renderForConsole(Exception $e)
{
	$this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e);
}

// 渲染HTTP請求的異常響應
protected function renderHttpResponse(Exception $e)
{
	$this->getExceptionHandler()->render($this->app['request'], $e)->send();
}
複製代碼

在處理器裏主要經過ExceptionHandlerreport方法上報異常、這裏是記錄異常到storage/laravel.log文件中,而後根據請求類型渲染異常的響應生成輸出給到客戶端。這裏的ExceptionHandler就是\App\Exceptions\Handler類的實例,它是在項目最開始註冊到服務容器中的:數據庫

// bootstrap/app.php

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
*/

$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
*/
......

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

複製代碼

這裏再順便說一下set_error_handler函數,它的做用是註冊錯誤處理器函數,由於在一些年代久遠的代碼或者類庫中大可能是採用PHP那件函數trigger_error函數來拋出錯誤的,異常處理器只能處理Exception不能處理Error,因此爲了可以兼容老類庫一般都會使用set_error_handler註冊全局的錯誤處理器方法,在方法中捕獲到錯誤後將錯誤轉化成異常再從新拋出,這樣項目中全部的代碼沒有被正確執行時都能拋出異常實例了。編程

/**
 * Convert PHP errors to ErrorException instances.
 *
 * @param  int  $level
 * @param  string  $message
 * @param  string  $file
 * @param  int  $line
 * @param  array  $context
 * @return void
 *
 * @throws \ErrorException
 */
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
    if (error_reporting() & $level) {
        throw new ErrorException($message, 0, $level, $file, $line);
    }
}
複製代碼

經常使用的Laravel異常實例

Laravel中針對常見的程序異常狀況拋出了相應的異常實例,這讓開發者可以捕獲這些運行時異常並根據本身的須要來作後續處理(好比:在catch中調用另一個補救方法、記錄異常到日誌文件、發送報警郵件、短信)bootstrap

在這裏我列一些開發中常遇到異常,並說明他們是在什麼狀況下被拋出的,平時編碼中必定要注意在程序裏捕獲這些異常作好異常處理才能讓程序更健壯。數組

  • Illuminate\Database\QueryException Laravel中執行SQL語句發生錯誤時會拋出此異常,它也是使用率最高的異常,用來捕獲SQL執行錯誤,比方執行Update語句時不少人喜歡判斷SQL執行後判斷被修改的行數來判斷UPDATE是否成功,但有的情景裏執行的UPDATE語句並無修改記錄值,這種狀況就無法經過被修改函數來判斷UPDATE是否成功了,另外在事務執行中若是捕獲到QueryException 能夠在catch代碼塊中回滾事務。
  • Illuminate\Database\Eloquent\ModelNotFoundException 經過模型的findOrFailfirstOrFail方法獲取單條記錄時若是沒有找到會拋出這個異常(findfirst找不到數據時會返回NULL)。
  • Illuminate\Validation\ValidationException 請求未經過Laravel的FormValidator驗證時會拋出此異常。
  • Illuminate\Auth\Access\AuthorizationException 用戶請求未經過Laravel的策略(Policy)驗證時拋出此異常
  • Symfony\Component\Routing\Exception\MethodNotAllowedException 請求路由時HTTP Method不正確
  • Illuminate\Http\Exceptions\HttpResponseException Laravel的處理HTTP請求不成功時拋出此異常

擴展Laravel的異常處理器

上面說了Laravel把\App\Exceptions\Handler 註冊成功了全局的異常處理器,代碼中沒有被catch到的異常,最後都會被\App\Exceptions\Handler捕獲到,處理器先上報異常記錄到日誌文件裏而後渲染異常響應再發送響應給客戶端。可是自帶的異常處理器的方法並很差用,不少時候咱們想把異常上報到郵件或者是錯誤日誌系統中,下面的例子是將異常上報到Sentry系統中,Sentry是一個錯誤收集服務很是好用:bash

public function report(Exception $exception)
{
    if (app()->bound('sentry') && $this->shouldReport($exception)) {
        app('sentry')->captureException($exception);
    }

    parent::report($exception);
}
複製代碼

還有默認的渲染方法在表單驗證時生成響應的JSON格式每每跟咱們項目裏統一的JOSN格式不同這就須要咱們自定義渲染方法的行爲。app

public function render($request, Exception $exception)
{
    //若是客戶端預期的是JSON響應,  在API請求未經過Validator驗證拋出ValidationException後
    //這裏來定製返回給客戶端的響應.
    if ($exception instanceof ValidationException && $request->expectsJson()) {
        return $this->error(422, $exception->errors());
    }

    if ($exception instanceof ModelNotFoundException && $request->expectsJson()) {
        //捕獲路由模型綁定在數據庫中找不到模型後拋出的NotFoundHttpException
        return $this->error(424, 'resource not found.');
    }


    if ($exception instanceof AuthorizationException) {
        //捕獲不符合權限時拋出的 AuthorizationException
        return $this->error(403, "Permission does not exist.");
    }

    return parent::render($request, $exception);
}
複製代碼

自定義後,在請求未經過FormValidator驗證時會拋出ValidationException, 以後異常處理器捕獲到異常後會把錯誤提示格式化爲項目統一的JSON響應格式並輸出給客戶端。這樣在咱們的控制器中就徹底省略了判斷表單驗證是否經過若是不經過再輸出錯誤響應給客戶端的邏輯了,將這部分邏輯交給了統一的異常處理器來執行能讓控制器方法瘦身很多。

使用自定義異常

這部份內容其實不是針對Laravel框架自定義異常,在任何項目中均可以應用我這裏說的自定義異常。

我見過不少人在Repository或者Service類的方法中會根據不一樣錯誤返回不一樣的數組,裏面包含着響應的錯誤碼和錯誤信息,這麼作固然是能夠知足開發需求的,可是並不能記錄發生異常時的應用的運行時上下文,發生錯誤時沒辦法記錄到上下文信息就很是不利於開發者進行問題定位。

下面的是一個自定義的異常類

namespace App\Exceptions\;

use RuntimeException;
use Throwable;

class UserManageException extends RuntimeException
{
    /**
     * The primitive arguments that triggered this exception
     *
     * @var array
     */
    public $primitives;
    /**
     * QueueManageException constructor.
     * @param array $primitives
     * @param string $message
     * @param int $code
     * @param Throwable|null $previous
     */
    public function __construct(array $primitives, $message = "", $code = 0, Throwable $previous = null)
    {
        parent::__construct($message, $code, $previous);
        $this->primitives = $primitives;
    }

    /**
     * get the primitive arguments that triggered this exception
     */
    public function getPrimitives()
    {
        return $this->primitives;
    }
}
複製代碼

定義完異常類咱們就能在代碼邏輯中拋出異常實例了

class UserRepository
{
  
    public function updateUserFavorites(User $user, $favoriteData)
    {
        ......
    	if (!$executionOne) {
            throw new UserManageException(func_get_args(), 'Update user favorites error', '501');
    	}
    	
    	......
    	if (!$executionTwo) {
            throw new UserManageException(func_get_args(), 'Another Error', '502');
    	}
    	
    	return true;
    }
}

class UserController extends ...
{
    public function updateFavorites(User $user, Request $request)
    {
    	.......
    	$favoriteData = $request->input('favorites');
        try {
            $this->userRepo->updateUserFavorites($user, $favoritesData);
        } catch (UserManageException $ex) {
            .......
        }
    }
}
複製代碼

除了上面Repository列出的狀況更多的時候咱們是在捕獲到上面列舉的通用異常後在catch代碼塊中拋出與業務相關的更細化的異常實例方便開發者定位問題,咱們將上面的updateUserFavorites 按照這種策略修改一下

public function updateUserFavorites(User $user, $favoriteData)
{
    try {
        // database execution
        
        // database execution
    } catch (QueryException $queryException) {
        throw new UserManageException(func_get_args(), 'Error Message', '501' , $queryException);
    }

	return true;
}
複製代碼

在上面定義UserMangeException類的時候第四個參數$previous是一個實現了Throwable接口類實例,在這種情景下咱們由於捕獲到了QueryException的異常實例而拋出了UserManagerException的實例,而後經過這個參數將QueryException實例傳遞給PHP異常的堆棧,這提供給咱們回溯整個異常的能力來獲取更多上下文信息,而不是僅僅只是當前拋出的異常實例的上下文信息, 在錯誤收集系統可使用相似下面的代碼來獲取全部異常的信息。

while($e instanceof \Exception) {
    echo $e->getMessage();
    $e = $e->getPrevious();
}
複製代碼

異常處理是PHP很是重要但又容易讓開發者忽略的功能,這篇文章簡單解釋了Laravel內部異常處理的機制以及擴展Laravel異常處理的方式方法。更多的篇幅着重分享了一些異常處理的編程實踐,這些正是我但願每一個讀者都能看明白並實踐下去的一些編程習慣,包括以前分享的Interface的應用也是同樣。

本文已經整理髮布到系列文章Laravel核心代碼學習中,歡迎訪問閱讀,多多交流。

相關文章
相關標籤/搜索