跳轉至內容

錯誤處理

簡介

當你建立一個新的 Laravel 專案時,錯誤和異常處理已經為你配置好了;不過,你可以隨時在應用程式的 bootstrap/app.php 檔案中使用 withExceptions 方法來管理應用程式如何報告和渲染異常。

提供給 withExceptions 閉包的 $exceptions 物件是 Illuminate\Foundation\Configuration\Exceptions 的一個例項,負責管理應用程式中的異常處理。我們將在本文件中深入探討該物件。

配置

config/app.php 配置檔案中的 debug 選項決定了向用戶顯示多少關於錯誤的資訊。預設情況下,此選項設定為遵循 APP_DEBUG 環境變數的值,該變數儲存在你的 .env 檔案中。

在本地開發過程中,你應該將 APP_DEBUG 環境變數設定為 true

在生產環境中,APP_DEBUG 的值應始終為 false。如果在生產環境中將其設定為 true,你可能會面臨將敏感配置值暴露給應用程式終端使用者的風險。

處理異常

報告異常

在 Laravel 中,異常報告用於記錄異常或將其傳送到外部服務,如 SentryFlare。預設情況下,異常將根據你的 日誌 配置進行記錄。不過,你可以隨心所欲地記錄異常。

如果你需要以不同方式報告不同型別的異常,可以在應用程式的 bootstrap/app.php 中使用 report 異常方法來註冊一個閉包,當需要報告特定型別的異常時,該閉包將被執行。Laravel 將透過檢查閉包的型別提示來確定閉包要報告的異常型別。

1use App\Exceptions\InvalidOrderException;
2 
3->withExceptions(function (Exceptions $exceptions): void {
4 $exceptions->report(function (InvalidOrderException $e) {
5 // ...
6 });
7})

當你使用 report 方法註冊自定義異常報告回撥時,Laravel 仍會使用應用程式的預設日誌配置記錄該異常。如果你希望停止將異常傳播到預設日誌堆疊,可以在定義報告回撥時使用 stop 方法,或者從回撥中返回 false

1use App\Exceptions\InvalidOrderException;
2 
3->withExceptions(function (Exceptions $exceptions): void {
4 $exceptions->report(function (InvalidOrderException $e) {
5 // ...
6 })->stop();
7 
8 $exceptions->report(function (InvalidOrderException $e) {
9 return false;
10 });
11})

若要自定義特定異常的報告方式,你還可以利用 可報告異常 (reportable exceptions)

全域性日誌上下文

如果可用,Laravel 會自動將當前使用者的 ID 作為上下文資料新增到每個異常的日誌訊息中。你可以使用應用程式 bootstrap/app.php 檔案中的 context 異常方法來定義自己的全域性上下文資料。此資訊將包含在應用程式寫入的每個異常日誌訊息中。

1->withExceptions(function (Exceptions $exceptions): void {
2 $exceptions->context(fn () => [
3 'foo' => 'bar',
4 ]);
5})

異常日誌上下文

雖然向每條日誌訊息新增上下文很有用,但有時某個特定的異常可能具有你希望包含在日誌中的獨特上下文。透過在應用程式的異常類中定義 context 方法,你可以指定與該異常相關的任何資料,這些資料將被新增到該異常的日誌條目中。

1<?php
2 
3namespace App\Exceptions;
4 
5use Exception;
6 
7class InvalidOrderException extends Exception
8{
9 // ...
10 
11 /**
12 * Get the exception's context information.
13 *
14 * @return array<string, mixed>
15 */
16 public function context(): array
17 {
18 return ['order_id' => $this->orderId];
19 }
20}

report 輔助函式

有時你可能需要報告一個異常,但仍要繼續處理當前的請求。report 輔助函式允許你快速報告異常,而無需向用戶渲染錯誤頁面。

1public function isValid(string $value): bool
2{
3 try {
4 // Validate the value...
5 } catch (Throwable $e) {
6 report($e);
7 
8 return false;
9 }
10}

對報告的異常進行去重

如果你在整個應用程式中頻繁使用 report 函式,有時可能會多次報告同一個異常,從而在日誌中建立重複條目。

