set_error_handlerで警告を例外に変える、見逃しをなくす

set_error_handlerとは、警告や通知などのエラーを横取りするための関数です。ErrorExceptionに変換して投げれば、見逃しがちな警告をtry/catchで捕まえられます。

警告は放っておくと素通りする

PHPのエラーには段階があって、E_WARNING や E_NOTICE のような非致命的なものは、既定では処理を止めずにログや画面に出るだけで先へ進みます。これが曲者で、本来あってはいけない状態がメッセージ一行で流されてしまうんですよね。try/catch でも捕まえられないので、対処が後手に回りがちです。

$data = file_get_contents('/no/such/file'); // Warning が出るだけ
// $data は false、でも処理は止まらない
strlen($data); // ここで別の警告が連鎖する

set_error_handlerでErrorExceptionに変換する

そこで set_error_handler を使って、警告や通知を例外に昇格させると、try/catch で捕まえられるようになります。ErrorException に詰め替えて投げるのが定番の書き方ですね。こうすると「警告レベルの異常」を例外と同じ土俵で扱えます。

set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    // @ で抑制されている場合は素通りさせる
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new \ErrorException($message, 0, $severity, $file, $line);
});

try {
    $data = file_get_contents('/no/such/file');
} catch (\ErrorException $e) {
    $logger->error('読み込み失敗', ['msg' => $e->getMessage()]);
}

error_reporting との and を見ているのは、@ 演算子で意図的に抑制された箇所まで例外にしないためです。ここを省くと、抑制しているつもりの場所で落ちて驚くことになります。

致命的エラーはregister_shutdown_functionで拾う

set_error_handler は E_ERROR のような致命的エラーは捕まえられません。プロセスが即死する種類のものは register_shutdown_function で最後に拾う、という二段構えにしておくと取りこぼしが減ります。ここでログを残しておけば、原因不明の500の調査がだいぶ楽になります。

register_shutdown_function(function (): void {
    $err = error_get_last();
    if ($err !== null && ($err['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR))) {
        error_log("致命的: {$err['message']} @ {$err['file']}:{$err['line']}");
    }
});

まとめ

警告を例外に変えると、これまで素通りしていた異常が catch できる対象になり、見逃しがぐっと減ります。ただ@抑制の扱いには気をつけたいところですね。加えて致命的エラーは shutdown 関数で拾う二段構えにしておくと、本番で静かに壊れていた、という事態を避けやすくなると思います。

よくある質問

Q. なぜ警告を例外に変えるのですか?
A. E_WARNINGなどはtry/catchで捕まえられず素通りするので、ErrorExceptionに変換すると見逃しを防げます。

Q. @で抑制した箇所まで例外になりませんか?
A. error_reporting()と$severityのビット積を見て、抑制されている場合は素通りさせれば大丈夫です。

Q. 致命的エラーも捕まえられますか?
A. set_error_handlerではE_ERRORは拾えないので、register_shutdown_functionと組み合わせた二段構えにします。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です