#はじめに
新しいLaravelプロジェクトを開始すると、エラーと例外のハンドリングはすでに設定されています。App\Exceptions\Handlerクラスは、アプリケーションでスローされたすべての例外をログに記録し、ユーザーに表示する役割を持ちます。このドキュメント全体でこのクラスについて詳しく説明します。
#設定
config/app.phpのdebugオプションは、エラーに関する情報をユーザーにどの程度表示するかを決定します。デフォルトでは、このオプションは.envファイルに保存されているAPP_DEBUG環境変数の値を尊重するように設定されています。
ローカル開発中は、APP_DEBUG環境変数をtrueに設定してください。本番環境では、この値は常にfalseにする必要があります。本番環境でtrueに設定すると、機密設定値がアプリケーションのエンドユーザーに漏れるリスクがあります。
#例外ハンドラー
#例外の報告
すべての例外はApp\Exceptions\Handlerクラスで処理されます。このクラスには、カスタムの例外報告およびレンダリングのコールバックを登録できるregisterメソッドがあります。これらの概念を詳しく見ていきます。例外報告は、例外をログに記録したり、Flare、Bugsnag、Sentryなどの外部サービスに送信したりするために使います。デフォルトでは、例外はログ設定に基づいてログに記録されますが、自由にログ方法を変更できます。
異なるタイプの例外を異なる方法で報告したい場合は、reportableメソッドを使って、特定のタイプの例外が報告される際に実行されるクロージャを登録できます。Laravelはクロージャの型ヒントを調べて、どのタイプの例外を報告するかを判断します:
use App\Exceptions\InvalidOrderException;
/**
* アプリケーションの例外処理コールバックを登録します。
*/
public function register(): void
{
$this->reportable(function (InvalidOrderException $e) {
// ...
});
}
reportableメソッドでカスタム例外報告コールバックを登録しても、Laravelはデフォルトのログ設定に従って例外をログに記録します。例外のデフォルトログへの伝播を止めたい場合は、報告コールバック定義時にstopメソッドを使うか、コールバックからfalseを返してください:
$this->reportable(function (InvalidOrderException $e) {
// ...
})->stop();
$this->reportable(function (InvalidOrderException $e) {
return false;
});
特定の例外の報告をカスタマイズするには、報告可能な例外も利用できます。
#グローバルログコンテキスト
可能な場合、Laravelは現在のユーザーIDをすべての例外ログメッセージにコンテキストデータとして自動的に追加します。独自のグローバルコンテキストデータは、アプリケーションのApp\Exceptions\Handlerクラスにcontextメソッドを定義することで指定できます。この情報は、アプリケーションが書き込むすべての例外ログメッセージに含まれます:
/**
* ログ用のデフォルトコンテキスト変数を取得します。
*
* @return array<string, mixed>
*/
protected function context(): array
{
return array_merge(parent::context(), [
'foo' => 'bar',
]);
}
#例外ログコンテキスト
すべてのログメッセージにコンテキストを追加するのは有用ですが、特定の例外に固有のコンテキストをログに含めたい場合があります。アプリケーションの例外クラスにcontextメソッドを定義することで、その例外に関連する任意のデータを例外ログに追加できます:
<?php
namespace App\Exceptions;
use Exception;
class InvalidOrderException extends Exception
{
// ...
/**
* 例外のコンテキスト情報を取得します。
*
* @return array<string, mixed>
*/
public function context(): array
{
return ['order_id' => $this->orderId];
}
}
#report ヘルパー
例外を報告しつつ、現在のリクエスト処理を続行したい場合があります。reportヘルパー関数は、エラーページを表示せずに例外ハンドラー経由で例外を素早く報告できます:
public function isValid(string $value): bool
{
try {
// 値を検証します...
} catch (Throwable $e) {
report($e);
return false;
}
}
#報告例外の重複排除
アプリケーション全体でreport関数を使うと、同じ例外を複数回報告してログに重複エントリができることがあります。
例外のインスタンスが一度だけ報告されるようにしたい場合は、アプリケーションのApp\Exceptions\Handlerクラス内で$withoutDuplicatesプロパティをtrueに設定してください:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* 例外インスタンスは一度だけ報告されることを示します。
*
* @var bool
*/
protected $withoutDuplicates = true;
// ...
}
同じ例外インスタンスでreportヘルパーが呼ばれた場合、最初の呼び出しだけが報告されます:
$original = new RuntimeException('Whoops!');
report($original); // 報告される
try {
throw $original;
} catch (Throwable $caught) {
report($caught); // 無視される
}
report($original); // 無視される
report($caught); // 無視される
#例外ログレベル
アプリケーションのログにメッセージが書き込まれる際、メッセージは指定されたログレベルで記録されます。ログレベルはメッセージの重要度や深刻度を示します。
前述の通り、reportableメソッドでカスタム例外報告コールバックを登録しても、Laravelはデフォルトのログ設定で例外をログに記録します。ただし、ログレベルはメッセージが記録されるチャネルに影響することがあるため、特定の例外のログレベルを設定したい場合があります。
これを実現するには、アプリケーションの例外ハンドラーに$levelsプロパティを定義します。このプロパティは例外タイプと対応するログレベルの配列です:
use PDOException;
use Psr\Log\LogLevel;
/**
* 例外タイプと対応するカスタムログレベルのリスト。
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
PDOException::class => LogLevel::CRITICAL,
];
#タイプ別の例外無視
アプリケーション構築時に、報告したくない例外タイプがある場合があります。これらの例外を無視するには、例外ハンドラーに$dontReportプロパティを定義してください。このプロパティに追加したクラスは報告されませんが、カスタムレンダリングは可能です:
use App\Exceptions\InvalidOrderException;
/**
* 報告しない例外タイプのリスト。
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
InvalidOrderException::class,
];
内部的に、Laravelはすでに404 HTTPエラーや無効なCSRFトークンによる419 HTTPレスポンスなどの例外を無視しています。特定の例外タイプの無視をやめたい場合は、例外ハンドラーのregisterメソッド内でstopIgnoringメソッドを呼び出してください:
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* アプリケーションの例外処理コールバックを登録します。
*/
public function register(): void
{
$this->stopIgnoring(HttpException::class);
// ...
}
#例外のレンダリング
デフォルトで、Laravelの例外ハンドラーは例外をHTTPレスポンスに変換します。ただし、特定のタイプの例外に対してカスタムレンダリングクロージャを登録できます。これは例外ハンドラーのrenderableメソッドを使って実現します。
renderableメソッドに渡すクロージャはIlluminate\Http\Responseのインスタンスを返す必要があります。これはresponseヘルパーで生成できます。Laravelはクロージャの型ヒントを調べて、どのタイプの例外をレンダリングするか判断します:
use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;
/**
* アプリケーションの例外処理コールバックを登録します。
*/
public function register(): void
{
$this->renderable(function (InvalidOrderException $e, Request $request) {
return response()->view('errors.invalid-order', [], 500);
});
}
renderable メソッドを使用して、NotFoundHttpException のような組み込みの Laravel や Symfony の例外に対するレンダリングの挙動を上書きできます。renderable メソッドに渡したクロージャが値を返さない場合は、Laravel のデフォルトの例外レンダリングが使用されます:
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* アプリケーションの例外処理コールバックを登録します。
*/
public function register(): void
{
$this->renderable(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
}
#報告可能およびレンダリング可能な例外
例外ハンドラーのregisterメソッドでカスタム報告やレンダリングを定義する代わりに、アプリケーションの例外クラスに直接reportおよびrenderメソッドを定義できます。これらのメソッドが存在すると、フレームワークが自動的に呼び出します:
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class InvalidOrderException extends Exception
{
/**
* 例外を報告します。
*/
public function report(): void
{
// ...
}
/**
* 例外をHTTPレスポンスにレンダリングします。
*/
public function render(Request $request): Response
{
return response(/* ... */);
}
}
例外がすでにレンダリング可能な例外(LaravelやSymfonyの組み込み例外など)を継承している場合、例外のrenderメソッドからfalseを返すことで、例外のデフォルトHTTPレスポンスをレンダリングできます:
/**
* 例外をHTTPレスポンスにレンダリングします。
*/
public function render(Request $request): Response|bool
{
if (/** 例外がカスタムレンダリングを必要とするか判定 */) {
return response(/* ... */);
}
return false;
}
例外に特定条件でのみ必要なカスタム報告ロジックがある場合、例外のreportメソッドからfalseを返して、Laravelのデフォルト例外処理設定で報告させることができます:
/**
* 例外を報告します。
*/
public function report(): bool
{
if (/** 例外がカスタム報告を必要とするか判定 */) {
// ...
return true;
}
return false;
}
reportメソッドの依存関係は型ヒントを付けることで、Laravelのサービスコンテナが自動的に注入します。
#報告例外のスロットリング
アプリケーションが大量の例外を報告する場合、実際にログや外部エラートラッキングサービスに送信する例外数を制限したいことがあります。
例外のランダムサンプリングを行うには、例外ハンドラーのthrottleメソッドからLotteryインスタンスを返します。App\Exceptions\Handlerクラスにこのメソッドがない場合は追加してください:
use Illuminate\Support\Lottery;
use Throwable;
/**
* 受信例外のスロットリングを行います。
*/
protected function throttle(Throwable $e): mixed
{
return Lottery::odds(1, 1000);
}
例外タイプに応じて条件付きでサンプリングすることも可能です。特定の例外クラスのインスタンスだけをサンプリングしたい場合は、そのクラスに対してのみLotteryインスタンスを返します:
use App\Exceptions\ApiMonitoringException;
use Illuminate\Support\Lottery;
use Throwable;
/**
* 受信例外のスロットリングを行います。
*/
protected function throttle(Throwable $e): mixed
{
if ($e instanceof ApiMonitoringException) {
return Lottery::odds(1, 1000);
}
}
外部エラートラッキングサービスに送信する例外のレート制限を行うには、Lotteryの代わりにLimitインスタンスを返します。これは、例えばアプリケーションが利用するサードパーティサービスがダウンした際に例外が急増してログが溢れるのを防ぐのに役立ちます:
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;
/**
* 受信例外のスロットリングを行います。
*/
protected function throttle(Throwable $e): mixed
{
if ($e instanceof BroadcastException) {
return Limit::perMinute(300);
}
}
デフォルトでは、レート制限のキーに例外のクラスが使われます。Limitのbyメソッドを使って独自のキーを指定することも可能です:
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;
/**
* 受信例外のスロットリングを行います。
*/
protected function throttle(Throwable $e): mixed
{
if ($e instanceof BroadcastException) {
return Limit::perMinute(300)->by($e->getMessage());
}
}
もちろん、異なる例外に対してLotteryとLimitのインスタンスを混在して返すこともできます:
use App\Exceptions\ApiMonitoringException;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Lottery;
use Throwable;
/**
* 受信例外のスロットリングを行います。
*/
protected function throttle(Throwable $e): mixed
{
return match (true) {
$e instanceof BroadcastException => Limit::perMinute(300),
$e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
default => Limit::none(),
};
}
#HTTP例外
一部の例外はサーバーからのHTTPエラーコードを表します。例えば、「ページが見つかりません」(404)、「認証エラー」(401)、または開発者が生成した500エラーなどです。アプリケーションのどこからでもこのようなレスポンスを生成するには、abortヘルパーを使います:
abort(404);
#カスタムHTTPエラーページ
Laravelは様々なHTTPステータスコードに対してカスタムエラーページを簡単に表示できます。例えば、404 HTTPステータスコードのエラーページをカスタマイズするには、resources/views/errors/404.blade.phpビューを作成します。このビューはアプリケーションで発生したすべての404エラーに対してレンダリングされます。このディレクトリ内のビューは対応するHTTPステータスコードの名前にしてください。abort関数で発生するSymfony\Component\HttpKernel\Exception\HttpExceptionインスタンスは、ビューに$exception変数として渡されます:
<h2>{{ $exception->getMessage() }}</h2>
Laravelのデフォルトエラーページテンプレートはvendor:publish Artisanコマンドで公開できます。公開後は自由にカスタマイズ可能です:
php artisan vendor:publish --tag=laravel-errors
#フォールバックHTTPエラーページ
特定のHTTPステータスコードに対応するページがない場合に表示される「フォールバック」エラーページも定義できます。これを実現するには、resources/views/errorsディレクトリに4xx.blade.phpと5xx.blade.phpテンプレートを作成してください。