如果你想確保同一個異常例項只被報告一次,可以在應用程式的 bootstrap/app.php 檔案中呼叫 dontReportDuplicates 異常方法。

1->withExceptions(function (Exceptions $exceptions): void {
2 $exceptions->dontReportDuplicates();
3})

現在,當使用同一個異常例項呼叫 report 輔助函式時,只有第一次呼叫會被報告。

1$original = new RuntimeException('Whoops!');
2 
3report($original); // reported
4 
5try {
6 throw $original;
7} catch (Throwable $caught) {
8 report($caught); // ignored
9}
10 
11report($original); // ignored
12report($caught); // ignored

異常日誌級別

當訊息被寫入應用程式的 日誌 時,訊息會以指定的 日誌級別 寫入,這表明了所記錄訊息的嚴重性或重要性。

如上所述,即使你使用 report 方法註冊了自定義異常報告回撥,Laravel 仍會使用應用程式的預設日誌配置記錄該異常;然而,由於日誌級別有時會影響訊息記錄所在的頻道,你可能希望配置特定異常記錄時的日誌級別。

為此,你可以在應用程式的 bootstrap/app.php 檔案中使用 level 異常方法。此方法接收異常型別作為第一個引數,日誌級別作為第二個引數。

1use PDOException;
2use Psr\Log\LogLevel;
3 
4->withExceptions(function (Exceptions $exceptions): void {
5 $exceptions->level(PDOException::class, LogLevel::CRITICAL);
6})

忽略特定型別的異常

在構建應用程式時,有些型別的異常你可能永遠不想報告。要忽略這些異常,可以在應用程式的 bootstrap/app.php 檔案中使用 dontReport 異常方法。提供給此方法的任何類都將永遠不會被報告;不過,它們仍然可以擁有自定義的渲染邏輯。

1use App\Exceptions\InvalidOrderException;
2 
3->withExceptions(function (Exceptions $exceptions): void {
4 $exceptions->dontReport([
5 InvalidOrderException::class,
6 ]);
7})

或者,你可以簡單地使用 Illuminate\Contracts\Debug\ShouldntReport 介面來“標記”一個異常類。當一個異常被標記為該介面時,它將永遠不會被 Laravel 的異常處理器報告。

1<?php
2 
3namespace App\Exceptions;
4 
5use Exception;
6use Illuminate\Contracts\Debug\ShouldntReport;
7 
8class PodcastProcessingException extends Exception implements ShouldntReport
9{
10 //
11}

如果你需要更精細地控制何時忽略特定型別的異常,可以為 dontReportWhen 方法提供一個閉包。

1use App\Exceptions\InvalidOrderException;
2use Throwable;
3 
4->withExceptions(function (Exceptions $exceptions): void {
5 $exceptions->dontReportWhen(function (Throwable $e) {
6 return $e instanceof PodcastProcessingException &&
7 $e->reason() === 'Subscription expired';
8 });
9})

在內部,Laravel 已經為你忽略了一些型別的錯誤,例如由 404 HTTP 錯誤、源不匹配導致的 403 HTTP 響應或無效 CSRF 令牌導致的 419 HTTP 響應產生的異常。如果你想指示 Laravel 停止忽略某種特定型別的異常,可以使用應用程式 bootstrap/app.php 檔案中的 stopIgnoring 異常方法。

1use Symfony\Component\HttpKernel\Exception\HttpException;
2 
3->withExceptions(function (Exceptions $exceptions): void {
4 $exceptions->stopIgnoring(HttpException::class);
5})

渲染異常

預設情況下,Laravel 異常處理器會將異常轉換為 HTTP 響應。不過,你可以自由地為特定型別的異常註冊自定義渲染閉包。你可以透過在應用程式的 bootstrap/app.php 檔案中使用 render 異常方法來實現。

傳遞給 render 方法的閉包應該返回一個 Illuminate\Http\Response 例項,該例項可以透過 response 輔助函式生成。Laravel 將透過檢查閉包的型別提示來確定閉包要渲染的異常型別。

1use App\Exceptions\InvalidOrderException;
2use Illuminate\Http\Request;
3 
4->withExceptions(function (Exceptions $exceptions): void {
5 $exceptions->render(function (InvalidOrderException $e, Request $request) {
6 return response()->view('errors.invalid-order', status: 500);
7 });
8})

