0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

概要

  • 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がある。

    2. HandleExceptionsクラスのbootstrap()で下記の記載がある。

      set_exception_handler($this->forwardsTo('handleException'));
      
    3. HandleExceptionsクラスのhandleException()が実行される。下記のtryの中の処理が走る。

      try {
          $this->getExceptionHandler()->report($e);
      } catch (Exception) {
          //
      }
      
    4. 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);
      }
      
    5. 上記の処理でExceptionHandlerのインターフェースを指定している。ExceptionHandlerのインターフェースの実装であるvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handlerがインスタンス化される。(当該のクラスがExceptionHandlerのインターフェースをimplementsしている。)

    6. インスタンス化されるときに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();
      }
      
    7. vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handlerのregister()が実行される。

      vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php
      public function register()
      {
          //
      }
      
    8. 「あれ?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);
      }
      
    9. そして「ExceptionHandlerのインターフェースの実装であるvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handlerがインスタンス化される。」と書いたがこれは間違いだった。vendor/laravel/framework/src/Illuminate/Contracts/Debug/ExceptionHandlerの実装はapp/Exceptions/Handlerにバインドされているらしい。

    10. vendor/laravel/framework/src/Illuminate/Contracts/Debug/ExceptionHandlerapp/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
      );
      
    11. ちなみに、bootstrap/app.phppublic/index.phpにて下記のように記載されており、requier_onceで一回だけ読み込まれている。

      public/index.php
      $app = require_once __DIR__.'/../bootstrap/app.php';
      
    12. そして、static::$app->make(ExceptionHandler::class);でインスタンス化するとバインドされている実装のクラスをインスタンス化するらしい。

    13. そのためapp/Exceptions/Handlerがインスタンス化され、インスタンス化時にapp/Exceptions/Handlerの継承元のvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handlerのコンストラクターが実行され、そのコンストラクターの中で$this->register();になっているため結果的にapp/Exceptions/Handlerのregister()が呼ばれる。

    14. これでアプリケーション初期化時にapp/Exceptions/Handlerのregister()が呼ばれる理由が理解できた。

    15. だから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に変更する。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?