k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

出力バッファリングとシャットダウン関数でエラー処理してみた

auto_prepend_fileで既存コードに手を加えずエラー発生時の処理を変更する方法の続きです。

参考リンク紹介するだけというのもなんなので、シャットダウン関数を併用したエラー処理について試してみました。
試行錯誤する中で、どうにも無名関数だけでやるのが厳しく感じたので、今回はクラスに分離しました。
以下、環境は Linux版 PHP5.3.14 です。

前回と同様、.htaccess で auto_prepend_file を設定します。

読み込ませる方のスクリプトは、クラスを使ったのでだいぶ記述が簡略化されています。

まずはじめに出力バッファリングを開始し、エラーハンドラ、例外ハンドラとも ErrorProcessor クラスにエラー情報を追加するだけにして、エラーメッセージの出力はシャットダウン関数まで持ち越しています。
今回はそういう事情もあって、エラーハンドラ内での ErrorException への変換をやめました。
その代わりに、スタックトレースの取得に debug_backtrace()関数を使っています。

シャットダウン関数ではバッファの内容を取得した後、error_get_last()関数を使って、標準のエラーハンドラで処理されたエラーがあれば追加しています。
最後にステータスコードを送出して、バッファの内容を出力した後、display_errors が有効であればエラーメッセージを出力します。
シャットダウン関数の部分は色々と注意点があるので、後でまとめます。

長くなりますが、 ErrorProcessor クラスです。

やってることは、エラーハンドラ/例外ハンドラでのエラー情報の保持とログ追記、シャットダウン関数で送出するレスポンスのステータスラインやエラー出力の生成です。
前者はともかく、後者は一般的にはレスポンスクラスのような別のクラスがやることだと思いますが、この辺ちゃんと考えるとフレームワークの設計になってしまいますし、今回のような特殊用途で手数を減らすためだけに作成したクラスなので、汎用性の低さはご容赦ください。

addError()メソッドが肝なんですが、エラーを出力するかどうかをクラスの生成時ではなく利用時の値で判断する必要があるため、メソッド内で error_reporting() を呼んでます。
※この辺の処理がまずいせいではまったケース→ エラーハンドラが原因でSmartyのエラーが発生していた件

その他のメソッドは色々ありますが、hasError(), statusLine(), log(), content() 以外は全てエラーメッセージの整形処理です。
log()はインスタンス生成時にコールバックを登録してもらって、それを呼んでるだけです。(callableチェックしてませんが…)

フォームからAcme\HttpExceptionやPHPエラーを発生させるサンプルです。

前回とほとんど同じですが、「PHP Fatal Error」ボタンを押すことで E_ERROR レベルのエラーを発生させるようにしています。
また、 display_errors を無効にして白い画面でエラーのステータスコードのみ返されることを確認しました。

error_prepend_string と error_append_string の2つの設定は、標準エラーハンドラの出力結果を分かりやすくするために入れています。
(癒し効果の高いエラーメッセージが表示されるので、開発時にはおすすめです!)


以下、シャットダウン関数でエラー処理する際の注意点をいくつか。

  • ユーザーエラーハンドラでFalseを返した場合

処理後に標準のエラーハンドラが実行されるため、error_reporting に含まれるレベルのエラーで display_errors が有効に設定されている場合、エラーメッセージが出力されます。(xdebugを有効にしている場合、xdebugのエラーメッセージ)
捕捉できないエラーが発生した場合も標準のエラーハンドラで処理されるため、同様の結果となります。
(サンプルでは E_ERROR レベルのエラーがこのケースに該当します)
また、シャットダウン関数で呼んでいる error_get_last() は、標準エラーハンドラが最後に処理したエラーの情報を返すので、ユーザーエラーハンドラがFalseを返した場合とTrueを返した場合で結果が変わります。

  • ユーザーエラーハンドラで処理を中断しない場合

例外スローやexit等しない限り、エラー発生後も処理が続行されます。
たとえばアプリケーションコード内で使われている標準関数でエラーが発生した際、戻り値を見て例外をスローする等の適切な処理がされていないコードだと、まずいことになるかもしれません。
また、シャットダウン関数が呼ばれる時点ですでに通常のレスポンスが出力された状態になります。

サンプルコードではこれらを考慮して error_reporting に含まれないレベルのエラーはログだけ記録して出力対象に含めず、エラーハンドラではTrueを返して処理続行、それ以外のエラーはエラーメッセージを生成して ErrorProcessor に保持した後、 exit で処理を中断しています。
サンプルコードの設定では E_USER_NOTICE の場合に前者に該当し、ログのみ記録してステータス200で通常のレスポンスが返されます。
(ErrorProcessor クラスは構造上、複数のエラーメッセージを保持できるようにしていますが、上記のために実際にはそのようなケースは発生しません)

厄介なのはユーザーエラーハンドラで補足できない E_ERROR レベルのエラーで、標準のエラーハンドラで処理されバッファリングされたエラーメッセージをどう扱うかが悩みどころです。
エラー発生までの間に出力された内容までバッサリ破棄してしまうのは論外でしょうし、とりあえず今回はそのまま出力しています。
そのため、標準エラーハンドラのエラーメッセージと、シャットダウン関数で ErrorProcessor が出力するエラーメッセージが二重で出力されます。

サンプルでも使っている error_prepend_string や error_append_string を駆使すれば文字列置換で除去できるかもしれませんが、気持ち悪いかな。
display_errors をOFFに設定しつつ、シャットダウン関数ではそれを無視してエラーメッセージ出力すれば、希望通りの結果を得ることはできますが、それも分かりづらいし。
そういった苦労をかけてまで、ユーザーエラーハンドラで補足できないレベルのエラーを独自に整形出力する必要があるかどうか。

まとまりませんが、こんな感じです。