跳到內容

錯誤處理

簡介

當您啟動新的 Laravel 專案時,錯誤和例外處理已經為您設定完成;然而,在任何時候,您都可以使用應用程式 bootstrap/app.php 中的 withExceptions 方法來管理應用程式如何報告和呈現例外。

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

設定

config/app.php 設定檔中的 debug 選項決定了實際向使用者顯示多少錯誤資訊。預設情況下,此選項設定為尊重 APP_DEBUG 環境變數的值,該變數儲存在您的 .env 檔案中。

在本地開發期間,您應該將 APP_DEBUG 環境變數設定為 true在您的生產環境中,此值應始終為 false。如果該值在生產環境中設定為 true,您將冒著向應用程式的最終使用者暴露敏感設定值的風險。

處理例外

報告例外

在 Laravel 中,例外報告用於記錄例外或將其發送到外部服務 SentryFlare。預設情況下,例外將根據您的 日誌 設定進行記錄。但是,您可以自由地以任何您希望的方式記錄例外。

如果您需要以不同的方式報告不同類型的例外,您可以使用應用程式 bootstrap/app.php 中的 report 例外方法來註冊一個閉包,當需要報告給定類型的例外時,應該執行該閉包。Laravel 將通過檢查閉包的類型提示來確定閉包報告的例外類型

1->withExceptions(function (Exceptions $exceptions) {
2 $exceptions->report(function (InvalidOrderException $e) {
3 // ...
4 });
5})

當您使用 report 方法註冊自訂例外報告回呼時,Laravel 仍將使用應用程式的預設日誌設定來記錄例外。如果您希望停止將例外傳播到預設日誌堆疊,您可以在定義報告回呼時使用 stop 方法,或從回呼中返回 false

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

要自訂給定例外的例外報告,您也可以使用 可報告的例外

全域日誌上下文

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

1->withExceptions(function (Exceptions $exceptions) {
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) {
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) {
5 $exceptions->level(PDOException::class, LogLevel::CRITICAL);
6})

依類型忽略例外

在建構應用程式時,總會有一些您永遠不想報告的例外類型。要忽略這些例外,您可以使用應用程式 bootstrap/app.php 檔案中的 dontReport 例外方法。提供給此方法的任何類別將永遠不會被報告;但是,它們仍然可以具有自訂呈現邏輯

1use App\Exceptions\InvalidOrderException;
2 
3->withExceptions(function (Exceptions $exceptions) {
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}

在內部,Laravel 已經為您忽略了一些類型的錯誤,例如由 404 HTTP 錯誤或由無效 CSRF 令牌產生的 419 HTTP 回應引起的例外。如果您想指示 Laravel 停止忽略給定類型的例外,您可以使用應用程式 bootstrap/app.php 檔案中的 stopIgnoring 例外方法

1use Symfony\Component\HttpKernel\Exception\HttpException;
2 
3->withExceptions(function (Exceptions $exceptions) {
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) {
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) {
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) {
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) {
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})

可報告和可呈現的例外

您可以直接在應用程式的例外上定義 reportrender 方法,而不是在應用程式的 bootstrap/app.php 檔案中定義自訂報告和呈現行為。當這些方法存在時,它們將由框架自動調用

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 into 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 into 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 實例的閉包 (closure)。

1use Illuminate\Support\Lottery;
2use Throwable;
3 
4->withExceptions(function (Exceptions $exceptions) {
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) {
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) {
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) {
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) {
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 針對這些狀態碼具有內部專用頁面。若要客製化為這些狀態碼呈現的頁面,您應該為每個狀態碼單獨定義自訂錯誤頁面。