LoginSignup
17
17

More than 3 years have passed since last update.

Laravelでのエラー処理の流れを把握したいのでソースコードリーディング

Posted at

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で行われています。

bootstrap/app.php
$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

上のコードから具体的な処理内容に関してはApp\Exceptions\Handlerに実装されているようです。

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()を見ていきます。このメソッドでログなどを残す処理をしています。

\Illuminate\Foundation\Exceptions\Handler
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()を見ていきます。エラーメッセージなどを利用してレスポンスを生成する処理を行います。

\Illuminate\Foundation\Exceptions\Handler
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はまさにバリデーションエラーのときの例外です。少し見てみましょう。

\Illuminate\Foundation\Exceptions\Handler
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文を見てみましょう。

\Illuminate\Foundation\Exceptions\Handler
public function render($request, Exception $e)
{
    //...

    return $request->expectsJson()
                    ? $this->prepareJsonResponse($request, $e)
                    : $this->prepareResponse($request, $e);
}

返却する形式がJsonかそうでないかで分岐していますね。今回は$this->prepareResponse()を見ていきましょう。

\Illuminate\Foundation\Exceptions\Handler
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が吐くInvalidSignatureExceptionHttpExceptionを継承していてエラー画面が出ずに原因がわからず泣いたことがあります。

最後の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()を定義することによって柔軟にエラー処理を変更できそう。

17
17
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
17
17