Laravel 例外処理 ハンドラーによしなにやってもらう

# Laravelにおける例外

Laravelでは独自の例外クラスがたくさん定義されているみたい。。。 以下記事見ればその量に驚くはず。 
> [例外の命名の参考にするために Laravel の例外すべて漁ってみた](https://zenn.dev/yumemi_inc/articles/7b62ba574011ce)

実際のPJによって、例外に対する扱い方は変わると思うので、めちゃくちゃ細かく分割するもよし、何個かに絞るのもよしだと思います。 この記事では、今僕がジョインしている案件での例外処理について少し書いてみます〜

# Handlerとは？？

Laravelに標準で設けられており、アプリケーション上で発生するあらゆるエラーを検知してくれるすごいやつです。(以下公式Doc)

> エラーと例外の処理は、新しいLaravelプロジェクトの開始時に最初から設定されています。App\Exceptions\Handlerクラスは、アプリケーションが投げるすべての例外がログに記録され、ユーザーへレンダーする場所です。このドキュメント全体を通して、このクラスについて詳しく説明します。

中身はこんな感じ

```PHP
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }
}
```

### registerメソッド

上記の通り、デフォルトで記述されているメソッドです。基本的にエラーハンドリングは出力系（ログ、slackでの通知とか）とレンダリング（jsonレスポンス作成、HTML返却）の2つをメインに行います。 使い方はこんな感じ、

```PHP
    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {
        // バリデーションエラー時にログ出力
        // reportableは出力系
        $this->reportable(function (ValidationException $e) {
           Log::error('Validation failed: ' . $e->getMessage());
        });
        
        // バリデーションエラー時にjson返却
        // renderableはレンダリング系
        $this->renderable(function (ValidationException $e) {
           return response()->json(['message' => $e->errors()], 422);
        });
    }
}
```

### renderメソッド

レンダリング（jsonレスポンス作成、HTML返却）メインのメソッドです。

```PHP
public function render($request, Throwable $e): JsonResponse
    {
        // バリデーション失敗時
        if ($e instanceof ValidationException) {
            return response()->json([
                'result' => 'error',
                'invalid-params' => $e->errors(),
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }

        $statusCode = match (true) {
            $e instanceof NotFoundHttpException => Response::HTTP_NOT_FOUND,
            $e instanceof UnauthorizedException => Response::HTTP_FORBIDDEN,
            default =>  Response::HTTP_INTERNAL_SERVER_ERROR,
        };

        return response()->json([
            'result' => 'error',
            'message' => $e->getMessage(),
        ], $statusCode);

    }
```

### reportメソッド

出力系（ログ、slackでの通知とか）メインのメソッドです。こんな感じ

```PHP
 public function report(Throwable $e)
    {
        if ($this->shouldReport($e)) {
	    // 必要な情報をメールやSlackに通知する処理を書く
        }

        parent::report($exception);
    }
```

shouldReportってなんぞ？？（さっきどこかでみた気が。。。） 絶対$dontReportをよしなにしてるやん。。。

```PHP
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];
```

ライブラリ側のソースをdigしていくと、、、 みつけた。（$dontReportであらかじめ定義しておいた例外以外にtrueをかえしてくれるよ）

<img width="700" alt="スクリーンショット 2023-08-18 21.17.29.png (34.5 kB)" src="https://hajimari-esa-custom-bucket.s3.ap-northeast-1.amazonaws.com/uploads/production/attachments/20872/2023/08/18/155412/da6ae31d-3275-4c6b-a0c3-9f315498d6bf.png">

なんで通知したい例外を定義さしてくれないんだろう？？ 多分だけど、量が単純に多すぎるので明示する手間がかかるのと、 例外ってシステムの内部情報までメッセージが出ちゃうからセキュリティ的に危ない的な感じだと思います。（色々理由はあると思うけど） なので外部に出したくない例外をあえて明示的にしているのでは。

話がそれました。　てな感じで色々便利メソッドが用意されています。

# 実際にPJで扱う例外

今回主に扱う例外は4種類です。
| 種類  | ステータスコード |
| --------------------- | ---------- |
| バリデーション関連                | 422     |
| Not Found（リソースが見つからない場合）               | 404     |
| Unauthorized             | 403     |
| その他のサーバーエラー             | 500     |


※それぞれエラーの細かい説明は省略します。

## 実際にどういう処理にしていくか

※MVCを前提に進めます システム構成はフロントがSPA（Next.js）で、LaravelサーバをRESTAPIとしており、バックエンドアーキテクチャはMVCSです。 メインの方針は、各レイヤーではログを出力（厳密な責務管理はしていない）して、最終的にコントローラー層でHTTP系エラーをスローし、Handlerに拾ってもらう流れです。

### Contoroller
```PHP
    /**
     * ログイン
     *
     * @param LoginRequest $request
     * @return JsonResponse
     */
    public function login(LoginRequest $request): JsonResponse
    {
        $params = $request->validated();

        if (Auth::attempt($params)) {
            return response()->json(new UserResource(Auth::user()), Response::HTTP_OK);
        } else {
            throw new UnauthorizedException('Unauthorized.');
        }
    }
```

### Request

FormRequestクラスを継承しており、バリデーション失敗時にデフォルトでHTMLレスポンスを返してしまうのでオーバーライドしています

```PHP
    /**
     * バリデーション失敗時
     *
     * @param Validator $validator
     * @return void
     * @throws ValidationException
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new ValidationException($validator);
    }

}
```

### Handler

先ほどのrenderの例が実はその実装になっています。

```PHP
    /**
     * @param $request
     * @param Throwable $e
     * @return JsonResponse
     */
    public function render($request, Throwable $e): JsonResponse
    {
        // バリデーション失敗時
        if ($e instanceof ValidationException) {
            return response()->json([
                'result' => 'error',
                'invalid-params' => $e->errors(),
            ], Response::HTTP_UNPROCESSABLE_ENTITY);
        }
        
        // ステータスコードを例外ごとに代入
        $statusCode = match (true) {
            $e instanceof NotFoundHttpException => Response::HTTP_NOT_FOUND,
            $e instanceof UnauthorizedException => Response::HTTP_FORBIDDEN,
            default =>  Response::HTTP_INTERNAL_SERVER_ERROR,
        };

        return response()->json([
            'result' => 'error',
            'message' => $e->getMessage(),
        ], $statusCode);

    }
```

# 最後に

HandlerでのエラーハンドリングはLaravelに完全に依存しているので、PJの特徴ごとに方針を決めていただけたらなと思います！！ 最後まで見ていただきありがとうございました！