概要
- 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に変更する。