この記事はコネヒトアドベントカレンダー19日目の記事です
CakePHP4.4以降ではエラーハンドリングの機構が大きく変わりました。
ErrorHandler と ConsoleErrorHandler クラスは、非推奨となりました。 これらのクラスは、新しい ExceptionTrap と ErrorTrap クラスに置き換わりました
そのためCakePHPのエラーハンドリングを新しいクラスに置き換える必要が出てきたのですが、前提としてPHP本体のエラーについての理解が必要だったので自分なりに調べてみました。
# 実行環境
# php -v
PHP 8.1.25 (cli) (built: Nov 1 2023 12:51:36) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.25, Copyright (c) Zend Technologies
with Xdebug v3.2.2, Copyright (c) 2002-2023, by Derick Rethans
# cat vendor/cakephp/cakephp/VERSION.txt
4.3.11
サンプルソース
https://github.com/okdyy75/dev-cakephp/blob/cake43-errHandle/www/cakephp/sample.php
まずはCakePHPのエラーハンドリングについて調べてみる
CakePHPのエラーハンドリングについては下記の通りconfig/bootstrap.php
で行っているようでした
$isCli = PHP_SAPI === 'cli';
if ($isCli) {
(new ConsoleErrorHandler(Configure::read('Error')))->register();
} else {
(new ErrorHandler(Configure::read('Error')))->register();
}
この二つのクラスですが、継承元は同じ BaseErrorHandler
なので register()
の中身を見ていきます
public function register(): void
{
$level = $this->_config['errorLevel'] ?? -1;
error_reporting($level);
set_error_handler([$this, 'handleError'], $level);
set_exception_handler([$this, 'handleException']);
register_shutdown_function(function (): void {
if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && $this->_handled) {
return;
}
$megabytes = $this->_config['extraFatalErrorMemory'] ?? 4;
if ($megabytes > 0) {
$this->increaseMemoryLimit($megabytes * 1024);
}
$error = error_get_last();
if (!is_array($error)) {
return;
}
$fatals = [
E_USER_ERROR,
E_ERROR,
E_PARSE,
];
if (!in_array($error['type'], $fatals, true)) {
return;
}
$this->handleFatalError(
$error['type'],
$error['message'],
$error['file'],
$error['line']
);
});
}
register()
時の処理は大きく分けて3つあり、PHP標準の関数を使用したエラーハンドリングでした。実際にこの3つの関数を動かして違いを調べてみました
- set_error_handler
- ユーザー定義のエラーハンドラ関数を設定する
- https://www.php.net/manual/ja/function.set-error-handler.php
- set_exception_handler
- ユーザー定義の例外ハンドラ関数を設定する
- https://www.php.net/manual/ja/function.set-error-handler.php
- register_shutdown_function
- シャットダウン時に実行する関数を登録する
- https://www.php.net/manual/ja/function.register-shutdown-function.php
PHPのエラーハンドリング
そもそもPHPのエラーについて
前提としてPHPには16種類ほどのエラーレベルが定数で定義されており、ざっくり分けると下記の通りになります
- PHP本体の内部エラー
- fatalエラー系
- E_ERROR
- E_PARSE
- E_CORE_ERROR
- E_COMPILE_ERROR
- E_RECOVERABLE_ERROR
- 警告系
- E_WARNING
- E_NOTICE
- E_CORE_WARNING
- E_COMPILE_WARNING
- E_DEPRECATED
- fatalエラー系
- ユーザーによって発行されるエラー
- fatalエラー系
- E_USER_ERROR
- 警告系
- E_USER_WARNING
- E_USER_NOTICE
- E_USER_DEPRECATED
- fatalエラー系
- その他
- E_STRICT
- 厳格なコード運用や互換性を維持するためのエラー。PHP7以降は廃止された
- E_ALL
- 全てのエラーと警告
- E_STRICT
※ドキュメントの説明を元に作成&一部表記を変えています。
上記を参考にしつつエラーハンドリング関数を見ていきます
定義済み定数
https://www.php.net/manual/ja/errorfunc.constants.php
set_error_handler
WarningやNoticeといったそのまま処理が継続できる警告系のエラーをハンドリングできます
- エラーレベルとしてはE_WARNINGやE_NOTICE、E_DEPRECATEDなど
-
trigger_error()
でユーザーエラーを発生させて、それをハンドリングすることもできる(E_USER_XXX系) - エラー発生後コールバック関数が呼ばれた後も、エラーが発生した次の行から処理は継続する
set_error_handler
https://www.php.net/manual/ja/function.set-error-handler.php
set_error_handler検証コード
function handleError(int $code, string $description, ?string $file = null, ?int $line = null)
{
var_dump(__METHOD__ . __LINE__);
var_dump($code, $description, $file, $line);
}
function handleException(Throwable $exception)
{
var_dump(__METHOD__ . __LINE__);
var_dump($exception);
}
function shutdown()
{
var_dump(__METHOD__ . __LINE__);
$error = error_get_last();
var_dump($error);
}
set_error_handler('handleError');
set_exception_handler('handleException');
register_shutdown_function('shutdown');
// set_error_handler()で捕捉できるエラー例
$hoge = $a;
// Warning: Undefined variable $a in /var/www/cakephp/sample.php on line 27
ob_clean();
// Notice: ob_clean(): Failed to delete buffer. No buffer to delete in /var/www/cakephp/sample.php on line 28
$arr = [PHP_INT_MAX + 1 => 'hoge'];
// Deprecated: Implicit conversion from float 9.223372036854776E+18 to int loses precision in /var/www/cakephp/sample.php on line 29
trigger_error('E_USER_DEPRECATEDです', E_USER_DEPRECATED);
// Deprecated: E_USER_DEPRECATEDです in /var/www/cakephp/sample.php on line 31
trigger_error('E_USER_NOTICEです', E_USER_NOTICE);
// Notice: E_USER_NOTICEです in /var/www/cakephp/sample.php on line 32
trigger_error('E_USER_WARNINGです', E_USER_WARNING);
// Warning: E_USER_WARNINGです in /var/www/cakephp/sample.php on line 33
trigger_error('E_USER_ERRORです', E_USER_ERROR);
// Fatal error: E_USER_ERRORです in /var/www/cakephp/sample.php on line 34
echo "---end---\n";
実際に検証してみたところ
- Deprecated、Notice、Warning等のエラーは
set_error_handler()
でハンドリングしなくても処理は継続される -
trigger_error()
でE_USER_ERRORを発生させた場合は少し特殊で、Fatal errorと同様にその場で処理が終了する。ただし でハンドリングすればそのまま処理を継続する事が可能
set_exception_handler
ExceptionやErrorといったtry/catchすれば処理の継続が可能なfatalエラーをハンドリングできる
- より具体的にはThrowableを実装したErrorやExceptionといったキャッチされなかった例外をハンドリングできる
- エラーレベルとしては旧来のE_ERRORやE_PARSE、E_RECOVERABLE_ERRORなど
- 「旧来」というのもPHP7以降ではfatal error や recoverable fatal error の多くが、Errorの例外に変換されるようになったため
- ただ全てのfatal errorをハンドリングできる訳ではないので後述の
register_shutdown_function()
で処理する必要がある- 例えば実行時間が長すぎる場合や、メモリ不足など
- エラー発生後コールバック関数が呼ばれた後は処理が終了する
定義済みの例外
https://www.php.net/manual/ja/reserved.exceptions.php
set_exception_handler
https://www.php.net/manual/ja/function.set-exception-handler.php
set_exception_handler検証コード
function handleError(int $code, string $description, ?string $file = null, ?int $line = null)
{
var_dump(__METHOD__ . __LINE__);
var_dump($code, $description, $file, $line);
}
function handleException(Throwable $exception)
{
var_dump(__METHOD__ . __LINE__);
var_dump($exception);
}
function shutdown()
{
var_dump(__METHOD__ . __LINE__);
$error = error_get_last();
var_dump($error);
}
set_error_handler('handleError');
set_exception_handler('handleException');
register_shutdown_function('shutdown');
// set_exception_handler()で捕捉できるエラー例
throw new Exception('Exceptionです');
// Fatal error: Uncaught Exception: Exceptionです in /var/www/cakephp/sample.php:44
$hoge = 1 / 0;
// Fatal error: Uncaught DivisionByZeroError: Division by zero in /var/www/cakephp/sample.php:46
aaa();
// Fatal error: Uncaught Error: Call to undefined function aaa() in /var/www/cakephp/sample.php:48
eval("echo 'hoge' echo 'fuga'");
// Parse error: syntax error, unexpected token "echo", expecting "," or ";" in /var/www/cakephp/sample.php(50) : eval()'d code on line 1
echo "---end---\n";
register_shutdown_functionについて
例外としてtry/catchできない処理の継続が困難なfatalエラーを最後にハンドリングできる
- エラーレベルとしてはE_ERRORやE_COMPILE_ERRORなど
- エラー発生後コールバック関数が呼ばれた後は処理が終了する
register_shutdown_function検証コード
function handleError(int $code, string $description, ?string $file = null, ?int $line = null)
{
var_dump(__METHOD__ . __LINE__);
var_dump($code, $description, $file, $line);
}
function handleException(Throwable $exception)
{
var_dump(__METHOD__ . __LINE__);
var_dump($exception);
}
function shutdown()
{
var_dump(__METHOD__ . __LINE__);
$error = error_get_last();
var_dump($error);
}
set_error_handler('handleError');
set_exception_handler('handleException');
register_shutdown_function('shutdown');
// register_shutdown_function()で捕捉できるエラー例
ini_set("max_execution_time", "1");for(;;){};
// Fatal error: Maximum execution time of 1 second exceeded in /var/www/cakephp/sample.php on line 54
str_repeat("aaa", PHP_INT_MAX);
// Fatal error: Possible integer overflow in memory allocation (3 * 9223372036854775807 + 32) in /var/www/cakephp/sample.php on line 57
eval('function foo($a, $b, $unused, $unused) {}');
// Fatal error: Redefinition of parameter $unused in /var/www/cakephp/sample.php(60) : eval()'d code on line 1
echo "---end---\n";
3つの関数の違いについて
まとめると
- set_error_handler
- WarningやNoticeといったそのまま処理が継続できる警告系のエラーをハンドリングできる
- set_exception_handler
- ExceptionやErrorといったtry/catchすれば処理の継続が可能なfatalエラーをハンドリングできる
- register_shutdown_function
- 例外としてtry/catchできない処理の継続が困難なfatalエラーを最後にハンドリングできる
いろいろ調べてみましたが、同じE_ERRORレベルであってもset_exception_handler()
で処理できる場合もあれば、register_shutdown_function()
でなければ処理できない場合もあるようで、エラーレベルごとにこのエラーハンドリング関数で処理されるとは一概に言えないようです。(error_reporting(E_ERROR)
でエラーレベルを絞って確認してみました)