錯誤處理
簡介
在開始新的 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 // Validate the value...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
1public function isValid(string $value): bool2{3 try {4 // Validate the value...5 } catch (Throwable $e) {6 report($e);78 return false;9 }10}
避免重複回報的 Exception
若你在專案中使用 report
函式,偶爾可能會發生同一個 Exception 被回報多次的情況,並在 Log 中造成重複的項目。
若想避免同一個 Exception 實體被回報多次,可以在 App\Exceptions\Handler
類別中將 $withoutDuplicates
屬性設為 true
:
1namespace App\Exceptions;23use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;45class Handler extends ExceptionHandler6{7 /**8 * Indicates that an exception instance should only be reported once.9 *10 * @var bool11 */12 protected $withoutDuplicates = true;1314 // ...15}
1namespace App\Exceptions;23use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;45class Handler extends ExceptionHandler6{7 /**8 * Indicates that an exception instance should only be reported once.9 *10 * @var bool11 */12 protected $withoutDuplicates = true;1314 // ...15}
現在,當使用相同 Exception 實體來呼叫 report
輔助函式時,就只有第一次呼叫會被回報:
1$original = new RuntimeException('Whoops!');23report($original); // reported45try {6 throw $original;7} catch (Throwable $caught) {8 report($caught); // ignored9}1011report($original); // ignored12report($caught); // ignored
1$original = new RuntimeException('Whoops!');23report($original); // reported45try {6 throw $original;7} catch (Throwable $caught) {8 report($caught); // ignored9}1011report($original); // ignored12report($caught); // ignored
Exception 的 Log 等級
在將訊息寫入專案的 Log 時,這些訊息會以特定的 Log 等級寫入。這個等級即代表該日誌訊息的嚴重程度。
上面也提過,即使使用了 reportable
方法註冊自定的 Exception 回報回呼,Laravel 也還是會使用專案預設的 Log 設定來記錄該 Exception。不過,由於 Log 等級有時候會影響訊息會被記錄在哪些通道內,因此有時候我們可能會想設定某個特定的 Exception 要被記錄在哪個 Log 等級上。
若要設定 Exception 的等級,請在專案的 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];
Ignoring Exceptions by Type
在製作專案時,有一些類型的 Exception 可能是我們想永遠忽略不回報的。若要忽略這類 Exception,請在專案的 Exception Handler 中定義一個 $dontReport
屬性。加入到此屬性的任何類別都不會被回報。不過,這些 Exception 還是可以有其自定轉譯邏輯:
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 內部,已經有一些型別的錯誤是會預設被忽略的,例如 404 HTTP 錯誤與無效 CSRF Token 產生的 419 HTTP Response 所產生的 Exception 等。若想讓 Laravel 不要忽略特定類型的 Exception,可以在 Exception Handler 的 register
方法中呼叫 stopIgnoring
方法:
1use Symfony\Component\HttpKernel\Exception\HttpException;23/**4 * Register the exception handling callbacks for the application.5 */6public function register(): void7{8 $this->stopIgnoring(HttpException::class);910 // ...11}
1use Symfony\Component\HttpKernel\Exception\HttpException;23/**4 * Register the exception handling callbacks for the application.5 */6public function register(): void7{8 $this->stopIgnoring(HttpException::class);910 // ...11}
轉譯 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}
Reportable and Renderable Exceptions
除了在 Exception Handler 的 register
方法中定義回報與轉譯行為,也可以直接在專案的 Exception 中定義 report
與 render
方法。當 Exception 中有這些方法時,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 (/** Determine if the exception needs custom rendering */) {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 (/** Determine if the exception needs custom rendering */) {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 (/** Determine if the exception needs custom reporting */) {78 // ...910 return true;11 }1213 return false;14}
1/**2 * Report the exception.3 */4public function report(): bool5{6 if (/** Determine if the exception needs custom reporting */) {78 // ...910 return true;11 }1213 return false;14}
可以在 report
方法中型別提示任何的相依性。Laravel 的 Service Container 會自動插入這些相依性。
頻率限制回報的 Exception
若你的專案會回報大量的 Exception,則你可能會想針對實際要被 Log 與傳送到專案外部錯誤追蹤服務的 Exception 進行頻率限制。
若要對 Exception 採用隨機的採樣率,可以在 Exception Handler 的 throttle
方法中回傳 Lottery 實體。若 App\Exceptions\Handler
類別中沒有此方法,只需要將其加入類別即可:
1use Illuminate\Support\Lottery;2use Throwable;34/**5 * Throttle incoming exceptions.6 */7protected function throttle(Throwable $e): mixed8{9 return Lottery::odds(1, 1000);10}
1use Illuminate\Support\Lottery;2use Throwable;34/**5 * Throttle incoming exceptions.6 */7protected function throttle(Throwable $e): mixed8{9 return Lottery::odds(1, 1000);10}
也可以根據 Exception 的型別來有條件地採樣。若只想採樣特定 Exception 類別的實體,只需要針對該類別回傳 Lottery
實體即可:
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Support\Lottery;3use Throwable;45/**6 * Throttle incoming exceptions.7 */8protected function throttle(Throwable $e): mixed9{10 if ($e instanceof ApiMonitoringException) {11 return Lottery::odds(1, 1000);12 }13}
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Support\Lottery;3use Throwable;45/**6 * Throttle incoming exceptions.7 */8protected function throttle(Throwable $e): mixed9{10 if ($e instanceof ApiMonitoringException) {11 return Lottery::odds(1, 1000);12 }13}
若不回傳 Lottery
而回傳 Limit
實體的話,就可以針對 Exception 的 Log 或傳送到外部錯誤追蹤服務進行頻率限制。這麼做可以避免突然增加的 Exception 使 Log 暴增,例如當網站使用的第三方服務突然離線的情況:
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45/**6 * Throttle incoming exceptions.7 */8protected function throttle(Throwable $e): mixed9{10 if ($e instanceof BroadcastException) {11 return Limit::perMinute(300);12 }13}
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45/**6 * Throttle incoming exceptions.7 */8protected function throttle(Throwable $e): mixed9{10 if ($e instanceof BroadcastException) {11 return Limit::perMinute(300);12 }13}
預設情況下,會使用 Exception 的類別名稱來作為頻率限制的索引鍵。可以在 Limit
上使用 by
方法來指定自定的索引鍵:
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45/**6 * Throttle incoming exceptions.7 */8protected function throttle(Throwable $e): mixed9{10 if ($e instanceof BroadcastException) {11 return Limit::perMinute(300)->by($e->getMessage());12 }13}
1use Illuminate\Broadcasting\BroadcastException;2use Illuminate\Cache\RateLimiting\Limit;3use Throwable;45/**6 * Throttle incoming exceptions.7 */8protected function throttle(Throwable $e): mixed9{10 if ($e instanceof BroadcastException) {11 return Limit::perMinute(300)->by($e->getMessage());12 }13}
當然,可以在不同的 Exception 間混合使用 Lottery
與 Limit
實體:
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Broadcasting\BroadcastException;3use Illuminate\Cache\RateLimiting\Limit;4use Illuminate\Support\Lottery;5use Throwable;67/**8 * Throttle incoming exceptions.9 */10protected function throttle(Throwable $e): mixed11{12 return match (true) {13 $e instanceof BroadcastException => Limit::perMinute(300),14 $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),15 default => Limit::none(),16 };17}
1use App\Exceptions\ApiMonitoringException;2use Illuminate\Broadcasting\BroadcastException;3use Illuminate\Cache\RateLimiting\Limit;4use Illuminate\Support\Lottery;5use Throwable;67/**8 * Throttle incoming exceptions.9 */10protected function throttle(Throwable $e): mixed11{12 return match (true) {13 $e instanceof BroadcastException => Limit::perMinute(300),14 $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),15 default => Limit::none(),16 };17}
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
樣板。