laravel Handlerのregisterメソッドでちょっと詰まった

# 概要

- laravelの例外処理部分をAPIで扱うために修正していたら確認時に若干詰まったのでどう詰まったのか簡単にまとめる。

# 詰まるまでの経緯

- 下記のようにHandler.phpを記載した。

    ```Handler.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Exceptions;
    
    use App\Http\Resources\Common\BadRequest;
    use App\Http\Resources\Common\Forbidden;
    use App\Http\Resources\Common\InternalServerError;
    use App\Http\Resources\Common\NotFound;
    use App\Http\Resources\Common\Unauthorized;
    use Illuminate\Auth\Access\AuthorizationException;
    use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
    use Throwable;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
    use Illuminate\Validation\ValidationException;
    use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
    
    class Handler extends ExceptionHandler
    {
        /**
         * A list of exception types with their corresponding custom log levels.
         *
         * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
         */
        protected $levels = [
            //
        ];
    
        /**
         * A list of the exception types that are not reported.
         *
         * @var array<int, class-string<\Throwable>>
         */
        protected $dontReport = [
            //
        ];
    
        /**
         * A list of the inputs that are never flashed to the session on validation exceptions.
         *
         * @var array<int, string>
         */
        protected $dontFlash = [
            'current_password',
            'password',
            'password_confirmation',
        ];
    
        /**
         * Register the exception handling callbacks for the application.
         *
         * @return void
         */
        public function register()
        {
            $this->renderable(function (Throwable $e, Request $request) {
                if ($request->is('api/*')) {
                    // 400エラー
                    if ($e instanceof ValidationException) {
                        $badRequestResource = new BadRequest();
                        return $badRequestResource;
                    }
                    // 401エラー
                    if ($e instanceof UnauthorizedHttpException) {
                        $unauthorizedResource = new Unauthorized();
                        return $unauthorizedResource;
                    }
                    // 403エラー
                    if ($e instanceof AuthorizationException) {
                        $forbiddenResource = new Forbidden();
                        return $forbiddenResource;
                    }
                    // 404エラー
                    if ($e instanceof NotFoundHttpException) {
                        $notFoundResource = new NotFound();
                        return $notFoundResource;
                    }
                    // 上記のエラー以外 500エラー
                    $internalServerErrorResource = new InternalServerError();
                    return $internalServerErrorResource;
                }
            });
        }
    }
    ```

- 下記のようなfailedValidationメソッドを記載したリクエストクラスを用意して任意のコントローラーでルーティングに紐づけた。

    ```FooRequest.php
    protected function failedValidation(Validator $validator): void
    {
        $response['errors']  = $validator->errors()->toArray();

        throw new HttpResponseException(response()->json($response, Response::HTTP_BAD_REQUEST));
    }
    ```

- 本当に正しいレスポンスが返るかを確認するため、`dd()`を仕込んで、わざとバリデーションエラーになるような内容のリクエストを投げた。下記に`dd()`を付与したHandler.phpを記載する。

    ```Handler.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Exceptions;
    
    use App\Http\Resources\Common\BadRequest;
    use App\Http\Resources\Common\Forbidden;
    use App\Http\Resources\Common\InternalServerError;
    use App\Http\Resources\Common\NotFound;
    use App\Http\Resources\Common\Unauthorized;
    use Illuminate\Auth\Access\AuthorizationException;
    use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
    use Throwable;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
    use Illuminate\Validation\ValidationException;
    use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
    
    class Handler extends ExceptionHandler
    {
        /**
         * A list of exception types with their corresponding custom log levels.
         *
         * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
         */
        protected $levels = [
            //
        ];
    
        /**
         * A list of the exception types that are not reported.
         *
         * @var array<int, class-string<\Throwable>>
         */
        protected $dontReport = [
            //
        ];
    
        /**
         * A list of the inputs that are never flashed to the session on validation exceptions.
         *
         * @var array<int, string>
         */
        protected $dontFlash = [
            'current_password',
            'password',
            'password_confirmation',
        ];
    
        /**
         * Register the exception handling callbacks for the application.
         *
         * @return void
         */
        public function register()
        {
            $this->renderable(function (Throwable $e, Request $request) {
                if ($request->is('api/*')) {
                    // 400エラー
                    if ($e instanceof ValidationException) {
                        dd('400エラーだよ〜');
                        $badRequestResource = new BadRequest();
                        return $badRequestResource;
                    }
                    // 401エラー
                    if ($e instanceof UnauthorizedHttpException) {
                        $unauthorizedResource = new Unauthorized();
                        return $unauthorizedResource;
                    }
                    // 403エラー
                    if ($e instanceof AuthorizationException) {
                        $forbiddenResource = new Forbidden();
                        return $forbiddenResource;
                    }
                    // 404エラー
                    if ($e instanceof NotFoundHttpException) {
                        $notFoundResource = new NotFound();
                        return $notFoundResource;
                    }
                    // 上記のエラー以外 500エラー
                    $internalServerErrorResource = new InternalServerError();
                    return $internalServerErrorResource;
                }
            });
        }
    }
    ```

