概要
- 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.phpprotected 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()メソッドはリクエストを受け取る前野状態にすでに実行されている模様である。
- 具体的には下記のタイミングで実行されている。
-
vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
に記載されているKernelクラスのbootstrap()
でアプリケーションの初期化を行っている。初期化処理の対象を定義しているプロパティの配列$bootstrappers
の中にHandleExceptions
がある。 -
HandleExceptions
クラスのbootstrap()
で下記の記載がある。set_exception_handler($this->forwardsTo('handleException'));
-
HandleExceptions
クラスのhandleException()
が実行される。下記のtryの中の処理が走る。try { $this->getExceptionHandler()->report($e); } catch (Exception) { // }
-
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); }
-
上記の処理でExceptionHandlerのインターフェースを指定している。ExceptionHandlerのインターフェースの実装である
vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler
がインスタンス化される。(当該のクラスがExceptionHandlerのインターフェースをimplementsしている。) -
インスタンス化されるときに
vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler
のコンストラクター(下記)が実行される。vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.phppublic function __construct(Container $container) { $this->container = $container; $this->register(); }
-
vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler
のregister()が実行される。vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.phppublic function register() { // }
-
「あれ?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); }
-
そして「ExceptionHandlerのインターフェースの実装である
vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler
がインスタンス化される。」と書いたがこれは間違いだった。vendor/laravel/framework/src/Illuminate/Contracts/Debug/ExceptionHandler
の実装はapp/Exceptions/Handler
にバインドされているらしい。 -
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 );
-
ちなみに、
bootstrap/app.php
はpublic/index.php
にて下記のように記載されており、requier_once
で一回だけ読み込まれている。public/index.php$app = require_once __DIR__.'/../bootstrap/app.php';
-
そして、
static::$app->make(ExceptionHandler::class);
でインスタンス化するとバインドされている実装のクラスをインスタンス化するらしい。 -
そのため
app/Exceptions/Handler
がインスタンス化され、インスタンス化時にapp/Exceptions/Handler
の継承元のvendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler
のコンストラクターが実行され、そのコンストラクターの中で$this->register();
になっているため結果的にapp/Exceptions/Handler
のregister()が呼ばれる。 -
これでアプリケーション初期化時に
app/Exceptions/Handler
のregister()が呼ばれる理由が理解できた。 -
だから
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に変更する。