你還可以使用 render 方法來覆蓋內建 Laravel 或 Symfony 異常(如 NotFoundHttpException)的渲染行為。如果傳遞給 render 方法的閉包沒有返回值,將使用 Laravel 的預設異常渲染。

1use Illuminate\Http\Request;
2use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
3 
4->withExceptions(function (Exceptions $exceptions): void {
5 $exceptions->render(function (NotFoundHttpException $e, Request $request) {
6 if ($request->is('api/*')) {
7 return response()->json([
8 'message' => 'Record not found.'
9 ], 404);
10 }
11 });
12})

將異常渲染為 JSON

在渲染異常時,Laravel 會根據請求的 Accept 頭自動確定是否應將異常渲染為 HTML 或 JSON 響應。如果你想自定義 Laravel 判斷渲染 HTML 還是 JSON 異常響應的方式,可以利用 shouldRenderJsonWhen 方法。

1use Illuminate\Http\Request;
2use Throwable;
3 
4->withExceptions(function (Exceptions $exceptions): void {
5 $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
6 if ($request->is('admin/*')) {
7 return true;
8 }
9 
10 return $request->expectsJson();
11 });
12})

自定義異常響應

在極少數情況下,你可能需要自定義 Laravel 異常處理器渲染的整個 HTTP 響應。為此,你可以使用 respond 方法註冊一個響應自定義閉包。

1use Symfony\Component\HttpFoundation\Response;
2 
3->withExceptions(function (Exceptions $exceptions): void {
4 $exceptions->respond(function (Response $response) {
5 if ($response->getStatusCode() === 419) {
6 return back()->with([
7 'message' => 'The page expired, please try again.',
8 ]);
9 }
10 
11 return $response;
12 });
13})

可報告與可渲染的異常

除了在應用程式的 bootstrap/app.php 檔案中定義自定義報告和渲染行為外,你也可以直接在應用程式的異常類中定義 reportrender 方法。當這些方法存在時,它們將被框架自動呼叫。

1<?php
2 
3namespace App\Exceptions;
4 
5use Exception;
6use Illuminate\Http\Request;
7use Illuminate\Http\Response;
8 
9class InvalidOrderException extends Exception
10{
11 /**
12 * Report the exception.
13 */
14 public function report(): void
15 {
16 // ...
17 }
18 
19 /**
20 * Render the exception as an HTTP response.
21 */
22 public function render(Request $request): Response
23 {
24 return response(/* ... */);
25 }
26}

如果你的異常擴充套件了一個已經是可渲染的異常(例如內建的 Laravel 或 Symfony 異常),你可以從異常的 render 方法中返回 false,以渲染該異常的預設 HTTP 響應。

1/**
2 * Render the exception as an HTTP response.
3 */
4public function render(Request $request): Response|bool
5{
6 if (/** Determine if the exception needs custom rendering */) {
7 
8 return response(/* ... */);
9 }
10 
11 return false;
12}

如果你的異常包含僅在滿足特定條件時才需要的自定義報告邏輯,你可能需要指示 Laravel 在某些情況下使用預設的異常處理配置來報告該異常。為此,你可以從異常的 report 方法中返回 false

1/**
2 * Report the exception.
3 */
4public function report(): bool
5{
6 if (/** Determine if the exception needs custom reporting */) {
7 
8 // ...
9 
10 return true;
11 }
12 
13 return false;
14}

你可以對 report 方法所需的任何依賴項進行型別提示,它們將由 Laravel 的 服務容器 自動注入。

異常報告限流

如果你的應用程式報告了大量的異常,你可能希望對實際記錄或傳送到應用程式外部錯誤跟蹤服務的異常數量進行限流。

要對異常進行隨機取樣,可以使用應用程式 bootstrap/app.php 檔案中的 throttle 異常方法。throttle 方法接收一個閉包,該閉包應返回一個 Lottery 例項。

1use Illuminate\Support\Lottery;
2use Throwable;
3 
4->withExceptions(function (Exceptions $exceptions): void {
5 $exceptions->throttle(function (Throwable $e) {
6 return Lottery::odds(1, 1000);
7 });
8})

