【PHP】PHPフレームワーク「Flow」を試してみる～独自Exception編～

# 初めに
前回は[こちら](https://qiita.com/nnhkrnk/items/739ac12036c858525e23)の記事でマイナフレームワークFlowとdoctrineというORMを用い、DB接続を伴うAPIを作成しました。
今回はFlowにおける独自Exceptionの定義方法と、そのハンドリング方法を学んでいきます。

# Flow標準のExceptionHandler
Flowでは標準で2種類のExceptionHandlerクラスが用意されています。

* [DebugExceptionHandler](ProductionExceptionHandler)
* [ProductionExceptionHandler](https://github.com/neos/flow-development-collection/blob/27ffc953ae485033bed5354b20b45dbcfd37ce81/Neos.Flow/Classes/Error/ProductionExceptionHandler.php)

2つのHandlerクラスはどちらも[AbstractExceptionHandler](https://github.com/neos/flow-development-collection/blob/27ffc953ae485033bed5354b20b45dbcfd37ce81/Neos.Flow/Classes/Error/AbstractExceptionHandler.php)クラスを継承しています。
使われる環境が違うのみで、大きな作りは変わりません。

## AbstractExceptionHandler
エラーハンドリングクラスが継承する親クラスです。
throwされたExceptionはこのクラスの```handleException()```メソッドで拾われます。

処理の最後に記載されている```echoExceptionWeb($exception)```にて、最終的なアウトプットを作成しているようです。
```echoExceptionWeb()```はabstractとなっており、継承先で実装する必要があります。

```php:AbstractExceptionHandler
    /**
     * Handles the given exception
     *
     * @param \Throwable $exception The exception object
     * @return void
     */
    public function handleException($exception)
    {
        // Ignore if the error is suppressed by using the shut-up operator @
        if (error_reporting() === 0) {
            return;
        }

        $this->renderingOptions = $this->resolveCustomRenderingOptions($exception);

        $exceptionWasLogged = false;
        if ($this->throwableStorage instanceof ThrowableStorageInterface && isset($this->renderingOptions['logException']) && $this->renderingOptions['logException']) {
            $message = $this->throwableStorage->logThrowable($exception);
            $this->logger->critical($message);
            $exceptionWasLogged = true;
        }

        if (PHP_SAPI === 'cli') {
            $this->echoExceptionCli($exception, $exceptionWasLogged);
        }

        $this->echoExceptionWeb($exception);
    }

    /**
     * Echoes an exception for the web.
     *
     * @param \Throwable $exception
     * @return void
     */
    abstract protected function echoExceptionWeb($exception);
    
```

## 各Handlerクラス

各Handlerでは```echoExceptionWeb()```をオーバーライドする必要があります。

DebugExceptionHandlerは以下のように実装されていました。
少し長いですが、最終的にはHTMLファイルをechoで表示する作りになっているようです。


<details><summary>DebugExceptionHandlerのソース（長いので折り畳み）</summary>


```php:DebugExceptionHandler
class DebugExceptionHandler extends AbstractExceptionHandler
{
    /**
     * The template for the HTML Exception output.
     *
     * @var string
     */
    protected $htmlExceptionTemplate = <<<'EOD'
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
    <head>
        <title>%s</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <style>
        %s
        </style>
    </head>
    <body>
        %s
        <br />
        <details class="Flow-Debug-Exception-Backtrace-Code">
            <summary>Toggle backtrace code</summary>
            %s
        </details>
        <br />
        %s
        <script>
        %s
        </script>
    </body>
</html>
EOD;

    /**
     * Formats and echoes the exception as XHTML.
     *
     * @param \Throwable $exception
     * @return void
     */
    protected function echoExceptionWeb($exception)
    {
        $statusCode = ($exception instanceof WithHttpStatusInterface) ? $exception->getStatusCode() : 500;
        $statusMessage = ResponseInformationHelper::getStatusMessageByCode($statusCode);
        if (!headers_sent()) {
            header(sprintf('HTTP/1.1 %s %s', $statusCode, $statusMessage));
        }

        if ($this->useCustomErrorView() === false) {
            $this->renderStatically($statusCode, $exception);
            return;
        }

        try {
            echo $this->buildView($exception, $this->renderingOptions)->render();
        } catch (\Throwable $throwable) {
            $this->renderStatically($statusCode, $throwable);
        }
    }

    
    /**
     * Returns the statically rendered exception message
     *
     * @param integer $statusCode
     * @param \Throwable $exception
     * @return void
     */
    protected function renderStatically(int $statusCode, \Throwable $exception)
    {
        $statusMessage = ResponseInformationHelper::getStatusMessageByCode($statusCode);
        $exceptionHeader = '<div class="Flow-Debug-Exception-Header">';
        while (true) {
            $filepaths = Debugger::findProxyAndShortFilePath($exception->getFile());
            $filePathAndName = $filepaths['proxy'] !== '' ? $filepaths['proxy'] : $filepaths['short'];
            $exceptionMessageParts = $this->splitExceptionMessage($exception->getMessage());

            $exceptionHeader .= '<h1 class="ExceptionSubject">' . htmlspecialchars($exceptionMessageParts['subject']) . '</h1>';
            if ($exceptionMessageParts['body'] !== '') {
                $exceptionHeader .= '<p class="ExceptionBody">' . nl2br(htmlspecialchars($exceptionMessageParts['body'])) . '</p>';
            }

            $exceptionHeader .= '<table class="Flow-Debug-Exception-Meta"><tbody>';
            $exceptionHeader .= '<tr><th>Exception Code</th><td class="ExceptionProperty">' . $exception->getCode() . '</td></tr>';
            $exceptionHeader .= '<tr><th>Exception Type</th><td class="ExceptionProperty">' . get_class($exception) . '</td></tr>';
            if ($exception instanceof WithReferenceCodeInterface) {
                $exceptionHeader .= '<tr><th>Log Reference</th><td class="ExceptionProperty">' . $exception->getReferenceCode() . '</td></tr>';
            }

            $exceptionHeader .= '<tr><th>Thrown in File</th><td class="ExceptionProperty">' . $filePathAndName . '</td></tr>';
            $exceptionHeader .= '<tr><th>Line</th><td class="ExceptionProperty">' . $exception->getLine() . '</td></tr>';

            if ($filepaths['proxy'] !== '') {
                $exceptionHeader .= '<tr><th>Original File</th><td class="ExceptionProperty">' . $filepaths['short'] . '</td></tr>';
            }
            $exceptionHeader .= '</tbody></table>';

            if ($exception->getPrevious() === null) {
                break;
            }
            $exceptionHeader .= '<br /><h2>Nested Exception</h2>';
            $exception = $exception->getPrevious();
        }

        $exceptionHeader .= '</div>';

        $backtraceCode = Debugger::getBacktraceCode($exception->getTrace());

        $footer = '<div class="Flow-Debug-Exception-Footer">';
        $footer .= '<table class="Flow-Debug-Exception-InstanceData"><tbody>';
        if (defined('FLOW_PATH_ROOT')) {
            $footer .= '<tr><th>Instance root</th><td class="ExceptionProperty">' . FLOW_PATH_ROOT . '</td></tr>';
        }
        if (Bootstrap::$staticObjectManager instanceof ObjectManagerInterface) {
            $bootstrap = Bootstrap::$staticObjectManager->get(Bootstrap::class);
            $footer .= '<tr><th>Application Context</th><td class="ExceptionProperty">' . $bootstrap->getContext() . '</td></tr>';
            $footer .= '<tr><th>Request Handler</th><td class="ExceptionProperty">' . get_class($bootstrap->getActiveRequestHandler()) . '</td></tr>';
        }
        $footer .= '</tbody></table>';
        $footer .= '</div>';

        echo sprintf(
            $this->htmlExceptionTemplate,
            $statusCode . ' ' . $statusMessage,
            file_get_contents(__DIR__ . '/../../Resources/Public/Error/Exception.css'),
            $exceptionHeader,
            $backtraceCode,
            $footer,
            file_get_contents(__DIR__ . '/../../Resources/Public/Error/Exception.js')
        );
    }
}
```
</details>


## 実際にExceptionHandlerを使ってみる
ということで、実際にFlow標準のExceptionHandlerクラスを利用してみましょう。
新しくAPIを用意して、実際に呼び出してみます。

以下のようなControlerを用意しました。
Controller内でExceptionをthrowしてレスポンスがどうなるか確認してみました。

```php:TestExceptionHandlerController
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;

class TestExceptionHandlerController extends ActionController
{

    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @return void
     */
    public function testExceptionAction()
    {
        throw new \Exception('Standard Exception Occured!!!!');
        $this->view->assign('value', array(
            'bar', 'baz'
        ));
    }
}
```

APIを叩いたところ、DebugExceptionHandlerで定義されていたHTMLファイルがレスポンスとして返ってきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/1700daae-c47b-d54f-04b7-2eb5bf0186d9.png)


# 独自ExceptionHandlerクラスを作成する
続いてExceptionHandlerクラスを独自で作成してみます。

以下のようなレスポンスを返してみましょう。

* レスポンスはJson形式
* 以下の情報を返す
  * ステータスコード
  * ステータスコードに紐づくメッセージ
  * Exceptionのthrow時に設定したメッセージ

プロジェクト構成は以下です。
TestExceptionHandler.phpを作成し、Settings.Error.yamlに作成したHandlerを使用する旨を記載すればOKです。

```
Quickstart
    ├ Configration/
    |    ├ Development/
    |    |    └ Settings.Error.yaml（★）
    |    └ Settings.yaml
    └ Packages/
         ├ Application/
         |    └ Neos.Welcome/
         |         └ Classes/
         |              ├ Controller/
         |              |    └ TestExceptionHandlerController.php（★）
         |              └ Error/
         |                   └ TestExceptionHandler.php（★）
         ├ Framework/
         └ Libraries/
```

## 独自ExceptionHandlerクラス作成
AbstractExceptionHandlerを継承したTestExceptionHandlerクラスを作成しました。

json形式のレスポンスを返すために、以下の記述をしています。
* ```header('Content-Type: application/json');```でレスポンスの形式を指定する
* エラーレスポンスをarray型で宣言し、```json_encode()```してprintする


```php:TestExceptionHandler
<?php
namespace Neos\Welcome\Error;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Error\AbstractExceptionHandler;
use Neos\Flow\Http\Helper\ResponseInformationHelper;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Flow\Error\WithHttpStatusInterface;

class TestExceptionHandler extends AbstractExceptionHandler
{
    /**
     * Echoes the exception in a web context
     *
     * @param \Exception $exception The exception to echo
     * @return void
     */
    protected function echoExceptionWeb($exception)
    {
        // Add your implementation here

        
        $statusCode = ($exception instanceof WithHttpStatusInterface) ? $exception->getStatusCode() : 500;
        $statusMessage = ResponseInformationHelper::getStatusMessageByCode($statusCode);
        if (!headers_sent()) {
            header(sprintf('HTTP/1.1 %s %s', $statusCode, $statusMessage));
            header('Content-Type: application/json');
        }

        $response = array(
            'error' => array(
                'code' => $statusCode,
                'message' => $statusMessage,
                'exception' => $exception->getMessage(),
                'test' => 'Original ExceptionHandler!!!!'
            )
        );
        
        print json_encode($response);

        
    }

    public function handleException($exception)
    {
        $this->echoExceptionWeb($exception);
    }
}

```

## Settings.Error.yamlの記述
設定ファイルに作成したExceptionHandlerを利用する旨を記述していきます。
Setting.Error.yamlファイルを作成し、以下のように記述しました。
```yaml:Settings.Error.yaml
Neos:
  Flow:
    error:
      exceptionHandler:
        className: 'Neos\Welcome\Error\TestExceptionHandler'

```

:::note warn
Configration直下のSetting.yamlに記述をしても問題ないはずなのですが、私の環境ではうまく動作しませんでした。そのためConfigration/Development配下のSetting.Error.yamlに記述しています。
ちなみに、Setting.Error.yamlはSetting.yamlという名前にしても問題なく動きます。
:::

## 独自ExceptionHandlerクラスの動作確認
先ほど作成したAPIを呼び出したところ、定義したレスポンスが返ってきました。

```sh
$ curl http://localhost:8081/Neos.Welcome/TestExceptionHandler/testException
{"error":{"code":500,"message":"Internal Server Error","exception":"Standard Exception Occured!!!!","test":"Original ExceptionHandler!!!!"}}
```


# 独自のExceptionクラスを作成する
最後に、独自のExceptionクラスを作成してみます。

TestExceptionクラスを新規で作成してControllerからthrowしてみます。

```
Quickstart
    ├ Configration/
    |    ├ Development/
    |    |    └ Settings.Error.yaml
    |    └ Settings.yaml
    └ Packages/
         ├ Application/
         |    └ Neos.Welcome/
         |         └ Classes/
         |              ├ Controller/
         |              |    └ TestExceptionHandlerController.php（★）
         |              ├ Error/
         |              |    └ TestExceptionHandler.php
         |              └ Exception/
         |                   └ TestException.php（★）
         ├ Framework/
         └ Libraries/
```

## 独自Exceptionクラス作成
独自Exceptionクラスを以下のように作成しました。
ポイントは以下です。

* Neos\Flow\Exceptionクラスを継承
* ```$statusCode```にこのExceptionが出た場合に返したいステータスコードを設定（今回はわかりやすく505にしました）


```php:TestException
<?php
namespace Neos\Welcome\Exception;

use Neos\Flow\Exception;

class TestException extends Exception
{
    /**
     * @var integer
     */
    protected $statusCode = 505;

}

```

## 独自Exceptionクラスをthrowする
Controllerの中で独自Exceptionクラスをthrowするように修正します。

```php:TestExceptionHandlerController
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\Welcome\Exception\TestException;

class TestExceptionHandlerController extends ActionController
{

    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @return void
     */
    public function testExceptionAction()
    {
        throw new TestException('Original Exception Occured!!!!');
        $this->view->assign('value', array(
            'bar', 'baz'
        ));
    }
}
```

## 独自Exceptionクラスの動作確認
実際にAPIを呼び出して確認したところ、独自Exceptionクラスで指定したステータスコードを返却していることが確認できました。

```sh
$ curl http://localhost:8081/Neos.Welcome/TestExceptionHandler/testException
{"error":{"code":505,"message":"HTTP Version Not Supported","exception":"Original Exception Occured!!!!","test":"Original ExceptionHandler!!!!"}}
```



# 終わりに
今回はFlowにおける独自Exceptionの設定についてアウトプットしました。
独自Exceptionクラスができれば開発の幅がぐんと広がりそうです。
また何か分かったことがあれば定期的にアウトプットしていきます。


# 参考資料
https://flowframework.readthedocs.io/en/stable/Quickstart/index.html