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


## Laravelでのエラー処理の流れ

laravelではキャッチされなかった例外は`vendor/laravel/framework/src/Illuminate/Routing/Pipeline::handleException`で処理をされます。
なぜこのクラスに処理がくるのかはこちらを参考に→[Laravelの処理の流れコードリーディング（投げたエラーはどこで処理されている？？？）](https://qiita.com/wim/items/ba64e4c89a7310fa47dc)

このメソッドを見ることでlaravelでのエラー処理の流れを理解することができます。

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


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

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

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

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

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

```php:\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文を見てみましょう。

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

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

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


```php:\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の標準では、真っ白い画面にステータスコードとメッセージを出す本番用の画面と、エラーの内容を詳細に表示してくれる画面の２種類があります。

エラー画面は`$this->convertExceptionToResponse()`でよしなにしてくれているのです。（実際の処理の分岐は`renderExceptionContent()`で行っています。）

しかしエラーが`HttpException`（40x系エラーなど）だとapp.debugをtrueにしても詳細エラー画面は出してくれません。アカウント登録のメール認証でのmiddlewareが吐く`InvalidSignatureException`が`HttpException`を継承していてエラー画面が出ずに原因がわからず泣いたことがあります。

最後のreturn文を見てみます。

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