也可以根據異常型別進行條件取樣。如果你只想對特定的異常類例項進行取樣,可以只為該類返回 Lottery 例項。

1use App\Exceptions\ApiMonitoringException;
2use Illuminate\Support\Lottery;
3use Throwable;
4 
5->withExceptions(function (Exceptions $exceptions): void {
6 $exceptions->throttle(function (Throwable $e) {
7 if ($e instanceof ApiMonitoringException) {
8 return Lottery::odds(1, 1000);
9 }
10 });
11})

你還可以透過返回 Limit 例項而不是 Lottery 來對記錄或傳送到外部錯誤跟蹤服務的異常進行速率限制。如果你想防止突發的異常浪湧淹沒你的日誌(例如,當應用程式使用的第三方服務宕機時),這非常有用。

1use Illuminate\Broadcasting\BroadcastException;
2use Illuminate\Cache\RateLimiting\Limit;
3use Throwable;
4 
5->withExceptions(function (Exceptions $exceptions): void {
6 $exceptions->throttle(function (Throwable $e) {
7 if ($e instanceof BroadcastException) {
8 return Limit::perMinute(300);
9 }
10 });
11})

預設情況下,限制將使用異常的類作為速率限制鍵。你可以透過在 Limit 上使用 by 方法指定自己的鍵來自定義此行為。

1use Illuminate\Broadcasting\BroadcastException;
2use Illuminate\Cache\RateLimiting\Limit;
3use Throwable;
4 
5->withExceptions(function (Exceptions $exceptions): void {
6 $exceptions->throttle(function (Throwable $e) {
7 if ($e instanceof BroadcastException) {
8 return Limit::perMinute(300)->by($e->getMessage());
9 }
10 });
11})

當然,你可以為不同的異常返回 LotteryLimit 例項的混合組合。

1use App\Exceptions\ApiMonitoringException;
2use Illuminate\Broadcasting\BroadcastException;
3use Illuminate\Cache\RateLimiting\Limit;
4use Illuminate\Support\Lottery;
5use Throwable;
6 
7->withExceptions(function (Exceptions $exceptions): void {
8 $exceptions->throttle(function (Throwable $e) {
9 return match (true) {
10 $e instanceof BroadcastException => Limit::perMinute(300),
11 $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
12 default => Limit::none(),
13 };
14 });
15})

HTTP 異常

有些異常描述了來自伺服器的 HTTP 錯誤程式碼。例如,“頁面未找到”錯誤 (404)、“未經授權錯誤” (401),甚至是開發者生成的 500 錯誤。為了在應用程式的任何位置生成此類響應,你可以使用 abort 輔助函式。

1abort(404);

自定義 HTTP 錯誤頁面

Laravel 可以輕鬆地為各種 HTTP 狀態碼顯示自定義錯誤頁面。例如,要自定義 404 HTTP 狀態碼的錯誤頁面,請建立一個 resources/views/errors/404.blade.php 檢視模板。此檢視將為應用程式生成的所有 404 錯誤進行渲染。該目錄中的檢視應以對應的 HTTP 狀態碼命名。由 abort 函式引發的 Symfony\Component\HttpKernel\Exception\HttpException 例項將作為 $exception 變數傳遞給檢視。

1<h2>{{ $exception->getMessage() }}</h2>

你可以使用 vendor:publish Artisan 命令釋出 Laravel 的預設錯誤頁面模板。一旦模板釋出,你就可以根據自己的喜好對其進行自定義。

1php artisan vendor:publish --tag=laravel-errors

備用 HTTP 錯誤頁面

你還可以為一系列特定的 HTTP 狀態碼定義“備用”錯誤頁面。如果發生的特定 HTTP 狀態碼沒有對應的頁面,將渲染此頁面。為此,請在應用程式的 resources/views/errors 目錄中定義 4xx.blade.php 模板和 5xx.blade.php 模板。

在定義備用錯誤頁面時,備用頁面不會影響 404500503 錯誤響應,因為 Laravel 針對這些狀態碼有內部專用頁面。要自定義這些狀態碼的渲染頁面,你應該為它們中的每一個單獨定義自定義錯誤頁面。