[Laravel 5.7] レスポンス、ロギングを定義した例外の作成

Laravelの例外は、

- renderメソッド(レスポンスを生成する)
- reportメソッド(ロギングする）

を実装することで、例外をスローした時の処理をひとまとめにできます。
例としてスローされたとき以下の動作をする例外を作成してみます。

- 渡したビューをレスポンスとして返す
- スローされたときSlackに通知する

※SlackのIncoming Webhookを設定しておいてください。
https://readouble.com/laravel/5.7/ja/logging.html

# 新しい例外の定義

artisanコマンドで作成します。

```sh
$ php artisan make:exception SampleException
```

`App\Exception\SampleException.php`が作成されるので、開いて以下のようにします。

```php:app/Exceptions/SampleException.php
<?php

namespace App\Exceptions;

use Exception;
use Throwable;
use Illuminate\View\View;

class SampleException extends Exception
{
    protected $view;

    public function __construct(
        View $view,
        string $message = 'Sample Error',
        int $code = 0,
        Throwable $previous = null
    ) {
        $this->view = $view;
        parent::__construct($message, $code, $previous);
    }

    public function report()
    {
        // logger()はロガーにアクセスするヘルパ関数
        logger()->critical(
            $this->getMessage(),
            ['exception' => $this]
        );
        logger()->channel('slack')->critical('Sample Critical Error.');
    }

    public function render()
    {
        return $this->view;
    }
}
```

確認のため、`/sample`にアクセスしたら作成した例外が投げられるようにします。

```php:routes/web.php
use App\Exceptions\SampleException;

Route::get('/sample', function () {
    throw new SampleException(view('error', ['text' => 'Sample Critical Error']));
});
```

`/sample`にアクセスすると、ビューが返され、Slackにも通知されています。

ビュー
<img width="302" alt="スクリーンショット 2019-02-19 17.51.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/230795/e3109b25-ce4c-3d45-76ff-1a5e4b53b359.png">

Slack
<img width="253" alt="スクリーンショット 2019-02-19 17.52.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/230795/1e745ced-2fe0-82af-8c22-e797c1b0e295.png">

# [おまけ] 例外発生時の流れ

Laravelで発生した例外は`Illuminate/Foundation/Http/Kernel`クラスのhandleメソッドでキャッチされます。
（コンソールアプリケーションの場合は`Illuminate/Foundation/Console/Kernel`クラスです）。

```php:Illuminate/Foundation/Http/Kernel.php
public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        // ログに書き込み
        $this->reportException($e);
        // レスポンスの生成
        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}
```

reportException, renderExceptionメソッドはそれぞれ`App\Exceptions\Handler`クラスのreport, renderメソッドを呼び出します。

```php:Illuminate/Foundation/Http/Kernel.php
protected function reportException(Exception $e)
{
     // bootstrap/app.phpでApp\Exceptions\Handler::classがバインドされている
    $this->app[ExceptionHandler::class]->report($e);
}

protected function renderException($request, Exception $e)
{
    return $this->app[ExceptionHandler::class]->render($request, $e);
}
```

`App\Exceptions\Handler`クラスの親`Illuminate/Foundation/Exceptions/Handler`クラスのreport, renderメソッドを見ると・・・
色々分岐して処理が書かれていますが、レスポンスの返却、ロギングをしています。

```php:
/**
 *　ロギングする
 */
public function report(Exception $e)
{
    // ロギングしない例外なら何もしない
    if ($this->shouldntReport($e)) {
        return;
    }

    // 例外にreportメソッドがあれば実行
    if (method_exists($e, 'report')) {
        return $e->report();
    }

    // なければロガーを取得して・・・
    try {
        $logger = $this->container->make(LoggerInterface::class);
    } catch (Exception $ex) {
        throw $e;
    }

    // ここでロギング
    $logger->error(
        $e->getMessage(),
        array_merge($this->context(), ['exception' => $e]
    ));
}

/**
 *　例外にあったレスポンスを生成する
 */
public function render($request, Exception $e)
{
    // 例外がrenderメソッドかResponsableインターフェースを実装していれば、
    // その戻り値をレスポンスとして返す
    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);
}
```