Laravelでのエラー処理の流れ
laravelではキャッチされなかった例外はvendor/laravel/framework/src/Illuminate/Routing/Pipeline::handleException
で処理をされます。
なぜこのクラスに処理がくるのかはこちらを参考に→Laravelの処理の流れコードリーディング(投げたエラーはどこで処理されている???)
このメソッドを見ることでlaravelでのエラー処理の流れを理解することができます。
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;
}
エラー処理の流れとしては、report()
してrender()
からのレスポンスを返却しているだけです。簡単ですね。
$this->container->make()
で実際に処理を任せるハンドラを生成しています。
コンテナのバインドはbootstrapで行われています。
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
上のコードから具体的な処理内容に関してはApp\Exceptions\Handler
に実装されているようです。
public function report(Exception $exception)
{
parent::report($exception);
}
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
継承しているIlluminate\Foundation\Exceptions\Handler
の処理を呼び出しているだけみたいです。
report
まずはreport()
を見ていきます。このメソッドでログなどを残す処理をしています。
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->context(), ['exception' => $e]
));
}
$this->shouldntReport()
でプロパティの$dontReport
、$internalDontReport
の中に書かれた例外に関してはreport処理を行わないようにしています。
is_callable()
で例外クラスにreport()
が定義されていればそのメソッドを呼ぶようにしています。例外クラスによってreport処理を変えたいときは使えそうです。
2つのifに当てはまらなかった場合はログインスタンスを取得してログを残す処理を行っています。
render
次にrender()
を見ていきます。エラーメッセージなどを利用してレスポンスを生成する処理を行います。
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);
}
最初のif文では、例外クラスにrender()
が実装されている、かつそのメソッドがレスポンスを返す場合にレスポンスを生成しています。
また、$e instanceof Responsable
ならば、こちらもすぐにレスポンスを返却するようにしています。
$this->prepareException()
では特定の例外クラスを別の例外クラスに詰め替えなおしています。
次のif文では特定の例外クラスならば、しかるべき処理をしてその場でレスポンスを返却しています。ValidationException
はまさにバリデーションエラーのときの例外です。少し見てみましょう。
protected function invalid($request, ValidationException $exception)
{
return redirect($exception->redirectTo ?? url()->previous())
->withInput(Arr::except($request->input(), $this->dontFlash))
->withErrors($exception->errors(), $exception->errorBag);
}
返却するレスポンスがJsonでないときは、invalid()
が呼ばれます。ただ単にinputの値などをセッションにいれて前ページにリダイレクトさせているだけです。
もとの処理に戻って最後のreturn文を見てみましょう。
public function render($request, Exception $e)
{
//...
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
返却する形式がJsonかそうでないかで分岐していますね。今回は$this->prepareResponse()
を見ていきましょう。
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
);
}
HttpException
でない、かつapp.debugがtrueに設定している場合は、$this->toIlluminateResponse()
でレスポンスを生成します。
Laravelの標準では、真っ白い画面にステータスコードとメッセージを出す本番用の画面と、エラーの内容を詳細に表示してくれる画面の2種類があります。
エラー画面は$this->convertExceptionToResponse()
でよしなにしてくれているのです。(実際の処理の分岐はrenderExceptionContent()
で行っています。)
しかしエラーがHttpException
(40x系エラーなど)だとapp.debugをtrueにしても詳細エラー画面は出してくれません。アカウント登録のメール認証でのmiddlewareが吐くInvalidSignatureException
がHttpException
を継承していてエラー画面が出ずに原因がわからず泣いたことがあります。
最後のreturn文を見てみます。
return $this->toIlluminateResponse(
$this->renderHttpException($e), $e
);
protected function renderHttpException(HttpExceptionInterface $e)
{
$this->registerErrorViewPaths();
if (view()->exists($view = "errors::{$e->getStatusCode()}")) {
return response()->view($view, [
'errors' => new ViewErrorBag,
'exception' => $e,
], $e->getStatusCode(), $e->getHeaders());
}
return $this->convertExceptionToResponse($e);
}
viewsディレクトリの中にerrorsディレクトリを作成し、その中に400.blade.php
のようにbladeファイルを用意しておけばステータスコード毎にエラー画面をカスタマイズできるようになります。
要ししてなければ$this->convertExceptionToResponse()
で真っ白い画面を出すようにレスポンスを返してくれます。
本番環境でステータスコードでなく、柔軟にエラー画面を出したいならば、このメソッドをApp\Exceptions\Handler
でオーバーライドしてごにょごにょするのがよさそうです。
まとめ
laravelでは実際のエラー処理は、Illuminate\Foundation\Exceptions\Handler
に実装されている。
カスタムがしたければ、app/Exceptions/Handler
に記述していけばよさそうです。
自分で作成した例外クラスにreport()
やrender()
を定義することによって柔軟にエラー処理を変更できそう。