# 詰まった

- `dd()`で止まらない。
- しかし、`dd()`を記載する位置を下記のように`register()`の直下にすると停止する。

    ```Handler.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Exceptions;
    
    use App\Http\Resources\Common\BadRequest;
    use App\Http\Resources\Common\Forbidden;
    use App\Http\Resources\Common\InternalServerError;
    use App\Http\Resources\Common\NotFound;
    use App\Http\Resources\Common\Unauthorized;
    use Illuminate\Auth\Access\AuthorizationException;
    use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
    use Throwable;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
    use Illuminate\Validation\ValidationException;
    use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
    
    class Handler extends ExceptionHandler
    {
        /**
         * A list of exception types with their corresponding custom log levels.
         *
         * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
         */
        protected $levels = [
            //
        ];
    
        /**
         * A list of the exception types that are not reported.
         *
         * @var array<int, class-string<\Throwable>>
         */
        protected $dontReport = [
            //
        ];
    
        /**
         * A list of the inputs that are never flashed to the session on validation exceptions.
         *
         * @var array<int, string>
         */
        protected $dontFlash = [
            'current_password',
            'password',
            'password_confirmation',
        ];
    
        /**
         * Register the exception handling callbacks for the application.
         *
         * @return void
         */
        public function register()
        {
            dd('');
            $this->renderable(function (Throwable $e, Request $request) {
                if ($request->is('api/*')) {
                    // 400エラー
                    if ($e instanceof ValidationException) {
                        $badRequestResource = new BadRequest();
                        return $badRequestResource;
                    }
                    // 401エラー
                    if ($e instanceof UnauthorizedHttpException) {
                        $unauthorizedResource = new Unauthorized();
                        return $unauthorizedResource;
                    }
                    // 403エラー
                    if ($e instanceof AuthorizationException) {
                        $forbiddenResource = new Forbidden();
                        return $forbiddenResource;
                    }
                    // 404エラー
                    if ($e instanceof NotFoundHttpException) {
                        $notFoundResource = new NotFound();
                        return $notFoundResource;
                    }
                    // 上記のエラー以外 500エラー
                    $internalServerErrorResource = new InternalServerError();
                    return $internalServerErrorResource;
                }
            });
        }
    }
    ```

- 例外発生時に`$this->renderable()`が実行されていないことがわかった。しかし`register()`メソッド直下に`dd()`を記載したときに処理が停止するのは不可解である。

# 理由

## Handlerのregister()はリクエストを受け取る前に実行されている

