Laravel6の例外処理の動作を確認したのでメモとして残しておく。
最新版については、リポジトリ等参のこと。
処理の流れ
ブラウザからのアクセスによって例外が発生した場合、Illuminate\Routing\Pipeline::handleException() で処理されるようだ。
※コンソール経由の場合は未確認
/**
* Handle the given exception.
*
* @param mixed $passable
* @param \Exception $e
* @return mixed
*
* @throws \Exception
*/
protected function handleException($passable, Exception $e)
{
if (! $this->container->bound(ExceptionHandler::class) ||
! $passable instanceof Request) {
throw $e;
}
$handler = $this->container->make(ExceptionHandler::class);
$handler->report($e);
$response = $handler->render($passable, $e);
if (method_exists($response, 'withException')) {
$response->withException($e);
}
return $response;
}
Laravelインストール直後の状態では$handlerに渡されるのは App\Exceptions\Handler になるので、
App\Exceptions\Handler::report()
public function report(Exception $exception)
{
parent::report($exception);
}
App\Exceptions\Handler::render()
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
の順序で実行される。
見ての通り、親クラスのreport() / render() を呼んでいるだけなので、
- カスタマイズしたい場合、App\Exceptions\Handler を修正
- インストール直後の状態ででのような動きをするのか、は親クラスの Illuminate\Foundation\Exceptions\Handler を見れば良い
となる。
次項で、Illuminate\Foundation\Exceptions\Handler の実装がどうなっているのか確認する。
デフォルト動作の確認(Illuminate\Foundation\Exceptions\Handler)
Illuminate\Foundation\Exceptions\Handler::report()
まずreport() について
下記ソースコードの通り。大雑把に言えば、
- 発生した例外が shouldntReport に含まれている場合は何もせず終了
- 発生した例外が report() メソッドを持つ場合、その結果を返す
- 上記いずれにも当てはまらない場合、Loggerでログを吐く
となる。
/**
* Report or log an exception.
*
* @param \Exception $e
* @return void
*
* @throws \Exception
*/
public function report(Exception $e)
{
if ($this->shouldntReport($e)) {
return;
}
if (is_callable($reportCallable = [$e, 'report'])) {
return $this->container->call($reportCallable);
}
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $e;
}
$logger->error(
$e->getMessage(),
array_merge(
$this->exceptionContext($e),
$this->context(),
['exception' => $e]
)
);
}
Illuminate\Foundation\Exceptions\Handler::render()
次に、Illuminate\Foundation\Exceptions\Handler::render() について。
下記ソースコードの通りだが、reportより少し複雑。大雑把には、
-
発生した例外がrender() または toResponse() を持つ場合、その結果を返す
-
いくつかの特定の例外に対しては、固有の処理を行う
※後述するが、Laravelフレームワーク側の話が多いので、通常はあまり気にする必要がない
-
上記いずれにも当てはまらない場合、$request->expectsJson() の結果に応じてprepareJsonResponse() またはprepareResponse() の結果を返す
※開発者が気にするのはだいたいの場合こっち
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Exception
*/
public function render($request, Exception $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
いくつかの特定の例外について
下記3種類の例外は、それぞれ固有の処理が行われているので注意。
Illuminate\Http\Exceptions\HttpResponseException
HttpResponseException型の例外が発生した場合は、例外クラスに実装されているgetResponse()の結果がレスポンスとして返る。
Illuminate\Auth\AuthenticationException
認証に失敗した場合。この例外の場合は、unauthenticated() に処理が渡され、ステータスコード401でレスポンスが返る。
https://readouble.com/laravel/6.x/ja/authentication.html
と合わせて理解しておくと良い。
Illuminate\Validation\ValidationException
バリデーションに失敗した場合。この例外の場合は、convertValidationExceptionToResponse() に処理が渡されて、入力内容とともに一つ前の画面にリダイレクトされる。
https://readouble.com/laravel/6.x/ja/validation.html#form-request-validation
と合わせて理解しておくと良い。
いくつかの特定の例外について(prepareException()で型が書き換えられるもの)
Illuminate\Database\Eloquent\ModelNotFoundException
DBのレコードが見つからない(findOrFail() でfailする)と発生するやつ。NotFoundHttpExceptionに書き換えられ、ステータスコード404でレスポンスが返る。
Illuminate\Auth\Access\AuthorizationException
許可されていない操作を実行しようとすると発生する例外。AccessDeniedHttpExceptionに書き換えられ、ステータスコード403でレスポンスが返る。
https://readouble.com/laravel/6.x/ja/authorization.html
この辺と合わせて理解しておくと良い
Illuminate\Session\TokenMismatchException
CSRFトークンの検証が失敗すると発生する例外。HttpExceptionに書き換えられ、ステータスコード419でレスポンスが返る。
https://readouble.com/laravel/6.x/ja/csrf.html
この辺とあわせて。
Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException
よくわからん。ホスト名が一致しないと発生する??ステータスコード404でレスポンスが返る。
https://www.larajapan.com/2019/07/01/changelog%E3%81%AF%E9%9D%A2%E7%99%BD%E3%81%84/
過去ステータスコードが 404 => 500 => 404 という経緯で変わってるらしく、この辺の話題ばっかりヒットする
prepareResponse() の動作について
設定値 APP_DEBUG により処理が分岐するので一応注意が必要。
発生した例外が HttpExceptionInterface を実装していない場合
- デバッグモードtrueならデバッグ用の画面が返る
- デバッグモードfalseならステータスコード500でレスポンスが返る
発生した例外が HttpExceptionInterface を実装している場合、例外に定義されている内容に従ってレスポンスが返る。
protected function prepareResponse($request, Exception $e)
{
if (! $this->isHttpException($e) && config('app.debug')) {
return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
}
if (! $this->isHttpException($e)) {
$e = new HttpException(500, $e->getMessage());
}
return $this->toIlluminateResponse(
$this->renderHttpException($e), $e
);
}