Laravel Exceptions——異常與錯誤處理

leoyang發表於2017-08-13

前言

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis
對於一個優秀的框架來說,正確的異常處理可以防止暴露自身介面給使用者,可以提供快速追溯問題的提示給開發人員。本文會詳細的介紹 laravel 異常處理的原始碼。

 

PHP 異常處理

本章節參考 PHP錯誤異常處理詳解

異常處理(又稱為錯誤處理)功能提供了處理程式執行時出現的錯誤或異常情況的方法。
  
異常處理通常是防止未知錯誤產生所採取的處理措施。異常處理的好處是你不用再絞盡腦汁去考慮各種錯誤,這為處理某一類錯誤提供了一個很有效的方法,使程式設計效率大大提高。當異常被觸發時,通常會發生:

  • 當前程式碼狀態被儲存
  • 程式碼執行被切換到預定義的異常處理器函式
  • 根據情況,處理器也許會從儲存的程式碼狀態重新開始執行程式碼,終止指令碼執行,或從程式碼中另外的位置繼續執行指令碼

PHP 5 提供了一種新的物件導向的錯誤處理方法。可以使用檢測(try)、丟擲(throw)和捕獲(catch)異常。即使用try檢測有沒有丟擲(throw)異常,若有異常丟擲(throw),使用catch捕獲異常。

一個 try 至少要有一個與之對應的 catch。定義多個 catch 可以捕獲不同的物件。php 會按這些 catch 被定義的順序執行,直到完成最後一個為止。而在這些 catch 內,又可以丟擲新的異常。

異常的丟擲

當一個異常被丟擲時,其後的程式碼將不會繼續執行,PHP 會嘗試查詢匹配的 catch 程式碼塊。如果一個異常沒有被捕獲,而且又沒用使用set_exception_handler() 作相應的處理的話,那麼 PHP 將會產生一個嚴重的錯誤,並且輸出未能捕獲異常 (Uncaught Exception ... ) 的提示資訊。

丟擲異常,但不去捕獲它:

ini_set('display_errors', 'On');  
error_reporting(E_ALL & ~ E_WARNING);  
$error = 'Always throw this error';  
throw new Exception($error);  
// 繼續執行  
echo 'Hello World';  

上面的程式碼會獲得類似這樣的一個致命錯誤:

Fatal error: Uncaught exception 'Exception' with message 'Always throw this error' in E:\sngrep\index.php on line 5  
Exception: Always throw this error in E:\sngrep\index.php on line 5  
Call Stack:  
    0.0005     330680   1. {main}() E:\sngrep\index.php:0  

Try, throw 和 catch

要避免上面這個致命錯誤,可以使用try catch捕獲掉。

處理處理程式應當包括:

  • Try - 使用異常的函式應該位於 "try" 程式碼塊內。如果沒有觸發異常,則程式碼將照常繼續執行。但是如果異常被觸發,會丟擲一個異常。
  • Throw - 這裡規定如何觸發異常。每一個 "throw" 必須對應至少一個 "catch"
  • Catch - "catch" 程式碼塊會捕獲異常,並建立一個包含異常資訊的物件

丟擲異常並捕獲掉,可以繼續執行後面的程式碼:

try {  
    $error = 'Always throw this error';  
    throw new Exception($error);  

    // 從這裡開始,tra 程式碼塊內的程式碼將不會被執行  
    echo 'Never executed';  

} catch (Exception $e) {  
    echo 'Caught exception: ',  $e->getMessage(),'<br>';  
}  

// 繼續執行  
echo 'Hello World';  

頂層異常處理器 set_exception_handler

在我們實際開發中,異常捕捉僅僅靠 try {} catch () 是遠遠不夠的。set_exception_handler() 函式可設定處理所有未捕獲異常的使用者定義函式。

function myException($exception)  
{  
echo "<b>Exception:</b> " , $exception->getMessage();  
}  

set_exception_handler('myException');  
throw new Exception('Uncaught Exception occurred');

擴充套件 PHP 內建的異常處理類

使用者可以用自定義的異常處理類來擴充套件 PHP 內建的異常處理類。以下的程式碼說明了在內建的異常處理類中,哪些屬性和方法在子類中是可訪問和可繼承的。

