PHPのset_erorr_handlerとregister_shutdown_functionとob関数について ( エラーを整形出力したい )

いきなり今回のまとめ

例によって地味なPHP記事です。最初に今回で分かったことのまとめ。

  • 処理が続行可能なエラーはset_error_handlerで捕捉できる
  • Fatal Errorはset_error_handlerでは捕捉できない
  • Fatal Errorはregister_shutdown_functionで捕捉できる
  • shutdown処理の中に、ob関数(output buffering)があると、Fatal Errorの出力より先にshutdown処理が実行される
  • shutdown処理の中では、debug_backtraceは使えない

今回は、タイトルにもある通り、エラーを整形して最後に出力したいというのが当初のメインテーマです。それを実装する中で、上のようなことがわかりましたので、それらについて以下で書いていきます。

追記 ( 2011-04-06 )

こしあんさんがコードとしてまとめてくれました! 続きはGistで!

. @ahomu さんのこの記事 http://j.mp/e8db9R (set_error_handlerとかの件)に関して調べてみて gist に上げてみた https://gist.github.com/903173 (今更過ぎるけどwTue Apr 05 07:37:01 via Saezuri

エラーメッセージの自動送出を抑え、レスポンスボディの末尾に出力したい

整形のことは考えずに、とにかくレスポンスボディの最後に、それまでに表示されていたエラーメッセージを付け加えるのは以下のようなコードで実現できます。

// アウトプットバッファリングの開始
ob_start();

// 自動フラッシュをオフにする
ob_implicit_flush(false);

// 諸々のメインロジック
$responseBody = bulidResponse();

// 出力バッファから内容を取得し、バッファ内をクリアする
$ob = ob_get_clean();

// 本来のレスポンスボディを送出
print $responseBody;

// メインロジック中で出力されたエラーメッセージを送出
if ( !empty($ob) ) {
     print $ob;
}

アウトプットバッファリングを行うことで、エラーメッセージによる意図しないレスポンスヘッダーの送出も抑制でき、headerの2重送信に関する無駄なエラーも回避できます。

さらにエラーメッセージを整形して出力したい

放っておくとエラーメッセージはそのまま出力バッファに送出されてしまうので、プレーンテキストのままです。今回はエラーやスタックトレースを整形するというのも目的の中に入っているので、エラーのたびにそれらを補足します。

大半のエラーは、set_error_handlerで補足できる

5.3らしく無名関数で書いてしまいましょう。

set_error_handler(function($errno, $errstr, $errfile, $errline)
{
    echo "$errno $errstr in $errfile on line $errline";

    $stacks = debug_backtrace();

    foreach ( $stacks as $stack ) {
         // トレース内容を整形してecho/print
    }
}

これでエラーが発生するたびに、この処理が起動します。この処理を登録すると、本来のエラーメッセージは自動出力されないので、整形したエラーメッセージを自力で出力します。

これで大半のエラーはカバーできるのですが....。

しかし、Fatal Errorだけは掴めない

@alphabet_h set_error_handlerだと、fatal errorが捕まえられないみたいで。。。 shutdown関数もfatal errorのバッファが吐かれた後に走ってたのでウーンってなってたのです。Fri Dec 24 07:19:04 via Echofon

詳しくは、上のページにかなり丁寧に書かれています。本当に細かい動作を確認しておきたい場合は必見。

これらのハンドラ系も、spl_autoload_registerのように複数の実行したい関数をキューに突っ込めるようになっていればいいんですが。それはできないので、EventDispatcherのようなものでハンドラの中からイベントを送信したほうが良さそう。

register_shutdown_function() は複数回コールする ことが可能で、登録された順に関数がコールされます。 PHP: register_shutdown_function - Manual

と、思ったらマニュアルによると少なくともregister_shutdown_functionは、複数登録できるみたいです

Fatal Errorは、register_shutdown_functionで消息を掴める

ob_start();
ob_implicit_flush(false);

echo 'startup';

// shutdown処理の登録
register_shutdown_function(function()
{
     $ob = ob_get_clean();

     echo 'shutdown';

     if ( !empty($ob) ) {
          echo $ob;
     }

     echo 'end';
}

// 存在しないクラスを呼んでfatal errorを起こす
NotExsistsClass::hogehoge;

/*
##上のスクリプトの出力結果

startup
shutdown
Fatal error: Class 'NotExsistsClass' not found in ........
end
*/

shutdown関数が起動した時点で、出力バッファ内には、Fatal Errorのメッセージも出力されています。

Tips1: shutdownは中にob系関数があると、fatal errorのバッファ出力より先に走る

shutdown関数の中でob系関数つかったら、なぜかfatalのバッファ出力より先にshutdown関数が走るようになった。fatalのエラーメッセージはバッファ内に溜めた状態で。Fri Dec 24 07:20:47 via Echofon

前項の例のようにob系の関数を使わないと、shutdown関数より先にFatal Errorのメッセージが送出され、start, Fatal error...., shutdown, endの順に出力されます。

ただし、上のページで言及されているように、PHP4とPHP5では異なる挙動をみせるみたいです。※ この記事(今ご覧になっているページ)では、すべてPHP5.3.3で確認した動作について書いています。

Tips2: shutdown内ではdebug_backtraceは使えない

register_shutdown_functionで登録した関数の中で、debug_backtraceはなんかダメっぽい。(あまり詳しくは調査してないけど...)Fri Dec 24 07:23:26 via Echofon

You may get the idea to call debug_backtrace or debug_print_backtrace from inside a shutdown function, to trace where a fatal error occurred. Unfortunately, these functions will not work inside a shutdown function. PHP: register_shutdown_function - Manual

あまり詳しく調査はしてませんでしたが、PHP.netのregister_shutdown_functionページを参照すると、やはりdebug_backtraceは使用できないみたいです。fatalならbacktraceするまでもなく理由が明快だろうから困りはしないのだけど。