錯誤處理
簡介
在開始新的 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 */6public function register(): void7{8 $this->reportable(function (InvalidOrderException $e) {9 // ...10 });11}
1use App\Exceptions\InvalidOrderException;23/**4 * Register the exception handling callbacks for the application.5 */6public function register(): void7{8 $this->reportable(function (InvalidOrderException $e) {9 // ...10 });11}
使用 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 array<string, mixed>5 */6protected function context(): array7{8 return array_merge(parent::context(), [9 'foo' => 'bar',10 ]);11}
1/**2 * Get the default context variables for logging.3 *4 * @return array<string, mixed>5 */6protected function context(): array7{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 array<string, mixed>15 */16 public function context(): array17 {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 array<string, mixed>15 */16 public function context(): array17 {18 return ['order_id' => $this->orderId];19 }20}
report
輔助函式
有時候,我們可能會想回報某個 Exception,但又想繼續執行目前的 Request。使用 report
輔助函式,就能輕鬆地在不轉譯出錯誤頁面的情況下使用 Exception Handler 來回報這個 Exception:
1public function isValid(string $value): bool2{3 try {4 // 驗證資料...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
1public function isValid(string $value): bool2{3 try {4 // 驗證資料...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
Exception 的 Log 等級
在將訊息寫入專案的 Log 時,這些訊息會以特定的 Log 等級寫入。這個等級即代表該日誌訊息的嚴重程度。
上面也提過,即使使用了 reportable
方法註冊自定的 Exception 回報回呼,Laravel 也還是會使用專案預設的 Log 設定來記錄該 Exception。不過,由於 Log 等級有時候會影響訊息會被記錄在哪些通道內,因此有時候我們可能會想設定某個特定的 Exception 要被記錄在哪個 Log 等級上。
若要調整 Log 等級,可以在專案的 Exception Handler 上的 $levels
屬性中定義一組 Exception 型別於其 Log 等級的陣列:
1use PDOException;2use Psr\Log\LogLevel;34/**5 * A list of exception types with their corresponding custom log levels.6 *7 * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>8 */9protected $levels = [10 PDOException::class => LogLevel::CRITICAL,11];
1use PDOException;2use Psr\Log\LogLevel;34/**5 * A list of exception types with their corresponding custom log levels.6 *7 * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>8 */9protected $levels = [10 PDOException::class => LogLevel::CRITICAL,11];
以類型忽略例外
在製作專案時,我們可能會想忽略一些類型的 Exception,讓這些 Exception 永遠不要被回報。在專案中的 Exception Handler 中包含了一個 $dontReport
屬性,該屬性被初始化為空陣列。只要將任何類別加到該屬性中,這些類別就不會被回報。不過,還是可以為這些類別定義自訂的轉譯邏輯:
1use App\Exceptions\InvalidOrderException;23/**4 * A list of the exception types that are not reported.5 *6 * @var array<int, class-string<\Throwable>>7 */8protected $dontReport = [9 InvalidOrderException::class,10];
1use App\Exceptions\InvalidOrderException;23/**4 * A list of the exception types that are not reported.5 *6 * @var array<int, class-string<\Throwable>>7 */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;2use Illuminate\Http\Request;34/**5 * Register the exception handling callbacks for the application.6 */7public function register(): void8{9 $this->renderable(function (InvalidOrderException $e, Request $request) {10 return response()->view('errors.invalid-order', [], 500);11 });12}
1use App\Exceptions\InvalidOrderException;2use Illuminate\Http\Request;34/**5 * Register the exception handling callbacks for the application.6 */7public function register(): void8{9 $this->renderable(function (InvalidOrderException $e, Request $request) {10 return response()->view('errors.invalid-order', [], 500);11 });12}
也可以使用 renderable
方法來複寫 Laravel 或 Symfony 內建 Exception 的轉移行外。如:NotFoundHttpException
。若傳給 renderable
方法的閉包未回傳任何值,則會使用 Laravel 的預設 Exception 轉譯:
1use Illuminate\Http\Request;2use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;34/**5 * Register the exception handling callbacks for the application.6 */7public function register(): void8{9 $this->renderable(function (NotFoundHttpException $e, Request $request) {10 if ($request->is('api/*')) {11 return response()->json([12 'message' => 'Record not found.'13 ], 404);14 }15 });16}
1use Illuminate\Http\Request;2use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;34/**5 * Register the exception handling callbacks for the application.6 */7public function register(): void8{9 $this->renderable(function (NotFoundHttpException $e, Request $request) {10 if ($request->is('api/*')) {11 return response()->json([12 'message' => 'Record not found.'13 ], 404);14 }15 });16}
可回報與可轉譯的 Exception
除了在 Exception Handler 的 register
方法上設定 Exception 的類型外,我們還可以直接在我們的自訂 Exception 上定義 report
與 render
方法。當這些方法存在時,Laravel 會自動呼叫這些方法:
1<?php23namespace App\Exceptions;45use Exception;6use Illuminate\Http\Request;7use Illuminate\Http\Response;89class InvalidOrderException extends Exception10{11 /**12 * Report the exception.13 */14 public function report(): void15 {16 // ...17 }1819 /**20 * Render the exception into an HTTP response.21 */22 public function render(Request $request): Response23 {24 return response(/* ... */);25 }26}
1<?php23namespace App\Exceptions;45use Exception;6use Illuminate\Http\Request;7use Illuminate\Http\Response;89class InvalidOrderException extends Exception10{11 /**12 * Report the exception.13 */14 public function report(): void15 {16 // ...17 }1819 /**20 * Render the exception into an HTTP response.21 */22 public function render(Request $request): Response23 {24 return response(/* ... */);25 }26}
若你的 Exception 繼承的 Exception 已經是可轉譯的了 (如 Laravel 或 Symfony 內建的 Exception),可在該 Exception 的 render
方法內回傳 false
來轉譯某個 Exception 的預設 HTTP Response:
1/**2 * Render the exception into an HTTP response.3 */4public function render(Request $request): Response|bool5{6 if (/** 判斷 Exception 是否需要自定轉譯程式 */) {78 return response(/* ... */);9 }1011 return false;12}
1/**2 * Render the exception into an HTTP response.3 */4public function render(Request $request): Response|bool5{6 if (/** 判斷 Exception 是否需要自定轉譯程式 */) {78 return response(/* ... */);9 }1011 return false;12}
若你的 Exception 中包含了只有在特定情況下才會使用的自訂回報邏輯,則可讓 Laravel 在某些時候使用預設的 Exception 處理設定來回報這個 Exception。若要這麼做,請在該 Exception 的 report
方法內回傳 false
:
1/**2 * Report the exception.3 */4public function report(): bool5{6 if (/** 判斷 Exception 是否需要自定回報邏輯 */) {78 // ...910 return true;11 }1213 return false;14}
1/**2 * Report the exception.3 */4public function report(): bool5{6 if (/** 判斷 Exception 是否需要自定回報邏輯 */) {78 // ...910 return true;11 }1213 return false;14}
可以在 report
方法中型別提示任何的相依性。Laravel 的 Service Container 會自動插入這些相依性。
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
遞補的 HTTP 錯誤頁
可以為給定的一系列 HTTP 狀態碼定義一個「遞補的」錯誤頁面。當發生的 HTTP 狀態碼沒有對應頁面時,就會轉譯這個遞補的頁面。若要使用遞補頁面,請在專案的 resources/views/errors
目錄下定義一個 4xx.blade.php
樣板與 5xx.blade.php
樣板。