class Exception  
{  
    protected $message = 'Unknown exception';   // 異常資訊  
    protected $code = 0;                        // 使用者自定義異常程式碼  
    protected $file;                            // 發生異常的檔名  
    protected $line;                            // 發生異常的程式碼行號  

    function __construct($message = null, $code = 0);  

    final function getMessage();                // 返回異常資訊  
    final function getCode();                   // 返回異常程式碼  
    final function getFile();                   // 返回發生異常的檔名  
    final function getLine();                   // 返回發生異常的程式碼行號  
    final function getTrace();                  // backtrace() 陣列  
    final function getTraceAsString();          // 已格成化成字串的 getTrace() 資訊  

    /* 可過載的方法 */  
    function __toString();                       // 可輸出的字串  
}  

如果使用自定義的類來擴充套件內建異常處理類,並且要重新定義建構函式的話,建議同時呼叫 parent::__construct() 來檢查所有的變數是否已被賦值。當物件要輸出字串的時候,可以過載 __toString() 並自定義輸出的樣式。

class MyException extends Exception  
{  
    // 重定義構造器使 message 變為必須被指定的屬性  
    public function __construct($message, $code = 0) {  
        // 自定義的程式碼  

        // 確保所有變數都被正確賦值  
        parent::__construct($message, $code);  
    }  

    // 自定義字串輸出的樣式 */  
    public function __toString() {  
        return __CLASS__ . ": [{$this->code}]: {$this->message}\n";  
    }  

    public function customFunction() {  
        echo "A Custom function for this type of exception\n";  
    }  
} 

MyException 類是作為舊的 exception 類的一個擴充套件來建立的。這樣它就繼承了舊類的所有屬性和方法,我們可以使用 exception 類的方法,比如 getLine()getFile() 以及 getMessage()

 

PHP 錯誤處理

PHP 的錯誤級別

常量 說明
1 E_ERROR 致命的執行時錯誤。這類錯誤一般是不可恢復的情況,例如記憶體分配導致的問題。後果是導致指令碼終止不再繼續執行。
2 E_WARNING 執行時警告 (非致命錯誤)。僅給出提示資訊,但是指令碼不會終止執行。
4 E_PARSE 編譯時語法解析錯誤。解析錯誤僅僅由分析器產生。
8 E_NOTICE 執行時通知。表示指令碼遇到可能會表現為錯誤的情況,但是在可以正常執行的指令碼里面也可能會有類似的通知。
16 E_CORE_ERROR 在PHP初始化啟動過程中發生的致命錯誤。該錯誤類似 E_ERROR,但是是由PHP引擎核心產生的。
32 E_CORE_WARNING PHP初始化啟動過程中發生的警告 (非致命錯誤) 。類似 E_WARNING,但是是由PHP引擎核心產生的。
64 E_COMPILE_ERROR 致命編譯時錯誤。類似E_ERROR, 但是是由Zend指令碼引擎產生的。
128 E_COMPILE_WARNING 編譯時警告 (非致命錯誤)。類似 E_WARNING,但是是由Zend指令碼引擎產生的。
256 E_USER_ERROR 使用者產生的錯誤資訊。類似 E_ERROR, 但是是由使用者自己在程式碼中使用PHP函式 trigger_error()來產生的。
512 E_USER_WARNING 使用者產生的警告資訊。類似 E_WARNING, 但是是由使用者自己在程式碼中使用PHP函式 trigger_error()來產生的。
1024 E_USER_NOTICE 使用者產生的通知資訊。類似 E_NOTICE, 但是是由使用者自己在程式碼中使用PHP函式 trigger_error()來產生的。
2048 E_STRICT 啟用 PHP 對程式碼的修改建議,以確保程式碼具有最佳的互操作性和向前相容性。
4096 E_RECOVERABLE_ERROR 可被捕捉的致命錯誤。 它表示發生了一個可能非常危險的錯誤,但是還沒有導致PHP引擎處於不穩定的狀態。 如果該錯誤沒有被使用者自定義控制程式碼捕獲 (參見 set_error_handler()),將成為一個 E_ERROR 從而指令碼會終止執行。
8192 E_DEPRECATED 執行時通知。啟用後將會對在未來版本中可能無法正常工作的程式碼給出警告。
16384 E_USER_DEPRECATED 使用者產少的警告資訊。 類似 E_DEPRECATED, 但是是由使用者自己在程式碼中使用PHP函式 trigger_error()來產生的。
30719 E_ALL 使用者產少的警告資訊。 類似 E_DEPRECATED, 但是是由使用者自己在程式碼中使用PHP函式 trigger_error()來產生的。

