錯誤處理
簡介
在開始新的 Laravel 專案時,Laravel 已經先幫你設定好錯誤與 Exception Handler。在你的專案中擲回的所有 Exception 都會由 App\Exceptions\Handler
負責紀錄 Log 並轉譯給使用者。我們會在這篇說明文件中深入瞭解這個類別。
設定
config/app.php
設定檔中的 debug
選項用來判斷錯誤在實際顯示給使用者時要包含多少資訊。預設情況下,這個選項被設為依照 APP_DEBUG
環境變數值,該環境變數儲存於 .env
檔內。
在本機上開發時,應將 APP_DEBUG
環境變數設為 true
。 在正式環境上,這個值一定要是 false
。若在正式環境上將該值設為 true
,則會有將機敏設定值暴露給應用程式終端使用者的風險。
Exception Handler
回報 Exception
所有的 Exception 都由 App\Exceptions\Handler
類別負責處理。該類別中包含了一個 register
方法,可用來註冊所有自訂的 Exception 回報與轉譯回呼。我們來詳細看看其中各個概念。「回報 Exception」就是指將例外紀錄到 Log,或是傳送到如 Flare、Bugsnag、Sentry⋯⋯等外部服務。預設情況下,Laravel 會使用專案的Log 設定來紀錄 Exception。不過,我們也可以隨意調整 Exception 要如何紀錄。
舉例來說,如果想以不同的方式回報不同類型的 Exception,可以使用 reportable
方法來註冊一個閉包。這個閉包會在給定類型的 Exception 需要回報時被呼叫。Laravel 會自動使用該閉包的型別提示來推導該閉包接受什麼類型的 Exception:
1use App\Exceptions\InvalidOrderException;23/**4 * Register the exception handling callbacks for the application.5 *6 * @return void7 */8public function register()9{10 $this->reportable(function (InvalidOrderException $e) {11 //12 });13}
1use App\Exceptions\InvalidOrderException;23/**4 * Register the exception handling callbacks for the application.5 *6 * @return void7 */8public function register()9{10 $this->reportable(function (InvalidOrderException $e) {11 //12 });13}
使用 reportable
方法定義自訂的 Exception 回報回呼時,Laravel 還是會使用專案的預設 Log 設定來紀錄例外。若想停止將 Exception 傳播給預設的日誌 Stack,請在定義回報回呼時使用 stop
方法,或是在該回呼內回傳 false
:
1$this->reportable(function (InvalidOrderException $e) {2 //3})->stop();45$this->reportable(function (InvalidOrderException $e) {6 return false;7});
1$this->reportable(function (InvalidOrderException $e) {2 //3})->stop();45$this->reportable(function (InvalidOrderException $e) {6 return false;7});
若要為給定的例外自訂 Exception 回報,可使用 Reportable 的例外。
全域 Log 上下文
當有目前使用者 ID 的時候,Laravel 會自動將使用者 ID 加到所有的例外 Log 訊息,以作為上下文資料。可以複寫專案中 App\Exceptions\Handler
類別的 context
來定義你自己的全域上下文資料。這個資料會被包含在專案輸出的所有例外 Log 訊息中:
1/**2 * Get the default context variables for logging.3 *4 * @return array5 */6protected function context()7{8 return array_merge(parent::context(), [9 'foo' => 'bar',10 ]);11}
1/**2 * Get the default context variables for logging.3 *4 * @return array5 */6protected function context()7{8 return array_merge(parent::context(), [9 'foo' => 'bar',10 ]);11}
Exception Log 的上下文
為所有 Log 訊息都新增額外的上下文可能會很實用,但有些特別的 Exception 可能會有一些獨特的上下文,而我們也想將這類上下文加到 Log 上。只要在我們的其中一個自訂 Exception 中定義一個 context
方法,就可以指定與該 Exception 相關的資料,將這些資料包含到例外的 Log 中:
1<?php23namespace App\Exceptions;45use Exception;67class InvalidOrderException extends Exception8{9 // ...1011 /**12 * Get the exception's context information.13 *14 * @return array15 */16 public function context()17 {18 return ['order_id' => $this->orderId];19 }20}
1<?php23namespace App\Exceptions;45use Exception;67class InvalidOrderException extends Exception8{9 // ...1011 /**12 * Get the exception's context information.13 *14 * @return array15 */16 public function context()17 {18 return ['order_id' => $this->orderId];19 }20}
report
輔助函式
有時候,我們可能會想回報某個 Exception,但又想繼續執行目前的 Request。使用 report
輔助函式,就能輕鬆地在不轉譯出錯誤頁面的情況下使用 Exception Handler 來回報這個 Exception:
1public function isValid($value)2{3 try {4 // Validate the value...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
1public function isValid($value)2{3 try {4 // Validate the value...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
以類型忽略例外
在製作專案時,我們可能會想忽略一些類型的 Exception,讓這些 Exception 永遠不要被回報。在專案中的 Exception Handler 中包含了一個 $dontReport
屬性,該屬性被初始化為空陣列。只要將任何類別加到該屬性中,這些類別就不會被回報。不過,還是可以為這些類別定義自訂的轉譯邏輯:
1use App\Exceptions\InvalidOrderException;23/**4 * A list of the exception types that should not be reported.5 *6 * @var array7 */8protected $dontReport = [9 InvalidOrderException::class,10];
1use App\Exceptions\InvalidOrderException;23/**4 * A list of the exception types that should not be reported.5 *6 * @var array7 */8protected $dontReport = [9 InvalidOrderException::class,10];
在 Laravel 內部,Laravel 已經預先幫你忽略了一些類型的錯誤。如:產生 404 HTTP「找不到」錯誤的 Exception、還有因為無效 CSRF Token 產生的 419 HTTP Response。
轉譯 Exception
預設情況下,Laravel 的 Exception Handler 會幫你把 Exception 轉成 HTTP Response。不過,我們也可以自由地為某個類型的 Exception 註冊自訂轉譯閉包。只要使用 Exception Handler 的 renderable
方法,就註冊轉譯閉包。
傳給 renderable
方法的閉包應回傳一個 Illuminate\Http\Response
的實體。可以使用 response
輔助函式來產生該實體。Laravel 會依照該閉包的型別提示來判斷這個閉包能轉移哪種類型的 Exception:
1use App\Exceptions\InvalidOrderException;23/**4 * Register the exception handling callbacks for the application.5 *6 * @return void7 */8public function register()9{10 $this->renderable(function (InvalidOrderException $e, $request) {11 return response()->view('errors.invalid-order', [], 500);12 });13}
1use App\Exceptions\InvalidOrderException;23/**4 * Register the exception handling callbacks for the application.5 *6 * @return void7 */8public function register()9{10 $this->renderable(function (InvalidOrderException $e, $request) {11 return response()->view('errors.invalid-order', [], 500);12 });13}
也可以使用 renderable
方法來複寫 Laravel 或 Symfony 內建 Exception 的轉移行外。如:NotFoundHttpException
。若傳給 renderable
方法的閉包未回傳任何值,則會使用 Laravel 的預設 Exception 轉譯:
1use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;23/**4 * Register the exception handling callbacks for the application.5 *6 * @return void7 */8public function register()9{10 $this->renderable(function (NotFoundHttpException $e, $request) {11 if ($request->is('api/*')) {12 return response()->json([13 'message' => 'Record not found.'14 ], 404);15 }16 });17}
1use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;23/**4 * Register the exception handling callbacks for the application.5 *6 * @return void7 */8public function register()9{10 $this->renderable(function (NotFoundHttpException $e, $request) {11 if ($request->is('api/*')) {12 return response()->json([13 'message' => 'Record not found.'14 ], 404);15 }16 });17}
可回報與可轉譯的 Exception
除了在 Exception Handler 的 register
方法上設定 Exception 的類型外,我們還可以直接在我們的自訂 Exception 上定義 report
與 render
方法。當這些方法存在時,Laravel 會自動呼叫這些方法:
1<?php23namespace App\Exceptions;45use Exception;67class InvalidOrderException extends Exception8{9 /**10 * Report the exception.11 *12 * @return bool|null13 */14 public function report()15 {16 //17 }1819 /**20 * Render the exception into an HTTP response.21 *22 * @param \Illuminate\Http\Request $request23 * @return \Illuminate\Http\Response24 */25 public function render($request)26 {27 return response(...);28 }29}
1<?php23namespace App\Exceptions;45use Exception;67class InvalidOrderException extends Exception8{9 /**10 * Report the exception.11 *12 * @return bool|null13 */14 public function report()15 {16 //17 }1819 /**20 * Render the exception into an HTTP response.21 *22 * @param \Illuminate\Http\Request $request23 * @return \Illuminate\Http\Response24 */25 public function render($request)26 {27 return response(...);28 }29}
若你的 Exception 繼承的 Exception 已經是可轉譯的了 (如 Laravel 或 Symfony 內建的 Exception),可在該 Exception 的 render
方法內回傳 false
來轉譯某個 Exception 的預設 HTTP Response:
1/**2 * Render the exception into an HTTP response.3 *4 * @param \Illuminate\Http\Request $request5 * @return \Illuminate\Http\Response6 */7public function render($request)8{9 // Determine if the exception needs custom rendering...1011 return false;12}
1/**2 * Render the exception into an HTTP response.3 *4 * @param \Illuminate\Http\Request $request5 * @return \Illuminate\Http\Response6 */7public function render($request)8{9 // Determine if the exception needs custom rendering...1011 return false;12}
若你的 Exception 中包含了只有在特定情況下才會使用的自訂回報邏輯,則可讓 Laravel 在某些時候使用預設的 Exception 處理設定來回報這個 Exception。若要這麼做,請在該 Exception 的 report
方法內回傳 false
:
1/**2 * Report the exception.3 *4 * @return bool|null5 */6public function report()7{8 // Determine if the exception needs custom reporting...910 return false;11}
1/**2 * Report the exception.3 *4 * @return bool|null5 */6public function report()7{8 // Determine if the exception needs custom reporting...910 return false;11}
可以在 report
方法中型別提示任何的相依性。Laravel 的 Service Container 會自動插入這些相依性。
依型別映射 Exception
專案中使用的第三方函式庫可能會擲回 Exception,而有時候我們會想讓這些 Exception 變成是可被轉譯的,但因為我們無法控制第三方的 Exception,因此無法做到。
幸好,在 Laravel 中,我們可以將這些 Exception 映射為其他由專案所管理的 Exception 型別。若要映射這些 Exception,可以在 Exception Handler 的 register
方法內呼叫 map
方法:
1use League\Flysystem\Exception;2use App\Exceptions\FilesystemException;34/**5 * Register the exception handling callbacks for the application.6 *7 * @return void8 */9public function register()10{11 $this->map(Exception::class, FilesystemException::class);12}
1use League\Flysystem\Exception;2use App\Exceptions\FilesystemException;34/**5 * Register the exception handling callbacks for the application.6 *7 * @return void8 */9public function register()10{11 $this->map(Exception::class, FilesystemException::class);12}
若想進一步控制目標 Exception,可以傳入一個閉包給 map
方法:
1use League\Flysystem\Exception;2use App\Exceptions\FilesystemException;34$this->map(fn (Exception $e) => new FilesystemException($e));
1use League\Flysystem\Exception;2use App\Exceptions\FilesystemException;34$this->map(fn (Exception $e) => new FilesystemException($e));
HTTP Exception
有的 Exception 是用來描述伺服器的 HTTP 錯誤代碼。例如,這些 Exception 可能是:「找不到頁面」錯誤 (404)、「未經授權」錯誤 (401)⋯⋯等,甚至是開發人員造成的 500 錯誤。在你的程式中的任何地點內,若要產生這種 Response,可使用 abort
輔助函式:
1abort(404);
1abort(404);
自訂 HTTP 錯誤頁面
在 Laravel 中,要給各種 HTTP 狀態碼顯示自訂錯誤頁非常容易。舉例來說,若要自訂 404 HTTP 狀態碼的錯誤頁面,請建立 resources/views/errors/404.blade.php
View 樣板。程式中只要產生 404 錯誤,就會轉譯這個 View。在該目錄中的 View 應以對應的 HTTP 狀態碼來命名。由 abort
函式產生的 Symfony\Component\HttpKernel\Exception\HttpException
實體會以 $exception
變數傳給該 View:
1<h2>{{ $exception->getMessage() }}</h2>
1<h2>{{ $exception->getMessage() }}</h2>
可以使用 vendor:publish
Artisan 指令來將 Laravel 的預設錯誤頁樣板安裝到專案內。安裝好樣板後,就可以隨意自訂這些樣板:
1php artisan vendor:publish --tag=laravel-errors
1php artisan vendor:publish --tag=laravel-errors