【CakePHP3】例外がthrowされた時の処理を追ってみた

## 追記
(今更ですが・・)

「Error例外が発生した場合はMiddlewareを貫通してしまう」という記述をしていますが、v3.5.7以降は修正がなされており、現行バージョンでは 「ThrowableならMiddleware内でcatchされる」という挙動になっています

https://github.com/cakephp/cakephp/pull/11462

---

業務でCakePHP3を用いたシステムを構築しています。
エラーを監視してSaaSに投げるようにしたかったのですが、適切なやり方が分からなかったので、調べてみました。
分かったことやムムムと感じた点がありましたので、まとめてみたいと思います。

※ PHP7環境での話になります。

## TL;DR
エラー/例外について、「ErrorHandlerMiddlewareに捕捉されるかされないか」で処理される流れが異なる、というのがポイントだと思いました。

1. `\Error` に関しては`set_exception_handler()`の設定を通じて、`BaseErrorHandler::handleError()` に渡される
2. `\Exception`に関しては`ErrorHandlerMiddleware`によって補足される
	* ∴ (必ずしも))`set_exception_handler()`の設定したハンドラーまで処理が到達する訳ではない
3. Shell処理内での例外については `set_exception_handler()`での設定内容に従う
4. 本来 `set_error_handler` や `register_shutdown_function` で扱われていた内容は、その通りに処理される
    * `BaseErrorHandler::register()` の内部で設定が行われる