- どうやらHandlerのregister()メソッドはリクエストを受け取る前野状態にすでに実行されている模様である。
- 具体的には下記のタイミングで実行されている。
    1. `vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php`に記載されているKernelクラスの`bootstrap()`でアプリケーションの初期化を行っている。初期化処理の対象を定義しているプロパティの配列`$bootstrappers`の中に`HandleExceptions`がある。
    1. `HandleExceptions`クラスの`bootstrap()`で下記の記載がある。

        ```php
        set_exception_handler($this->forwardsTo('handleException'));
        ```

    1. `HandleExceptions`クラスの`handleException()`が実行される。下記のtryの中の処理が走る。

        ```php
        try {
            $this->getExceptionHandler()->report($e);
        } catch (Exception) {
            //
        }
        ```

    1. `HandlerExceptions`クラスの`getExceptionHandler()`が実行される。
        ```vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php
        /**
         * Get an instance of the exception handler.
         *
         * @return \Illuminate\Contracts\Debug\ExceptionHandler
         */
        protected function getExceptionHandler()
        {
            return static::$app->make(ExceptionHandler::class);
        }
        ```

    1. 上記の処理でExceptionHandlerのインターフェースを指定している。ExceptionHandlerのインターフェースの実装である`vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler`がインスタンス化される。（当該のクラスがExceptionHandlerのインターフェースをimplementsしている。）
    1. インスタンス化されるときに`vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler`のコンストラクター（下記）が実行される。

        ```vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php
        public function __construct(Container $container)
        {
            $this->container = $container;

            $this->register();
        }
        ```

    1. `vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler`のregister()が実行される。

        ```vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php
        public function register()
        {
            //
        }
        ```

    1. 「あれ？register()中身ないじゃん」って思いがちだが、実はインスタンス化されるときに下記の様にインスタンス化されている。

        ```vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php
        /**
         * Get an instance of the exception handler.
         *
         * @return \Illuminate\Contracts\Debug\ExceptionHandler
         */
        protected function getExceptionHandler()
        {
            return static::$app->make(ExceptionHandler::class);
        }
        ```

    1. そして「ExceptionHandlerのインターフェースの実装である`vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler`がインスタンス化される。」と書いたがこれは間違いだった。`vendor/laravel/framework/src/Illuminate/Contracts/Debug/ExceptionHandler`の実装は`app/Exceptions/Handler`にバインドされているらしい。
    1. `vendor/laravel/framework/src/Illuminate/Contracts/Debug/ExceptionHandler`と`app/Exceptions/Handler`のバインドは`bootstrap/app.php`で行われており、下記のようにバインドされている。
        
        ```bootstrap/app.php
        /*
        |--------------------------------------------------------------------------
        | Bind Important Interfaces
        |--------------------------------------------------------------------------
        |
        | Next, we need to bind some important interfaces into the container so
        | we will be able to resolve them when needed. The kernels serve the
        | incoming requests to this application from both the web and CLI.
        |
        */
        
        $app->singleton(
            Illuminate\Contracts\Http\Kernel::class,
            App\Http\Kernel::class
        );
        
        $app->singleton(
            Illuminate\Contracts\Console\Kernel::class,
            App\Console\Kernel::class
        );
        
        $app->singleton(
            Illuminate\Contracts\Debug\ExceptionHandler::class,
            App\Exceptions\Handler::class
        );
        ```

    1. ちなみに、`bootstrap/app.php`は`public/index.php`にて下記のように記載されており、`requier_once`で一回だけ読み込まれている。

        ```public/index.php
        $app = require_once __DIR__.'/../bootstrap/app.php';
        ```

    1. そして、`static::$app->make(ExceptionHandler::class);`でインスタンス化するとバインドされている実装のクラスをインスタンス化するらしい。
    1. そのため`app/Exceptions/Handler`がインスタンス化され、インスタンス化時に`app/Exceptions/Handler`の継承元の`vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler`のコンストラクターが実行され、そのコンストラクターの中で`$this->register();`になっているため結果的に`app/Exceptions/Handler`のregister()が呼ばれる。
    1. これでアプリケーション初期化時に`app/Exceptions/Handler`のregister()が呼ばれる理由が理解できた。
    1. だから`app/Exceptions/Handler`のregister()直下にdd()を設置しても、例外が発生する前のアプリケーション初期化段階で止まってしまった。

## 総合して

- 下記のようにdd()を記載して、バリデーションエラーを起こしたときは、バリデーションエラーの例外でdd()で止まったわけではなく、アプリケーションの初期処理でdd()の部分のコードが実行されて止まった。

    ```
    public function register()
    {
        dd('');
        $this->renderable(function (Throwable $e, Request $request) {
            if ($request->is('api/*')) {
                // 400エラー
                if ($e instanceof ValidationException) {
                    $badRequestResource = new BadRequest();
                    return $badRequestResource;
                }
                // 401エラー
                if ($e instanceof UnauthorizedHttpException) {
                    $unauthorizedResource = new Unauthorized();
                    return $unauthorizedResource;
                }
                // 403エラー
                if ($e instanceof AuthorizationException) {
                    $forbiddenResource = new Forbidden();
                    return $forbiddenResource;
                }
                // 404エラー
                if ($e instanceof NotFoundHttpException) {
                    $notFoundResource = new NotFound();
                    return $notFoundResource;
                }
                // 上記のエラー以外 500エラー
                $internalServerErrorResource = new InternalServerError();
                return $internalServerErrorResource;
            }
        });
    }
    ```

- 下記のようにdd()を記載して、バリデーションエラーを起こしたときはHttpResponseExceptionが`app/Exceptions/Hanler.php`を通過しない特殊な例外だったためdd()の部分にはいらず止まらなかった。

    ```
   public function register()
    {
        dd('');
        $this->renderable(function (Throwable $e, Request $request) {
            if ($request->is('api/*')) {
                // 400エラー
                if ($e instanceof ValidationException) {
                    dd('400エラーだよ〜');
                    $badRequestResource = new BadRequest();
                    return $badRequestResource;
                }
                // 401エラー
                if ($e instanceof UnauthorizedHttpException) {
                    $unauthorizedResource = new Unauthorized();
                    return $unauthorizedResource;
                }
                // 403エラー
                if ($e instanceof AuthorizationException) {
                    $forbiddenResource = new Forbidden();
                    return $forbiddenResource;
                }
                // 404エラー
                if ($e instanceof NotFoundHttpException) {
                    $notFoundResource = new NotFound();
                    return $notFoundResource;
                }
                // 上記のエラー以外 500エラー
                $internalServerErrorResource = new InternalServerError();
                return $internalServerErrorResource;
            }
        });
    }
    ```

# 解決策

- リクエストクラスのバリデーションが失敗したときの例外をHttpResponseExceptionからValidationExceptionに変更する。