Open6
Laravel 例外ハンドリング
本スクラップでの目的
- Laravelの例外ハンドリングの理解を深めること
- 例外ハンドリングの責務とcontrollerなどの責務を分離し、クリーンで保守性・可読性の高いコード設計を実現するためのプラクティスをつかむこと
// アンチパターン:
// 例外をキャッチしても何もしない
try {
// 何らかの処理
} catch (\Exception $e) {
}
// これではログにも残らず、問題が闇に葬られる
//--------------------------------------------------
// 改善策:
// ユーザーへの応答を制御しつつ、必ずログに記録する
try {
// 何らかの処理
} catch (\Exception $e) {
// reportヘルパ関数でログに記録
report($e);
// ユーザーにはエラーメッセージを表示
return back()->with('error', '処理中にエラーが発生しました。');
}
exceptionの補足をここでせず、throw したら、共通化した処理が行われるようにしたい。
<?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(/* ... */);
}
}
共通クラス
・メリット
責務分離
テスト用異性の向上
・デメリット
staticでは呼び出しが自由にできてしまう。
状態があるクラス定義にはならない(今回の場合)
Trait
メリット
・宣言で依存関係を定義できる
デメリット
・実際にどこに影響が生まれるかよみにくい(テストがもれていないことが重要)
helper
・メリット
定義が簡単
・デメリット
名前空間の汚染が発生する.
境界づけを考えたら、staticは向いていないかも。
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操作...
// ここで例外が起きたら自動でロールバックされる
});