エラーハンドリング・つづき

[追記] 以下の内容はすこし変わってしまいました。

このあいだのつづき。

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(), ...
    • エラーオブジェクトに使うクラスはEthna_Error、$modeはPEAR_ERROR_RETURN(なにもしない)、エラーコードはE_USER_ERRORとかにしてPEAR::raiseError()する
  • handleError()
    • new Ethna_Error()時に呼び出され、$_GLOBALS['_Ethna_error_callback_list']を順に実行する
  • set/clearErrorCallBack()
    • $_GLOBALS['_Ethna_error_callback_list']へのアクセサ

ちなみに、class Ethnaの定義はこれらの関数定義だけしかありません。Ethna.php自体は必要なファイルのincludeとか定数の定義とかがあります。

class Ethna_Error extends PEAR_Error (in Ethna_Error.php)

  • コンストラクタ
    • parent::PEAR_Error()した上でEthna::handleError()する

$modeはPEAR_ERROR_RETURNになっているので、PEAR_Errorクラスとしての機能はエラーオブジェクトとして振る舞うことのみです。エラーの表示とかはEthna::handleError()側でやります。

class Ethna_Controller (in Ethna_Controller.php)

  • コンストラクタ
    • Ethna::setErrorCallback()にcallbackとして自分($this)のhandleError()を与えます。
  • handleError()
    • Ethna_Loggerにログ出力を指示するだけです。
    • 紛らわしいけれど、Ethna::handleError()とは別モノです。

デフォルトのエラーハンドラは、Ethna_Controller::handleError()からEthna_Loggerを使う、ということになります。

function ethna_error_handler() (in Ethna_Logger.php)

エラーと切っても切り離せないログ、ということで、Ethna_Logger.phpにもエラー関連のコードが入っています(こうして見てみると、ちょっといけてない気もしてきた...)。

  • ethna_error_handler() (global関数)
    • Ethna::raiseError()とかを経由せずに発生したエラーをトラップします。
    • (おおざっぱには)php.iniの設定でエラーを画面に表示するようにしていて、でもEthna_Loggerには画面に表示するロガーがない場合は、気を利かせて(?)エラーをprintしてくれます*1
  • class Ethna_Logger
    • ログ出力関連です。Logwriterクラスとプラグインを経由して実際の出力をします。

エラー発生の流れ

ややこしいので、具体的にEthna::raiseError()するときのデフォルト動作を追ってみましょう。

  1. Ethna::raiseError()する
  2. Ethna_ErrorをエラークラスとしてPEAR::raiseError()が呼ばれる
  3. PEAR::raiseError()の中でnew Ethna_Error()される
  4. new Ethna_Error()の中でEthna::handleError()が呼ばれる
  5. $GLOBALS['_Ethna_error_callback']の唯一のcallbackであるApp_Controller::handleError()が呼ばれる
  6. 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のクラス群をちょっと突っ込んで使っています。

その際、PEAREthnaでエラーハンドリングがけっこうconflictしていて、PEARのクラス群を使っているEthna_PearWrapperではかなりひどいことになってます。

  • PEAR_Error v.s. Ethna_Error
    • raiseError()時にどっちが使われるかは明確なのでいいんですが、PEARのコード内でPEAR::raiseError()しているのを「Ethna::raiseError()に変えろ!」とは言えないので、pearcmd.phpに準拠してPEAR_Frontend_CLIのdisplayFatalError()を使うようにしています。Ethna流儀でのエラーはログが取れますが、PEARのコード内で発生したエラーはそのままexit(1)してしまうことになります。
  • pearcmd.phpのerror_handler() v.s. ethna_error_handler()
    • PEARのコード内には '@' がいろんなところで使われていて、これがかなりきつい。しかたないので、ethna_error_handler_skip_pear()なんていうしょぼいglobla関数をphp的エラーハンドラに設定し直して、PEARが出すwarningとかを画面に出さないようにしています。

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以上は表示する、といった使いかたができます。

*1:Ethna::handleError()を呼ぶべきかも

*2:さすがにE_STRICTはムリ