概要については [book](https://book.cakephp.org/3.0/ja/development/errors.html) を参照です。
また、ミドルウェアに関するイメージは[コチラの記事](http://qiita.com/kozo/items/0d08a47307711c8f0382) がとてもイメージが掴みやすく、参考にさせて頂きました。

---

## エラーハンドリングに関する大まかな設定や処理の流れ
### Handlerの登録 in bootstrap.php
アプリケーションの起動フェーズで、以下のようにエラーハンドラーを登録しています。

source:https://github.com/cakephp/app/blob/c8a523898c03dfdec8057d8bd681b9a732f968cd/config/bootstrap.php#L119

```php
if ($isCli) {
    (new ConsoleErrorHandler(Configure::read('Error')))->register();
} else {
    (new ErrorHandler(Configure::read('Error')))->register();
}
```

`\Cake\Console\ConsoleErrorHandler\ConsoleErrorHandler`、`\Cake\Error\ErrorHandler`とも、 `\Cake\Error\BaseErrorHandler` を継承しています。


両者の大きな違いは「エラー出力をどのように行うか」が関心事であり[^1]、今回触れようとしている範囲においてハンドリングに関する決定的な差異は無いといって差し支えありません。
また、ここで現れた`register()`はBaseErrorHandlerの実装となります。

[^1]: 出力に関しての処理を司る`_displayError()` `_displayException()`に関しては、 `BaseErrorHandler` で抽象メソッドとして宣言されているため、サブクラスで実装を行う必要があります。

### `register()` 処理の中身を解剖
source: https://github.com/cakephp/cakephp/blob/003df77ed0c8582263ff7ab191136a8fa66b76fb/src/Error/BaseErrorHandler.php#L68
大まかに行われている仕事は4つです

* error_reporting
* set_error_handler
* set_exception_handler
* register_shutdown_function

要するにエラーの制御に関する設定と、エラー制御後のシャットダウン処理についての設定を行うことになります。

### 実際の流れ
#### エラーの場合[^3]
[^3]: `set_error_handler`の対象には[Error](http://php.net/manual/ja/class.error.php)は含まれませんでした。`set_exception_handler`で扱われました。(勘違いしてた・・・ 😖 => cf) [お前は PHP 7 における Fatal Error / Catchable Fatal Error / Error / ErrorException / Exception の違いを言えるか？ \- Qiita](http://qiita.com/mpyw/items/c69da9589e72ceac470c)

例えば、 `AppController::beforeFilter()`内で `trigger_error()`を呼び出して見た場合の処理は、以下のようになります。

```php
// App\Controller\AppController
public function beforeFilter(Event $event)
{
    trigger_error('errorが起きたよ！');
}
```

1. `AppController::beforeFilter()` → `trigger_error()`
2. `trigger_error()` → `BaseErrorHandler::handleError()`
	1. errorのレベルが `Fatal` なら `BaseErrorHandler::handleFatalError()`
		1. `handleFatalError()` → `BaseErrorHandler::handleException()`
		2. `handleException()` → `BaseErrorHandler::stop()`
		3. shutdown処理に入る
	2. errorのレベルが `Fatal`でなかったら `ErrorHandler::_displayError()`

つまり、 `set_error_handler()` で登録された通りのハンドラーに渡され、 (デフォルトの設定であれば)致命的なエラーでなければ復帰処理に入ります。

#### 例外の場合[^3]

例えば、 `AppController::beforeFilter()`内で `\Exception()`をthrowした場合は、以下のようになります。

1. `AppController::beforeFilter()` → `throw new \Exception()`
2. throw → `ErrorHandlerMiddleware::invoke()` 内での `catch (\Exception $e)`
3. catchブロック内で → `ErrorHandlerMiddleware::handleException()`
4. `handleException()` → `ExceptionRenderer::render()`
5. `ErrorHandler::logException()`
6. `return Response (\Psr\Http\Message\ResponseInterface)`

・・・・ 登録したexception handlerに処理が渡ってこないぞ・・？
##### web系の処理のときは、ErrorHandlerMiddlewareに処理が任される

エラー・例外ともにハンドラーをセットしているので、どこにも捕まらなかったものはセットされたハンドラーに引き渡されて処理される・・・というのを、個人的には期待していました。
しかし実態は、例外については、CakePHP全体の中でも最も外側に位置する部類の `ErrorHandlerMiddleware`内で捕捉されてしまっています。実質的には、アプリケーション内部から投げられた例外は(引き渡したかった相手である)ハンドラーにまでは渡ってきません。


#### `set_exception_handler()` の内容に処理が渡る場合
大まかにいえば、2つあると思います。
##### [PHP7のError例外](http://php.net/manual/ja/language.errors.php7.php) が発生した場合
強調のために`\Cake\Error\Middleware\` の内容を改めて見てみると、以下のようになっています。

```php
   public function __invoke($request, $response, $next)
    {
        try {
            return $next($request, $response);
        } catch (Exception $e) {
            return $this->handleException($e, $request, $response);
        }
    }
```
これが何を意味するかというと、 「ExceptionでないThrowableなクラスはキャッチされていない」ということです。
つまり、 `\Error` は(ミドルウェアを通り抜けて)直接 `set_exception_handler` に渡されて処理が実行されることになります。

##### Middlewareを介さない場合
ミドルウェアがキューされているのは `Application::middleware()`の中です。
Shell処理を行う場合はこの限りではないので、直接的に `set_excepton_handler` での設定どおりにハンドリングされることになります。
例えば`App\Shell\ConsoleShell::main()` 内で `\Exception`を投げた場合、以下のような流れになります。
※動作の確認用に、以下のように内容を書き換えています

```php
// \App\Shell\ConsoleShell
    public function main()
    {
        throw new \Exception('例外！！');
    }
// \Cake\Error\BaseErrorHandler
    public function register()
    {
        set_exception_handler(function ($t) {
            dd($t);
        });
    }
```

```
$ bin/cake console
/src/Error/BaseErrorHandler.php (line 87)
########## DEBUG ##########
object(Exception) {
	[protected] message => '例外！！'
	[protected] code => (int) 0
	[protected] file => '/home/me/app/src/Shell/consoleShell.php'
	[protected] line => (int) 35
	[private] string => ''
	[private] trace => [
		(int) 0 => [
			'file' => '/home/me/app/vendor/composer/cakephp/cakephp/src/Console/Shell.php',
			'line' => (int) 507,
			'function' => 'main',
			'class' => 'App\Shell\ConsoleShell',
			'type' => '->',
			'args' => []
		],
		(int) 1 => [
			'file' => '/home/me/app/vendor/composer/cakephp/cakephp/src/Console/ShellDispatcher.php',
			'line' => (int) 230,
			'function' => 'runCommand',
			'class' => 'Cake\Console\Shell',
			'type' => '->',
			'args' => [
				(int) 0 => [],
				(int) 1 => true,
				(int) 2 => []
			]
		],
		(int) 2 => [
			'file' => '/home/me/app/vendor/composer/cakephp/cakephp/src/Console/ShellDispatcher.php',
			'line' => (int) 182,
			'function' => '_dispatch',
			'class' => 'Cake\Console\ShellDispatcher',
			'type' => '->',
			'args' => [
				(int) 0 => []
			]
		],
		(int) 3 => [
			'file' => '/home/me/app/vendor/composer/cakephp/cakephp/src/Console/ShellDispatcher.php',
			'line' => (int) 128,
			'function' => 'dispatch',
			'class' => 'Cake\Console\ShellDispatcher',
			'type' => '->',
			'args' => [
				(int) 0 => []
			]
		],
		(int) 4 => [
			'file' => '/home/me/app/bin/cake.php',
			'line' => (int) 34,
			'function' => 'run',
			'class' => 'Cake\Console\ShellDispatcher',
			'type' => '::',
			'args' => [
				(int) 0 => [
					(int) 0 => '/home/me/app/bin/cake.php',
					(int) 1 => 'console'
				]
			]
		]
	]
	[private] previous => null
}
``` 

-----
### 「例外の処理を共通化する」には、どうすれば良いんだろう・・
この状況に対し、自分なりにしっくりとくる解法を見つける事がでできていません。[^2]
[^2]: 当初の目論見としては、BaseErrrorHandlerを継承する形で独自のErrorHandlerを定義、 `_logError()`及び `_logException()` をオーバーライドして「エラー内容をSasSに投げる」という目的を実現するつもりでした

いくつか方針を練っては見たのですが・・・

1. カスタムしたMiddlewareの実装を行って、その内部で例外をハンドリングするの？
  *  でもエラーに関しては「ミドルウェアの外側」まで到達して、 set_error_handlerで登録された処理に渡されるので何となくいびつ・・
2.  エラーハンドラーのカスタムを行って、エラーもMiddlewareレイヤーで補足する？
  * これやるとエラーレベルを問わず元の処理に復帰できない・・・

現状で取れる手立てとしては、ErrorHandlerとMiddlewareのhadleError()/handleException()どちらからも呼び出される共通の機構を別途に持たせる、というところになるのでしょうか。
どこかに実装例や知見がないかなぁ〜という感じです><