Slim 3(php)でカスタムExceptionを追加する

# 概要
* Slim 3に、カスタムのException（BadRequestException）を追加する
* BadRequestExceptionがthrowされた場合、HTTP Status Code 400とエラーメッセージを返す 
* Controller等でValidationがNGの場合、`throw BadRequestException('リクエストが不正です');`だけすればOKに

# 前置き
* 追加する前にSlimがどのようにExceptionをハンドリングしているか見てみる

## どこでExceptionがcatchされるか
* Controllerなどの内部で`try cache`がなければ、Slim\Appクラスのprocessメソッドでcachtされる
* handleExceptionメソッドが呼ばれる

```php:vender/slim/Slim/App.php
public function process(ServerRequestInterface $request, ResponseInterface $response)
{
    // ～省略～
    // Traverse middleware stack
    try {
        $response = $this->callMiddlewareStack($request, $response);
    } catch (Exception $e) {
        $response = $this->handleException($e, $request, $response); // ←ココ
    } catch (Throwable $e) {
        $response = $this->handlePhpError($e, $request, $response);
    }

    $response = $this->finalize($response);

    return $response;
}
```

* handleExceptionメソッドは、Exceptionのinstance毎に対応するコンテナに登録されているHandlerを呼ぶ
* NotAllowed、NotFound、SlimException以外のExceptionがthrowされた場合、`errorHandler`としてコンテナに登録されているHandlerが呼ばれる
* コンテナの`errorHandler`には、デフォルトでSlim\Handlers\Errorクラスが登録されている

```php:vender/slim/Slim/App.php
protected function handleException(Exception $e, ServerRequestInterface $request, ResponseInterface $response)
{
    if ($e instanceof MethodNotAllowedException) {
        $handler = 'notAllowedHandler';
        $params = [$e->getRequest(), $e->getResponse(), $e->getAllowedMethods()];
    } elseif ($e instanceof NotFoundException) {
        $handler = 'notFoundHandler';
        $params = [$e->getRequest(), $e->getResponse()];
    } elseif ($e instanceof SlimException) {
        // This is a Stop exception and contains the response
        return $e->getResponse();
    } else {
        // Other exception, use $request and $response params
        $handler = 'errorHandler';
        $params = [$request, $response, $e];
    }

    if ($this->container->has($handler)) {
        $callable = $this->container->get($handler);
        // Call the registered handler
        return call_user_func_array($callable, $params); // ←ここでコンテナのhandlerが呼ばれる
    }

    // No handlers found, so just throw the exception
    throw $e;
}
```
* なので、BadRequestExceptionをハンドリングするHandlerを作成し、コンテナの`errorHandler`に上書き登録しておけばよい

# 実装
前置きが長くなってしまったが、本題に入る
BadRequestExceptionクラスと`errorHandler`に登録するHandlerを作成する

## BadRequestExceptionの作成
* Exceptionを継承するだけ。今回は特に特別な処理は行わない

```php:app/exception/BadRequestException.php
<?php
namespace App\Exception;

use Exception;

class BadRequestException extends Exception
{
}
```

## Handlerの作成
* Slim\Handlers\Errorクラスを継承することで、BadRequest以外の処理は Slim\Handlers\Errorで処理されるようにする

```php:app/handler/error.php
<?php
namespace App\Handler;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use App\Exception\BadRequestException;

final class Error extends \Slim\Handlers\Error
{
    public function __construct($displayErrorDetails)
    {
        // エラーの詳細表示の有無設定(Slim\Handlers\Errorで必須)
        parent::__construct($displayErrorDetails);
    }
 
    public function __invoke(Request $request, Response $response, \Exception $e)
    {
        if ($e instanceof BadRequestException)
        {
            // Jsonで返す
            $response = $response
                ->withStatus(400)
                ->withHeader('Content-type', 'application/json')
                ->withJson(['message' => $e->getMessage()]);
        } else {
            // BadRequest以外のExceptionはSlim\Handlers\Errorへ
            $response = parent::__invoke($request, $response, $e);
        }
        return $response;
    }
}
```

## コンテナにHandlerを登録
* dependencies.phpにて、`errorHandler`にErrorクラスを上書き登録

```php:dependencies.php
// ↓を追加
$container['errorHandler'] = function ($c) {
    return new App\Handler\Error($c->get('settings')['displayErrorDetails']);
};
```
以上で、`throw BadRequestException($error_message);`をすれば、HTTP Status Code 400のResponseが返るはず。