錯誤的丟擲

除了系統在執行 php 程式碼丟擲的意外錯誤。我們還可以利用 rigger_error 產生一個自定義的使用者級別的 error/warning/notice 錯誤資訊:

if ($divisor == 0) {
    trigger_error("Cannot divide by zero", E_USER_ERROR);
}

頂級錯誤處理器

頂級錯誤處理器 set_error_handler 一般用於捕捉 E_NOTICEE_USER_ERRORE_USER_WARNINGE_USER_NOTICE 級別的錯誤,不能捕捉 E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERRORE_COMPILE_WARNING

致命錯誤捕捉處理器register_shutdown_function

register_shutdown_function() 函式可實現當程式執行完成後執行的函式,其功能為可實現程式執行完成的後續操作。程式在執行的時候可能存在執行超時,或強制關閉等情況,但這種情況下預設的提示是非常不友好的,如果使用 register_shutdown_function() 函式捕獲異常,就能提供更加友好的錯誤展示方式,同時可以實現一些功能的後續操作,如執行完成後的臨時資料清理,包括臨時檔案等。

可以這樣理解呼叫條件:

  • 當頁面被使用者強制停止時
  • 當程式程式碼執行超時時
  • 當PHP程式碼執行完成時,程式碼執行存在異常和錯誤、警告

我們前面說過,set_error_handler 能夠捕捉的錯誤型別有限,很多致命錯誤例如解析錯誤等都無法捕捉,但是這類致命錯誤發生時,PHP 會呼叫 register_shutdown_function 所註冊的函式,如果結合函式 error_get_last,就會獲取錯誤發生的資訊。

 

Laravel 異常處理

laravel 的異常處理由類 \Illuminate\Foundation\Bootstrap\HandleExceptions::class 完成:

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');
        }
    }
}

異常轉化

laravel 的異常處理均由函式 handleException 負責。

PHP7 實現了一個全域性的 throwable 介面,原來的 Exception 和部分 Error 都實現了這個介面, 以介面的方式定義了異常的繼承結構。於是,PHP7 中更多的 Error 變為可捕獲的 Exception 返回給開發者,如果不進行捕獲則為 Error ,如果捕獲就變為一個可在程式內處理的 Exception。這些可被捕獲的 Error 通常都是不會對程式造成致命傷害的 Error,例如函式不存在。

PHP7 中,基於 /Error exception,派生了5個新的engine exception:ArithmeticError / AssertionError / DivisionByZeroError / ParseError / TypeError。在 PHP7 裡,無論是老的 /Exception 還是新的 /Error ,它們都實現了一個共同的interface: /Throwable

因此,遇到非 Exception 型別的異常,首先就要將其轉化為 FatalThrowableError 型別:

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);
    }
}

FatalThrowableErrorSymfony 繼承 \ErrorException 的錯誤異常類:

class FatalThrowableError extends FatalErrorException
{
    public function __construct(\Throwable $e)
    {
        if ($e instanceof \ParseError) {
            $message = 'Parse error: '.$e->getMessage();
            $severity = E_PARSE;
        } elseif ($e instanceof \TypeError) {
            $message = 'Type error: '.$e->getMessage();
            $severity = E_RECOVERABLE_ERROR;
        } else {
            $message = $e->getMessage();
            $severity = E_ERROR;
        }

        \ErrorException::__construct(
            $message,
            $e->getCode(),
            $severity,
            $e->getFile(),
            $e->getLine()
        );

        $this->setTrace($e->getTrace());
    }
}

異常 Log

