読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

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

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

PHP

PHPではエラー発生時も普通にレスポンスヘッダでステータス200を返しますが、それだと困るという場合もあるかと思います。
PHPの設定ディレクティブに auto_prepend_file という、全てのスクリプトが読み込まれる前に別のスクリプトを自動で差し込める機能がありますが、それを使って既存のコードに手を加えることなく、エラー発生時のステータスコードを変更する方法を紹介します。

auto_prepend_file ディレクティブは php.ini .htaccess httpd.conf .user.ini のどこでも設定できますが、今回の目的(HTTPからのアクセス時に発生したエラーのレスポンスステータスを変更)であれば所定のディレクトリ以下のみに限定した方がいいでしょう。
今回は普通に .htaccess で以下の定義を追加します。

説明のため単純に同一階層に設置していますが、実際にはここで設定するスクリプトはドキュメントルート外に設置した方が良いと思います。
auto_prepend_file で読み込まれるスクリプト __prepand.php の中身はこんな感じです。

今回の目的だと set_error_handler() だけでもいいかと思いますが、あえてエラーハンドラ内で ErrorException をスローして、エラー処理を set_exception_handler() で定義した例外ハンドラの方に統一してみました。
横着な方法に感じられるかもしれませんが、実は Symfony, FuelPHP, Lithium, Slim 等のフレームワークでも ErrorException を使って同様の処理が行われています。
(ちなみにSymfonyでは Symfony\Component\HttpKernel\Debug\ErrorHandler でこの辺のコードが見られました)

ユーザー定義のエラーハンドラではerror_reporting()の値に関係なく全てのエラー発生時に処理が呼ばれるので、error_reporting()で出力されないエラーはログに追記した後はFALSEを返して通常のPHPエラーハンドラへ処理を戻しています。

また今回の目的であるレスポンスステータスですが、HTTP用の例外クラスとして Acme\HttpException を定義しておき、例外ハンドラでは例外のコードに合わせたステータスコードをレスポンスヘッダに設定し、簡易のエラー画面的なものを出力しています。
スタックトレースException::getTraceAsString() を使ってますが、このメソッドは1行がかなり短い文字数で切られてしまいますし、変数の型なども出力されませんので、きちんと調査したいのであれば Exception::getTrace() で得られる配列を自分で加工した方がいいでしょう。
(拙作の Volcanus\Error\TraceFormatter など参考まで…PHPの標準トレース出力書式をベースに、変数の型などもある程度分かるよう加工しています)

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

エラーハンドラで error_reporting() を見ている部分の確認のため、E_USER_NOTICE は error_reporting() から除外しています。
エラーの発生方法を調べるのが面倒なので普通に trigger_error() 関数を使ってますが、渡ってくる定数の値(上のサンプルでの $errno)が違うだけで、エラーハンドラ内でやることは同じです。

その他いくつか注意点

  1. set_error_handler()は複数登録できない…最後に登録した関数だけが呼ばれます。
  2. E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNINGなどは捕捉できない…マニュアルに書いてある通り、いわゆる「Fatal Error」や「Parse Error」は扱えません。
  3. ビルトインサーバでは.htaccessが解釈されない…auto_prepend_fileの代わりにビルトインサーバの初期化スクリプトで読み込ませるといいです。

今時、アプリケーションのエラー制御はアプリケーションフレームワークが受け持つことが多いのかもしれませんが、PHPの標準機能であれば WordPress などコアのソースに手を入れづらいアプリケーションにも適用できますので、こういった方法も覚えておいて損はないと思います。

なお、エラーハンドラで捕捉されない E_ERROR レベルのエラーを扱うには、set_error_handler()と、スクリプト終了時に実行されるシャットダウン関数を登録する register_shutdown_function() 関数を使い、シャットダウン関数内で error_get_last() 関数を呼ぶ方法があります。
これを綺麗に処理しようとすると、出力バッファリングを行った上で、エラー出力をエラーハンドラ内で行わずにグローバルな領域にエラー情報を(スタックトレースも含めて)溜めておき、最後にerror_get_last()で取得したエラーと一緒に加工してまとめて出力という、ちょっと面倒な手順になりますが…。

シャットダウン関数を併用する際のポイントについては、以下のサイトが詳しいです。
PHPのset_erorr_handlerとregister_shutdown_functionとob関数について ( エラーを整形出力したい ) ::ハブろぐ
PHP の「エラー処理ハンドラ」「シャットダウンハンドラ」「例外処理ハンドラ」の挙動 - Web/DB プログラミング徹底解説

今回の set_error_handler() 関数の使い方については、 PHP Advent Calendar jp 2011 でも「エラーハンドラと例外ハンドラによるエラー処理 」と題して書いたのですが、設置先レンタルサーバの契約切れとともにコンテンツも消失してしまったので…その罪滅ぼしも兼ねて改めて書きました。

[追記]
公開しました。
エラーハンドラと例外ハンドラによるエラー処理 (PHP Advent Calendar jp 2011 Day 11) - k-holyのPHPメモ