前言
本文 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_NOTICE
、E_USER_ERROR
、E_USER_WARNING
、E_USER_NOTICE
級別的錯誤,不能捕捉 E_ERROR
, E_PARSE
, E_CORE_ERROR
, E_CORE_WARNING
, E_COMPILE_ERROR
和E_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);
}
}
FatalThrowableError
是 Symfony
繼承 \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);
}
laravel
在 Ioc
容器中預設的異常處理類是 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
有不同的處理,大致有 HttpException
、HttpResponseException
、AuthorizationException
、ModelNotFoundException
、AuthenticationException
、ValidationException
。由於特定的不同異常帶有自身的不同需求,本文不會特別介紹。本文繼續介紹最普通的異常 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
作為異常去處理。