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と組み合わせた二段構えにします。