LoginSignup
4
0

CakePHPのエラーハンドリングについて調べてみた①(PHP本体のエラーハンドリング編)

Last updated at Posted at 2023-12-18

この記事はコネヒトアドベントカレンダー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つの関数を動かして違いを調べてみました

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エラー系
      • E_USER_ERROR
    • 警告系
      • E_USER_WARNING
      • E_USER_NOTICE
      • E_USER_DEPRECATED
  • その他
    • E_STRICT
      • 厳格なコード運用や互換性を維持するためのエラー。PHP7以降は廃止された
    • E_ALL
      • 全てのエラーと警告

※ドキュメントの説明を元に作成&一部表記を変えています。
上記を参考にしつつエラーハンドリング関数を見ていきます

定義済み定数
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)でエラーレベルを絞って確認してみました)

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0