対象バージョン: Laravel 13.x / PHP 8.3+(laravel/framework v13.38.0 で動作確認)
Laravel のエラー処理は、例外を記録する「報告(report)」と、利用者に返すレスポンスを作る「描画(render)」の2つに分かれます。この記事は、Laravel で 404 ページを自作したい方や、例外をログにどう残すか整理したい方向けです。bootstrap/app.php の withExceptions の使い方、カスタムエラーページの作り方、JSON のエラーレスポンス、ログへの報告、テストまでを順に説明します。
この記事で分かること
- 例外処理を設定する場所(
bootstrap/app.phpのwithExceptions)と、最初から入っている設定 APP_DEBUGによるエラー表示の違いと、本番での注意点abort()で HTTP エラーを返す方法404.blade.phpなどのカスタムエラーページと、4xx.blade.php/5xx.blade.phpが使われない8つのステータス- JSON のエラーレスポンスと、メッセージの差し替え
- 例外の報告(ログ)の仕組みと調整方法
Exceptions::fake()を使ったテスト
前提
Laravel 13.x は PHP 8.3 以上が必要です。動作確認は、composer create-project laravel/laravel で新規作成したアプリ(laravel/laravel v13.11.0、laravel/framework v13.38.0、PHP 8.4)で行いました。プロジェクトの作成方法は【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成を参照してください。
例外処理の設定場所: bootstrap/app.php
Laravel 11 以降、新規アプリには app/Exceptions/Handler.php がありません。例外の報告と描画は、bootstrap/app.php の withExceptions に渡すクロージャで設定します。引数の $exceptions は Illuminate\Foundation\Configuration\Exceptions のインスタンスです。
新規作成した 13.x アプリの bootstrap/app.php は次のとおりです。
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
})->create();
withExceptions のクロージャは空ではなく、shouldRenderJsonWhen の設定が最初から1つ入っています。この設定により、api/* のパスへのリクエストは、Accept: application/json ヘッダーが無くてもエラーが JSON で返ります。この行は laravel/laravel v13.9.0 以降のスケルトンに含まれています。それより前のスケルトンで作成したプロジェクトでは、内容が異なる(v13.8.0 は api/* の判定だけ、v13.7.0 以前は空のクロージャ)ことがあります。
この記事で追加する設定は、すべてこの既存の行の下に書きます。
APP_DEBUG とエラー表示
エラー表示の詳しさは config('app.debug') で決まり、この値は .env の APP_DEBUG に従います。
| 例外の種類 | APP_DEBUG=true |
APP_DEBUG=false |
|---|---|---|
HTTP 例外以外(RuntimeException など) |
例外クラス、メッセージ、ファイルパスを含むデバッグページ | 汎用の「Server Error」ページ(500) |
HTTP 例外(abort()、存在しないルート、findOrFail() の失敗) |
エラービュー | エラービュー |
JSON の場合も同様です。APP_DEBUG=true では message に加えて exception、file、line、trace が含まれます。APP_DEBUG=false では、HTTP 例外以外は {"message": "Server Error"} だけになります。
デバッグページにはファイルパスなどアプリ内部の情報が表示されます。本番環境では APP_DEBUG を必ず false にしてください。
HTTP 例外は APP_DEBUG=true でもエラービューで表示されます。そのため、自作の 404 ページはローカル環境でもそのまま確認できます。
動作確認用のルートを用意する
この記事の例で使うルートです。routes/web.php を次の内容にします(/ のルートは新規作成時からあるものです)。例外クラスは後の節で作成します。
<?php
use App\Exceptions\InvalidOrderException;
use App\Exceptions\OrderNotReadyException;
use App\Models\User;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
// abort() / abort_if() / abort_unless()
Route::get('/forbidden', function () {
abort(403, 'この操作は許可されていません。');
});
Route::get('/abort-if', function () {
abort_if(request()->boolean('deny'), 403);
return 'allowed';
});
Route::get('/abort-unless', function () {
abort_unless(request()->boolean('ok'), 404);
return 'ok';
});
// findOrFail() で見つからなければ 404
Route::get('/users/{id}', function (int $id) {
return User::findOrFail($id);
});
// キャッチされない例外
Route::get('/boom', function () {
throw new RuntimeException('Something broke (demo).');
});
// 独自の例外
Route::get('/invalid-order', function () {
throw new InvalidOrderException(42);
});
Route::get('/order-not-ready', function () {
throw new OrderNotReadyException('Order 7 is not ready.');
});
// 例外をキャッチして報告だけする
Route::get('/report-helper', function () {
try {
throw new RuntimeException('report helper demo');
} catch (Throwable $e) {
report($e);
}
return 'continued after report()';
});
// 本物のバリデーション例外(abort(422) との比較用)
Route::get('/validate', function () {
request()->validate(['name' => 'required']);
return 'valid';
});
abort() で HTTP エラーを返す
abort($code, $message) は HTTP 例外(HttpException)を投げます。404 の場合は NotFoundHttpException です。条件付きで中断したいときは abort_if() / abort_unless() を使います。
| 呼び出し | 結果 |
|---|---|
abort(403, 'この操作は許可されていません。') |
403 |
abort_if(true, 403) |
403 |
abort_if(false, 403) |
中断せず処理を続ける |
abort_unless(false, 404) |
404 |
abort_unless(true, 404) |
中断せず処理を続ける |
上のルートでは、/abort-if?deny=1 が 403、/abort-unless が 404 になります。
投げられた例外はエラービューに $exception として渡されます。$exception->getMessage() で abort() に渡したメッセージを取得できます。メッセージを渡さずに abort(404) とした場合は空文字列です。
ミドルウェアの中で abort(403) を使う例は【PHP】Laravelでミドルウェアを実装する方法で扱っています。認可の実装は【PHP】Laravelで認証と認可を実装する方法、API のトークン認証は【PHP】Laravel SanctumでAPIトークン認証を実装する方法を参照してください。
カスタムエラーページを作る
404.blade.php を作る
resources/views/errors/ にステータスコード名のビューを置くと、そのステータスのエラーページとして使われます。404.blade.php は、次のすべての 404 で表示されます。
abort(404)- 存在しない URL へのアクセス
findOrFail()でレコードが見つからない場合(ModelNotFoundException)
まず、エラーページで使うレイアウトコンポーネント resources/views/components/layouts/app.blade.php を作ります。
@props(['title' => config('app.name')])
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>{{ $title }}</title>
</head>
<body>
<main>
{{ $slot }}
</main>
</body>
</html>
次に resources/views/errors/404.blade.php を作ります。
<x-layouts.app title="ページが見つかりません">
<h1>ページが見つかりません</h1>
<p>お探しのページは見つかりませんでした。</p>
<a href="{{ url('/') }}">トップへ戻る</a>
</x-layouts.app>
レイアウトコンポーネントの詳細は【PHP】LaravelでBladeコンポーネントとレイアウトを使う方法を参照してください。
ビューで $exception->getMessage() を表示する場合は注意が必要です。存在しない URL では「The route no-such-page could not be found.」、findOrFail() の失敗では「No query results for model [App\Models\User] 999999」のように、ルートやモデルのクラス名、ID を含むメッセージになります。abort(404) のようにメッセージが空の場合もあるため、表示するなら $exception->getMessage() ?: 'ページが見つかりません' のように代わりの文言を用意します。
4xx.blade.php / 5xx.blade.php と、使われない8つのステータス
個別のビューが無いステータスには、フォールバックとして 4xx.blade.php と 5xx.blade.php が使われます。たとえば resources/views/errors/4xx.blade.php は次のように書けます。
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="utf-8"><title>エラー {{ $exception->getStatusCode() }}</title></head>
<body>
<h1>エラーが発生しました</h1>
<p>ステータス: {{ $exception->getStatusCode() }}</p>
</body>
</html>
5xx.blade.php も同じ形で作れます。
ただし、次の8つのステータスにはフォールバックが使われません。Laravel 本体がこれらのステータス用のビューを持っていて、4xx / 5xx より先にそちらが選ばれるためです。
| ステータス | 表示されるページ(Laravel 本体) |
|---|---|
| 401 | Unauthorized |
| 402 | Payment Required |
| 403 | Forbidden |
| 404 | Not Found |
| 419 | Page Expired |
| 429 | Too Many Requests |
| 500 | Server Error |
| 503 | Service Unavailable |
4xx.blade.php と 5xx.blade.php だけを置いた状態では、次のように分かれます。
4xx.blade.phpが使われる: 400、405、408、410、418、422 など5xx.blade.phpが使われる: 501、502、504 など- Laravel 本体のページのまま: 上の表の8つ
この8つのページを変えたい場合は、403.blade.php や 404.blade.php のように個別のビューを作ります。以前のドキュメントでは対象外を 404・500・503 の3つと説明していましたが、実際の動作は 11.x 以降どのバージョンでも上の8つです。13.x のドキュメントは8つに修正されています。
実際の例外の場合も、同じルールでページが選ばれます。
| 発生した状況 | ステータス | 使われるページ |
|---|---|---|
存在しない URL、findOrFail() の失敗 |
404 | 404.blade.php(無ければ本体の Not Found) |
| CSRF トークンの無い POST | 419 | 本体の Page Expired(419.blade.php を作れば差し替え可能) |
| 許可されていない HTTP メソッド | 405 | 4xx.blade.php |
throttle ミドルウェアの制限超過 |
429 | 本体の Too Many Requests(429.blade.php を作れば差し替え可能) |
キャッチされない例外(APP_DEBUG=false) |
500 | 本体の Server Error(500.blade.php を作れば差し替え可能) |
キャッチされない例外には 500.blade.php を作る
上の表のとおり、キャッチされない例外は 500 として描画され、5xx.blade.php ではなく Laravel 本体の「Server Error」ページになります。この画面を変えるには resources/views/errors/500.blade.php を作ります。
<!DOCTYPE html>
<html lang="ja">
<head><meta charset="utf-8"><title>サーバーエラー</title></head>
<body>
<h1>サーバーエラーが発生しました</h1>
<p>時間をおいて再度お試しください。</p>
</body>
</html>
Laravel 本体のエラービューを公開して編集する
Laravel 本体のエラービューをもとに編集したい場合は、次のコマンドでアプリにコピーできます。
php artisan vendor:publish --tag=laravel-errors
resources/views/errors/ に次の10ファイルがコピーされます。ただし、同名のファイルが既にある場合は上書きされないため、この記事の手順で作成した 404.blade.php と 500.blade.php はそのまま残ります。
401.blade.php、402.blade.php、403.blade.php、404.blade.php、419.blade.php、429.blade.php、500.blade.php、503.blade.php- 共通レイアウトの
layout.blade.phpとminimal.blade.php
各ステータスのビューは @extends('errors::minimal') でレイアウトを継承しています。コピーしたビューを編集すると、本体のビューより優先して使われます。たとえば本体の 404.blade.php は次の内容です。
@extends('errors::minimal')
@section('title', __('Not Found'))
@section('code', '404')
@section('message', __('Not Found'))
403.blade.php は __($exception->getMessage() ?: 'Forbidden') を表示するため、abort(403, 'この操作は許可されていません。') のメッセージがそのまま画面に出ます。一方、本体の 404.blade.php は常に固定の「Not Found」を表示します。
JSON のエラーレスポンス
次のどちらかに当てはまるとき、Laravel はエラーを HTML ではなく JSON で返します。
$request->expectsJson()が true(Accept: application/jsonヘッダーがあるなど)shouldRenderJsonWhenのコールバックが true を返す(新規アプリではapi/*のパス)
APP_DEBUG=false でも、HTTP 例外のメッセージは JSON にそのまま含まれます。
| リクエスト | レスポンス(APP_DEBUG=false) |
|---|---|
| 存在しない URL | {"message": "The route no-such-page could not be found."} |
abort(403, 'この操作は許可されていません。') |
{"message": "この操作は許可されていません。"} |
findOrFail() の失敗 |
{"message": "No query results for model [App\\Models\\User] 999999"} |
キャッチされない RuntimeException |
{"message": "Server Error"} |
存在しない URL ではパスが、findOrFail() の失敗ではモデルのクラス名と ID がメッセージに含まれます。外部に見せたくない場合は、render コールバックでレスポンスを差し替えます。bootstrap/app.php の先頭の use 文に次の1行を追加します。
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
そのうえで、withExceptions のクロージャ内、既存の shouldRenderJsonWhen の下に次を追加します。
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.',
], 404);
}
});
この例では、api/* の 404 は {"message": "Record not found."} になります。api/* 以外のリクエストではコールバックが何も返さないため、通常どおり 404.blade.php が表示されます。
API の作り方は【PHP】LaravelでRESTful APIを作成する方法を参照してください。
例外を報告する(ログに残す)
既定の記録先と書式
新規アプリのログ設定(LOG_CHANNEL=stack、LOG_STACK=single)では、報告された例外は storage/logs/laravel.log に次の形式で書き込まれます(パスは短縮しています)。
[2026-10-23 01:20:08] local.ERROR: Something broke (demo). {"exception":"[object] (RuntimeException(code: 0): Something broke (demo). at <app>/routes/web.php:36)
[stacktrace]
先頭の日時は app.timezone の設定に従い、新規アプリでは UTC です。上の 01:20:08 は日本時間では 10:20:08 にあたります。
ログインしているユーザーがいる場合、Laravel はそのユーザーの ID を "userId" としてログに追加します。
HTTP 例外は既定では報告されない
次の例外は、既定ではログに記録されません。
- HTTP 例外(
abort(404)、abort(500)を含む) findOrFail()の失敗によるModelNotFoundExceptionValidationException、AuthenticationException、AuthorizationException、TokenMismatchExceptionなど
abort(500) も記録されない点に注意してください。HTTP 例外も記録したい場合は、bootstrap/app.php の先頭の use 文に次を追加します。
use Symfony\Component\HttpKernel\Exception\HttpException;
そのうえで、withExceptions のクロージャ内に次を追加します。
$exceptions->stopIgnoring(HttpException::class);
独自の例外クラスを作る
ここからの例で使う例外クラス app/Exceptions/InvalidOrderException.php を作ります。
<?php
namespace App\Exceptions;
use Exception;
class InvalidOrderException extends Exception
{
public function __construct(public int $orderId, string $message = 'Invalid order.')
{
parent::__construct($message);
}
/**
* Exception-specific log context.
*
* @return array<string, mixed>
*/
public function context(): array
{
return ['order_id' => $this->orderId];
}
}
context() メソッドが返す配列は、この例外のログにコンテキストとして追加されます。
report コールバックで報告処理を追加する
特定の例外を報告するときに処理を追加するには、$exceptions->report() にクロージャを渡します。どの例外に適用するかは、クロージャの引数の型で決まります。bootstrap/app.php の先頭の use 文に次を追加します。
use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Log;
そのうえで、withExceptions のクロージャ内に次を追加します。
$exceptions->report(function (InvalidOrderException $e) {
Log::info('custom report callback: InvalidOrderException order='.$e->orderId);
});
/invalid-order にアクセスすると、laravel.log にはコールバックの INFO と、Laravel の既定の ERROR の2行が書き込まれます。コールバックを登録しても、既定のログ記録は続けて行われます。
既定のログ記録を止めたい場合は、コールバックに ->stop() をつなげるか、コールバックから false を返します。
$exceptions->report(function (InvalidOrderException $e) {
Log::info('custom report callback: InvalidOrderException order='.$e->orderId);
})->stop();
すべての例外ログに情報を追加する
$exceptions->context() を使うと、すべての例外ログに共通の情報を追加できます。withExceptions のクロージャ内に次を追加します。
$exceptions->context(fn () => [
'app_ctx' => 'demo',
]);
この設定と InvalidOrderException の context() により、ログには {"order_id":42,"app_ctx":"demo","exception":...} のように両方の情報が含まれます。
report() ヘルパーで、処理を続けたまま報告する
例外をキャッチしたうえでログには残したい場合は、report() ヘルパーを使います。エラーページは表示されず、処理はそのまま続きます。前述の /report-helper ルートは 200 で「continued after report()」を返し、ログには ERROR が1件残ります。
report('メッセージ') のように文字列を渡すと、例外に包んで報告されます。条件付きで報告する report_if() / report_unless() もあります。
報告を調整する設定
withExceptions のクロージャ内では、次の設定もできます。必要なものだけ追加してください。例外クラス名はアプリに合わせて置き換えます。
| 目的 | 設定 |
|---|---|
| 特定の例外を報告しない(ページは通常どおり表示) | $exceptions->dontReport([IgnoredException::class]); |
| 例外の種類ごとにログレベルを変える(既定は error) | $exceptions->level(CriticalLevelException::class, LogLevel::CRITICAL); |
| 同じ例外インスタンスを1回だけ報告する | $exceptions->dontReportDuplicates(); |
| 報告の回数を制限する | $exceptions->throttle(...)(下の例) |
level() を使う場合は use Psr\Log\LogLevel; を、throttle() の例では use Illuminate\Cache\RateLimiting\Limit; を bootstrap/app.php の先頭に追加します。表の例外クラスを自分のアプリのクラスに置き換えた場合は、そのクラスの use 文(例: use App\Exceptions\IgnoredException;)も bootstrap/app.php の先頭に追加します。
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof ThrottledException) {
return Limit::perMinute(2);
}
});
この例では、ThrottledException を5回報告しても、ログに残るのは1分あたり2件までです。
例外ごとにレスポンスを変える(render コールバック)
$exceptions->render() にクロージャを渡すと、特定の例外に対するレスポンスを自分で返せます。withExceptions のクロージャ内に次を追加します(InvalidOrderException の use 文は前の節で追加済みです)。
$exceptions->render(function (InvalidOrderException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => $e->getMessage(),
'order_id' => $e->orderId,
], 400);
}
return response()->view('errors.invalid-order', [
'orderId' => $e->orderId,
], 400);
});
ビュー resources/views/errors/invalid-order.blade.php を作ります。
<h1>注文が不正です</h1>
<p>注文番号: {{ $orderId }}</p>
/invalid-order は 400 でこのビューを返します。Accept: application/json を付けると {"message": "Invalid order.", "order_id": 42} が返ります。
例外クラスに report() / render() を定義する
例外クラス自身に report() と render() メソッドを定義することもできます。app/Exceptions/OrderNotReadyException.php を作ります。
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
class OrderNotReadyException extends Exception
{
/**
* Report the exception (void => default logging is skipped).
*/
public function report(): void
{
Log::warning('OrderNotReadyException self-report: '.$this->getMessage());
}
/**
* Render the exception as an HTTP response.
*/
public function render(Request $request): Response
{
return response()->view('errors.order-not-ready', [
'message' => $this->getMessage(),
], 409);
}
}
ビュー resources/views/errors/order-not-ready.blade.php を作ります。
<h1>注文の準備ができていません</h1>
<p>{{ $message }}</p>
/order-not-ready は、render() が返したビューを 409 で返します。
ここで注意したいのが report() の戻り値です。例外クラスに report() があり、false 以外を返す場合(戻り値が void の場合も含む)、Laravel の既定のログ記録は行われません。上の例では、ログに残るのは report() 内で書いた WARNING の1行だけです。既定のログ記録も行いたい場合は、report() から false を返します。
public function report(): bool
{
Log::info('ConditionalReportException custom part');
return false;
}
abort(422) とバリデーションエラーは別物
abort(422) は単なる HTTP 例外で、4xx.blade.php があればそのページが表示されます。一方、$request->validate() が投げる本物の ValidationException はエラーページになりません。
- 通常のリクエスト: 元のページへリダイレクト(302)
- JSON のリクエスト: 422 と、
errorsを含む JSON
バリデーションエラーの扱いは【PHP】Laravelでフォームバリデーションを実装する方法を参照してください。
エラー処理をテストする
エラーページや JSON のエラーレスポンスは、Feature テストで確認できます。Exceptions::fake() を使うと、例外が報告されたかどうかも検証できます。フェイクに置き換えてもレスポンスは通常どおり描画され、dontReport の設定も反映されます。
tests/Feature/ErrorHandlingTest.php の例です。ここまでの節で作成したビュー、例外クラス、bootstrap/app.php の設定を前提にしています。
<?php
namespace Tests\Feature;
use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Exceptions;
use RuntimeException;
use Tests\TestCase;
class ErrorHandlingTest extends TestCase
{
public function test_custom_404_page_is_rendered(): void
{
$this->get('/no-such-page')
->assertStatus(404)
->assertSee('お探しのページは見つかりませんでした。');
}
public function test_framework_403_page_shows_abort_message(): void
{
$this->get('/forbidden')
->assertForbidden()
->assertSee('この操作は許可されていません。');
}
public function test_json_error_responses(): void
{
$this->getJson('/no-such-page')
->assertNotFound()
->assertJson(['message' => 'The route no-such-page could not be found.']);
$this->getJson('/api/posts/1')
->assertNotFound()
->assertExactJson(['message' => 'Record not found.']);
}
public function test_real_validation_exception_is_not_abort_422(): void
{
$this->getJson('/validate')
->assertStatus(422)
->assertJsonValidationErrors('name');
$this->get('/validate')->assertRedirect();
}
public function test_reported_exceptions_can_be_asserted_with_fake(): void
{
Exceptions::fake();
$this->get('/report-helper')->assertOk()->assertSee('continued after report()');
Exceptions::assertReported(RuntimeException::class);
Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'report helper demo');
Exceptions::assertReportedCount(1);
}
public function test_fake_still_renders_custom_response(): void
{
Exceptions::fake();
$this->get('/invalid-order')->assertStatus(400)->assertSee('注文番号: 42');
Exceptions::assertReported(fn (InvalidOrderException $e) => $e->orderId === 42);
}
}
/api/posts/1 はルートを定義していない URL です。api/* のため JSON の 404 になり、前述の render コールバックのメッセージが返ります。報告されていないことを確かめるには Exceptions::assertNotReported(...) を使います。
テストの基本は【PHP】Laravelでテストを実装する方法を参照してください。
php artisan test
bootstrap/app.php の全体
この記事で追加した設定のうち、stopIgnoring と「報告を調整する設定」を除いたものをまとめると、bootstrap/app.php は次のようになります。
<?php
use App\Exceptions\InvalidOrderException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
// 新規作成時からある設定
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
$exceptions->report(function (InvalidOrderException $e) {
Log::info('custom report callback: InvalidOrderException order='.$e->orderId);
});
$exceptions->context(fn () => [
'app_ctx' => 'demo',
]);
$exceptions->render(function (InvalidOrderException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => $e->getMessage(),
'order_id' => $e->orderId,
], 400);
}
return response()->view('errors.invalid-order', [
'orderId' => $e->orderId,
], 400);
});
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.',
], 404);
}
});
})->create();
よくあるつまずき
| 症状 | 原因と対処 |
|---|---|
4xx.blade.php を作ったのに 403 や 404 が変わらない |
401・402・403・404・419・429・500・503 はフォールバックの対象外。403.blade.php などを個別に作る |
5xx.blade.php を作ったのに例外の画面が変わらない |
キャッチされない例外は 500。500.blade.php を作る |
api/* で Accept ヘッダーが無いのに JSON が返る |
新規アプリの shouldRenderJsonWhen の設定による |
| JSON のエラーにモデルのクラス名や ID が出る | HTTP 例外のメッセージは APP_DEBUG=false でも返る。render コールバックで差し替える |
| 本番でエラーの詳細が表示される | APP_DEBUG が true になっている。本番では必ず false にする |
abort(500) や abort(404)・存在しない URL の 404 がログに残らない |
HTTP 例外は既定では報告されない。必要なら stopIgnoring(HttpException::class)。findOrFail() や暗黙のモデルバインディングによる 404(ModelNotFoundException)は、この設定でも記録されない |
例外クラスに report() を書いたら既定のログが消えた |
false 以外(void を含む)を返すと既定のログ記録は行われない。残すなら false を返す |
abort(422) でバリデーションエラーの表示にならない |
abort(422) は HTTP 例外。バリデーションは ValidationException で処理する |
| ログの時刻が9時間ずれている | 新規アプリの app.timezone は UTC |
Sentry などのエラー監視サービスとの連携や、ログチャンネルの詳しい設定はこの記事では扱いません。
まとめ
- 例外の報告と描画は
bootstrap/app.phpのwithExceptionsで設定する。新規アプリにはshouldRenderJsonWhenの設定が最初から入っている - 本番では
APP_DEBUGを必ず false にする。HTTP 例外はAPP_DEBUGに関係なくエラービューで表示される resources/views/errors/404.blade.phpなどでステータスごとのページを作れる。4xx/5xxのフォールバックは 401・402・403・404・419・429・500・503 には使われない- キャッチされない例外のページを変えるには
500.blade.phpを作る - JSON では
APP_DEBUG=falseでも HTTP 例外のメッセージが返る。必要ならrenderコールバックで差し替える - HTTP 例外は既定ではログに残らない。例外クラスの
report()がfalse以外を返すと既定のログ記録は行われない laravel.logの時刻は新規アプリでは UTCabort(422)とバリデーションのValidationExceptionは別物Exceptions::fake()で、報告された例外をテストで検証できる
関連記事
- 【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成
- 【PHP】LaravelでBladeコンポーネントとレイアウトを使う方法
- 【PHP】Laravelでフォームバリデーションを実装する方法
- 【PHP】LaravelでRESTful APIを作成する方法
- 【PHP】Laravelでミドルウェアを実装する方法
- 【PHP】Laravelでテストを実装する方法
- 【PHP】Laravelで認証と認可を実装する方法
- 【PHP】Laravel SanctumでAPIトークン認証を実装する方法