當遇到異常情況的時候,laravel 首要做的事情就是記錄 log,這個就是 report 函式的作用。

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

laravelIoc 容器中預設的異常處理類是 Illuminate\Foundation\Exceptions\Handler:

class Handler implements ExceptionHandlerContract
{
    public function report(Exception $e)
    {
        if ($this->shouldntReport($e)) {
            return;
        }

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

        $logger->error($e);
    }

    protected function shouldntReport(Exception $e)
    {
        $dontReport = array_merge($this->dontReport, [HttpResponseException::class]);

        return ! is_null(collect($dontReport)->first(function ($type) use ($e) {
            return $e instanceof $type;
        }));
    }
}

異常頁面展示

記錄 log 後,就要將異常轉化為頁面向開發者展示異常的資訊,以便檢視問題的來源:

protected function renderHttpResponse(Exception $e)
{
    $this->getExceptionHandler()->render($this->app['request'], $e)->send();
}

class Handler implements ExceptionHandlerContract
{
    public function render($request, Exception $e)
    {
        $e = $this->prepareException($e);

        if ($e instanceof HttpResponseException) {
            return $e->getResponse();
        } elseif ($e instanceof AuthenticationException) {
            return $this->unauthenticated($request, $e);
        } elseif ($e instanceof ValidationException) {
            return $this->convertValidationExceptionToResponse($e, $request);
        }

        return $this->prepareResponse($request, $e);
    }
}

對於不同的異常,laravel 有不同的處理,大致有 HttpExceptionHttpResponseExceptionAuthorizationExceptionModelNotFoundExceptionAuthenticationExceptionValidationException。由於特定的不同異常帶有自身的不同需求,本文不會特別介紹。本文繼續介紹最普通的異常 HttpException 的處理:

protected function prepareResponse($request, Exception $e)
{
    if ($this->isHttpException($e)) {
        return $this->toIlluminateResponse($this->renderHttpException($e), $e);
    } else {
        return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
    }
}

protected function renderHttpException(HttpException $e)
{
    $status = $e->getStatusCode();

    view()->replaceNamespace('errors', [
        resource_path('views/errors'),
        __DIR__.'/views',
    ]);

    if (view()->exists("errors::{$status}")) {
        return response()->view("errors::{$status}", ['exception' => $e], $status, $e->getHeaders());
    } else {
        return $this->convertExceptionToResponse($e);
    }
}

對於 HttpException 來說,會根據其錯誤的狀態碼,選取不同的錯誤頁面模板,若不存在相關的模板,則會通過 SymfonyResponse 來構造異常展示頁面:

protected function convertExceptionToResponse(Exception $e)
{
    $e = FlattenException::create($e);

    $handler = new SymfonyExceptionHandler(config('app.debug'));

    return SymfonyResponse::create($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders());
}

protected function toIlluminateResponse($response, Exception $e)
{
    if ($response instanceof SymfonyRedirectResponse) {
        $response = new RedirectResponse($response->getTargetUrl(), $response->getStatusCode(), $response->headers->all());
    } else {
        $response = new Response($response->getContent(), $response->getStatusCode(), $response->headers->all());
    }

    return $response->withException($e);
}

 

laravel 錯誤處理

public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
    if (error_reporting() & $level) {
        throw new ErrorException($message, 0, $level, $file, $line);
    }
}

public function handleShutdown()
{
    if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
        $this->handleException($this->fatalExceptionFromError($error, 0));
    }
}

protected function fatalExceptionFromError(array $error, $traceOffset = null)
{
    return new FatalErrorException(
        $error['message'], $error['type'], 0, $error['file'], $error['line'], $traceOffset
    );
}

protected function isFatal($type)
{
    return in_array($type, [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]);
}

對於不致命的錯誤,例如 notice級別的錯誤,handleError 即可擷取, laravel 將錯誤轉化為了異常,交給了 handleException 去處理。

對於致命錯誤,例如 E_PARSE 解析錯誤,handleShutdown 將會啟動,並且判斷當前指令碼結束是否是由於致命錯誤,如果是致命錯誤,將會將其轉化為 FatalErrorException, 交給了 handleException 作為異常去處理。

相關文章