エラー処理
イントロダクション
新しいLaravelプロジェクトを開始すると、エラーと例外の処理はすでに設定されています。しかし、いつでもアプリケーションのbootstrap/app.phpでwithExceptionsメソッドを使用して、アプリケーションが例外をレポートおよびレンダーする方法を管理できます。
withExceptionsクロージャに渡される$exceptionsオブジェクトはIlluminate\Foundation\Configuration\Exceptionsのインスタンスであり、アプリケーションの例外処理の管理を担当します。このドキュメント全体を通して、このオブジェクトについて詳しく掘り下げていきます。
設定
config/app.php設定ファイルのdebugオプションは、エラーに関する情報がユーザーに実際にどの程度表示されるかを決定します。デフォルトでこのオプションは、.envファイルに保存されているAPP_DEBUG環境変数の値を尊重するように設定されています。
ローカル開発中は、APP_DEBUG環境変数をtrueに設定する必要があります。 本番環境では、この値は常にfalseでなければなりません。本番環境でこの値がtrueに設定されていると、機密性の高い設定値がアプリケーションのエンドユーザーに公開される危険があります。
例外の処理
例外のレポート
Laravelでは、例外レポートは例外をログに記録したり、SentryやFlareなどの外部サービスに送信したりするために使用されます。デフォルトでは、例外はログ設定に基づいてログに記録されます。ただし、自由に例外をログに記録する方法を決めることができます。
異なる種類の例外を異なる方法でレポートする必要がある場合は、アプリケーションの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})
例外ログコンテキスト
すべてのログメッセージにコンテキストを追加することは便利ですが、特定の例外にはログに含めたい独自のコンテキストがある場合があります。アプリケーションの例外の1つに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(): array17 {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); // ignored12report($caught); // ignored
例外のログレベル
アプリケーションのログにメッセージが書き込まれるとき、メッセージは指定されたログレベルで書き込まれます。これはログに記録されるメッセージの重大度や重要度を示します。
上記で述べたように、reportメソッドを使用してカスタム例外レポートコールバックを登録した場合でも、Laravelはアプリケーションのデフォルトログ設定を使用して例外をログに記録し続けます。しかし、ログレベルはメッセージが記録されるチャンネルに影響を与えることがあるため、特定の例外が記録されるログレベルを設定したい場合があります。
これを実現するには、アプリケーションのbootstrap/app.phpファイルでlevel例外メソッドを使用します。このメソッドは最初の引数として例外タイプを、2番目の引数としてログレベルを受け取ります。
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メソッドを使用して、NotFoundHttpExceptionなどの組み込みのLaravelやSymfonyの例外のレンダリング動作をオーバーライドすることもできます。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})
レポート可能/レンダリング可能な例外
アプリケーションのbootstrap/app.phpファイルでカスタムのレポートおよびレンダリング動作を定義する代わりに、アプリケーションの例外に直接reportメソッドとrenderメソッドを定義することができます。これらのメソッドが存在する場合、フレームワークによって自動的に呼び出されます。
1<?php 2 3namespace App\Exceptions; 4 5use Exception; 6use Illuminate\Http\Request; 7use Illuminate\Http\Response; 8 9class InvalidOrderException extends Exception10{11 /**12 * Report the exception.13 */14 public function report(): void15 {16 // ...17 }18 19 /**20 * Render the exception into an HTTP response.21 */22 public function render(Request $request): Response23 {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インスタンスを返すクロージャを受け取ります。
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})
ログに記録されたり外部のエラートラッキングサービスに送信されたりする例外を、Lotteryの代わりにLimitインスタンスを返すことでレート制限することもできます。これは、アプリケーションが使用しているサードパーティサービスがダウンしている場合など、突然の例外のバーストがログを溢れさせるのを防ぎたい場合に便利です。
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})
もちろん、異なる例外に対してLotteryとLimitのインスタンスを組み合わせて返すこともできます。
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テンプレートを定義します。
フォールバックエラーページを定義する際、Laravelにはこれらのステータスコード専用の内部ページがあるため、フォールバックページは404、500、503のエラーレスポンスには影響しません。これらのステータスコードに対してレンダリングされるページをカスタマイズするには、それぞれに対して個別のカスタムエラーページを定義する必要があります。