Open6

Laravel 例外ハンドリング

norishionorishio

本スクラップでの目的

  • Laravelの例外ハンドリングの理解を深めること
  • 例外ハンドリングの責務とcontrollerなどの責務を分離し、クリーンで保守性・可読性の高いコード設計を実現するためのプラクティスをつかむこと
norishionorishio
// アンチパターン:
// 例外をキャッチしても何もしない
try {
    // 何らかの処理
} catch (\Exception $e) {
    
}
// これではログにも残らず、問題が闇に葬られる

//-------------------------------------------------- 
// 改善策:
// ユーザーへの応答を制御しつつ、必ずログに記録する
try {
    // 何らかの処理
} catch (\Exception $e) {
    // reportヘルパ関数でログに記録
    report($e);

    // ユーザーにはエラーメッセージを表示
    return back()->with('error', '処理中にエラーが発生しました。');
}

exceptionの補足をここでせず、throw したら、共通化した処理が行われるようにしたい。

norishionorishio
<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class InvalidOrderException extends Exception
{
    /**
     * Report the exception.
     */
    public function report(): void
    {
        // ...
    }

    /**
     * Render the exception as an HTTP response.
     */
    public function render(Request $request): Response
    {
        return response(/* ... */);
    }
}
norishionorishio

共通クラス
・メリット
責務分離
テスト用異性の向上
・デメリット
staticでは呼び出しが自由にできてしまう。
状態があるクラス定義にはならない(今回の場合)

Trait
メリット
・宣言で依存関係を定義できる

デメリット
・実際にどこに影響が生まれるかよみにくい(テストがもれていないことが重要)

helper
・メリット
定義が簡単
・デメリット
 名前空間の汚染が発生する.

norishionorishio

1. Laravelのデフォルト例外について ✅

Laravelのdefaultのexceptionは、throwするだけでいい
Exceptionは、自動で投げられ、自動でrenderされる

Post::findOrFail($id);

Eloquentは自動的にModelNotFoundExceptionをthrowします。

2. カスタム例外について ✅ (補足あり)

カスタムは、自分で定義する(renderにreturnやログの定義)

class PaymentFailedException extends Exception
{
    /**
     * 例外をログに記録する (ログの定義)
     */
    public function report(): void
    {
        Log::channel('payment')->critical('決済に失敗しました: ' . $this->getMessage());
    }

    /**
     * 例外をHTTPレスポンスにレンダリングする (returnの定義)
     */
    public function render(Request $request): JsonResponse
    {
        return response()->json([
            'message' => '決済処理中にエラーが発生しました。',
        ], 422);
    }
}
  protected function prepareException(Throwable $e)
    {
        return match (true) {
            $e instanceof BackedEnumCaseNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
            $e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e),
            $e instanceof AuthorizationException && $e->hasStatus() => new HttpException(
                $e->status(), $e->response()?->message() ?: (Response::$statusTexts[$e->status()] ?? 'Whoops, looks like something went wrong.'), $e
            ),
            $e instanceof AuthorizationException && ! $e->hasStatus() => new AccessDeniedHttpException($e->getMessage(), $e),
            $e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),
            $e instanceof RequestExceptionInterface => new BadRequestHttpException('Bad request.', $e),
            $e instanceof RecordsNotFoundException => new NotFoundHttpException('Not found.', $e),
            default => $e,
        };
    }

3. DB::transactionクロージャについて ✅

トランザクション処理は、DB::transactionのクロージャ関数でやれば、try-catchは不要になる.

// ❌ 手動で書く場合
try {
    DB::beginTransaction();
    // ...複数のDB操作...
    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    throw $e; // 再度throwする必要がある
}

// ✅ クロージャを使う場合 (これだけでOK!)
DB::transaction(function () {
    // ...複数のDB操作...
    // ここで例外が起きたら自動でロールバックされる
});