追記
(今更ですが・・)
「Error例外が発生した場合はMiddlewareを貫通してしまう」という記述をしていますが、v3.5.7以降は修正がなされており、現行バージョンでは 「ThrowableならMiddleware内でcatchされる」という挙動になっています
業務でCakePHP3を用いたシステムを構築しています。
エラーを監視してSaaSに投げるようにしたかったのですが、適切なやり方が分からなかったので、調べてみました。
分かったことやムムムと感じた点がありましたので、まとめてみたいと思います。
※ PHP7環境での話になります。
TL;DR
エラー/例外について、「ErrorHandlerMiddlewareに捕捉されるかされないか」で処理される流れが異なる、というのがポイントだと思いました。
-
\Error
に関してはset_exception_handler()
の設定を通じて、BaseErrorHandler::handleError()
に渡される -
\Exception
に関してはErrorHandlerMiddleware
によって補足される- ∴ (必ずしも))
set_exception_handler()
の設定したハンドラーまで処理が到達する訳ではない
- ∴ (必ずしも))
- Shell処理内での例外については
set_exception_handler()
での設定内容に従う - 本来
set_error_handler
やregister_shutdown_function
で扱われていた内容は、その通りに処理される-
BaseErrorHandler::register()
の内部で設定が行われる
-
概要については book を参照です。
また、ミドルウェアに関するイメージはコチラの記事 がとてもイメージが掴みやすく、参考にさせて頂きました。
エラーハンドリングに関する大まかな設定や処理の流れ
Handlerの登録 in bootstrap.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の実装となります。
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
要するにエラーの制御に関する設定と、エラー制御後のシャットダウン処理についての設定を行うことになります。
実際の流れ
エラーの場合2
例えば、 AppController::beforeFilter()
内で trigger_error()
を呼び出して見た場合の処理は、以下のようになります。
// App\Controller\AppController
public function beforeFilter(Event $event)
{
trigger_error('errorが起きたよ!');
}
-
AppController::beforeFilter()
→trigger_error()
-
trigger_error()
→BaseErrorHandler::handleError()
- errorのレベルが
Fatal
ならBaseErrorHandler::handleFatalError()
-
handleFatalError()
→BaseErrorHandler::handleException()
-
handleException()
→BaseErrorHandler::stop()
- shutdown処理に入る
-
- errorのレベルが
Fatal
でなかったらErrorHandler::_displayError()
- errorのレベルが
つまり、 set_error_handler()
で登録された通りのハンドラーに渡され、 (デフォルトの設定であれば)致命的なエラーでなければ復帰処理に入ります。
例外の場合2
例えば、 AppController::beforeFilter()
内で \Exception()
をthrowした場合は、以下のようになります。
-
AppController::beforeFilter()
→throw new \Exception()
- throw →
ErrorHandlerMiddleware::invoke()
内でのcatch (\Exception $e)
- catchブロック内で →
ErrorHandlerMiddleware::handleException()
-
handleException()
→ExceptionRenderer::render()
ErrorHandler::logException()
return Response (\Psr\Http\Message\ResponseInterface)
・・・・ 登録したexception handlerに処理が渡ってこないぞ・・?
web系の処理のときは、ErrorHandlerMiddlewareに処理が任される
エラー・例外ともにハンドラーをセットしているので、どこにも捕まらなかったものはセットされたハンドラーに引き渡されて処理される・・・というのを、個人的には期待していました。
しかし実態は、例外については、CakePHP全体の中でも最も外側に位置する部類の ErrorHandlerMiddleware
内で捕捉されてしまっています。実質的には、アプリケーション内部から投げられた例外は(引き渡したかった相手である)ハンドラーにまでは渡ってきません。
set_exception_handler()
の内容に処理が渡る場合
大まかにいえば、2つあると思います。
PHP7のError例外 が発生した場合
強調のために\Cake\Error\Middleware\
の内容を改めて見てみると、以下のようになっています。
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
を投げた場合、以下のような流れになります。
※動作の確認用に、以下のように内容を書き換えています
// \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
}
「例外の処理を共通化する」には、どうすれば良いんだろう・・
この状況に対し、自分なりにしっくりとくる解法を見つける事がでできていません。3
いくつか方針を練っては見たのですが・・・
- カスタムしたMiddlewareの実装を行って、その内部で例外をハンドリングするの?
- でもエラーに関しては「ミドルウェアの外側」まで到達して、 set_error_handlerで登録された処理に渡されるので何となくいびつ・・
- エラーハンドラーのカスタムを行って、エラーもMiddlewareレイヤーで補足する?
- これやるとエラーレベルを問わず元の処理に復帰できない・・・
現状で取れる手立てとしては、ErrorHandlerとMiddlewareのhadleError()/handleException()どちらからも呼び出される共通の機構を別途に持たせる、というところになるのでしょうか。
どこかに実装例や知見がないかなぁ〜という感じです><
-
出力に関しての処理を司る
_displayError()
_displayException()
に関しては、BaseErrorHandler
で抽象メソッドとして宣言されているため、サブクラスで実装を行う必要があります。 ↩ -
set_error_handler
の対象にはErrorは含まれませんでした。set_exception_handler
で扱われました。(勘違いしてた・・・ 😖 => cf) お前は PHP 7 における Fatal Error / Catchable Fatal Error / Error / ErrorException / Exception の違いを言えるか? - Qiita ↩ ↩2 -
当初の目論見としては、BaseErrrorHandlerを継承する形で独自のErrorHandlerを定義、
_logError()
及び_logException()
をオーバーライドして「エラー内容をSasSに投げる」という目的を実現するつもりでした ↩