[追記] 以下の内容はすこし変わってしまいました。
このあいだのつづき。
Ethnaのエラーの取り扱い
このあいだにちょっとだけ書いたとおり、Ethnaは基本的にはPEARのエラーの使いかたをそのまま継承しています。が、PEARそのものよりももうすこし複雑かもしれません。
エラー自体はEthna_Errorクラスを使って表現し、Ethna_Loggerを使って表示したりファイルに書いたりします。未定義変数へのアクセスによるE_NOTICEのような、Ethnaの管理外にあるエラーもEthna_Loggerでログが取れるように、(phpのしくみである)set_error_handling()にEthna用のエラーハンドラを与えています。
class Ethna extends PEAR (in Ethna.php)
- raiseError(), raiseWarning(), raiseNotice(), ...
- handleError()
- set/clearErrorCallBack()
- $_GLOBALS['_Ethna_error_callback_list']へのアクセサ
ちなみに、class Ethnaの定義はこれらの関数定義だけしかありません。Ethna.php自体は必要なファイルのincludeとか定数の定義とかがあります。
class Ethna_Error extends PEAR_Error (in Ethna_Error.php)
$modeはPEAR_ERROR_RETURNになっているので、PEAR_Errorクラスとしての機能はエラーオブジェクトとして振る舞うことのみです。エラーの表示とかはEthna::handleError()側でやります。
class Ethna_Controller (in Ethna_Controller.php)
- コンストラクタ
- Ethna::setErrorCallback()にcallbackとして自分($this)のhandleError()を与えます。
デフォルトのエラーハンドラは、Ethna_Controller::handleError()からEthna_Loggerを使う、ということになります。
function ethna_error_handler() (in Ethna_Logger.php)
エラーと切っても切り離せないログ、ということで、Ethna_Logger.phpにもエラー関連のコードが入っています(こうして見てみると、ちょっといけてない気もしてきた...)。
- ethna_error_handler() (global関数)
エラー発生の流れ
ややこしいので、具体的にEthna::raiseError()するときのデフォルト動作を追ってみましょう。
- Ethna::raiseError()する
- Ethna_ErrorをエラークラスとしてPEAR::raiseError()が呼ばれる
- PEAR::raiseError()の中でnew Ethna_Error()される
- new Ethna_Error()の中でEthna::handleError()が呼ばれる
- $GLOBALS['_Ethna_error_callback']の唯一のcallbackであるApp_Controller::handleError()が呼ばれる
- Ethna_Loggerにログ出力が指示される
うーん、複雑だ...。
Ethnaでのエラー(とログ)の使いかた
基本的に、Ethna::raiseError()とかを経由せずにethna_error_handler()が呼ばれるようなエラーは例外的です。error_reporting(E_ALL)でも*2raiseError()しない限りエラーが出ないのが理想です。
そもそもphpの仕組みとして、set_error_handler()したcallbackが実行される場合は、error_reporting()の値によらず必ず実行されます。また、Ethna_Loggerによるログはerror_reporting()の値は見ません。ログにechoをnoticeで指定した場合、error_reporting(0)でもnoticeが表示されます。逆にログにechoを指定しなくてもprintされることがあります。
ということで、エラー表示レベルの指定はerror_reporting()ではなくアプリの設定ファイルetc/appid-ini.phpの範囲で指定するのがEthnaの流儀、ということになります。trigger_error()や@演算子を使うこともあまり想定されてません。(使いたいなら、コントローラのhandleError()をオーバーライドするとかしたほうがいいです。)
アプリごとのエラーとログの扱いを拡張する手段としては、以下のようなパターンがあります。
- Ethna::set/clearErrorCallback()でエラー発生時の動作を変更する
- Ethnaの動き自体を変えちゃう方針
- Ethna_Errorを継承したApp_Errorを使う
- エラーオブジェクトをカスタマイズする方針
- App_Controller::handleError()をオーバーライドする
- エラーオブジェクトをコントローラがどう扱うかをカスタマイズする
- Ethna_Loggerを継承したApp_Loggerを使う
- エラーについてはそのままで、ログオブジェクトをカスタマイズする
- Ethna_Plugin_Logwriter_Hogehogeのようなプラグインを追加する
- ログオブジェクトもそのままで、ログの種類を追加する
ethnaコマンドでのエラーハンドリング
Ethna-2.3.0では、pear channelからEthnaのプラグインとかをpearのパッケージ管理のしくみを使ってインストールできるようになる予定です。Ethnaの流儀をethnaコマンドに押し込むべく、pear/PEARのクラス群をちょっと突っ込んで使っています。
その際、PEARとEthnaでエラーハンドリングがけっこうconflictしていて、PEARのクラス群を使っているEthna_PearWrapperではかなりひどいことになってます。
ethnaコマンドでは、Ethna_ControllerとApp_Controllerが共存していたりとけっこう複雑な状態を綱渡りしてます。こうしてpearcmd.phpもごちゃごちゃになっていったんだろうなぁ...。
まとめ
私自身しばらく勘違いしてたんですが、error_reporting()によるエラーの制御は思い通りいかないことが多いです。とりあえずE_ALLにして、noticeとかが出ないようにきれいにコードを書いた上で、etc/appid-ini.phpをいろいろいじるのがよいと思います。2.3.0のpreviewの時点で
'log_facility' => 'file,echo', 'log_level_file' => 'debug', 'log_level_echo' => 'warning',
と書いておけば、ログファイルにdebugレベルの詳細なログを書きつつ、notice以下は画面に表示せずwarning以上は表示する、といった使いかたができます。