しばらくサボってたEthnaさんをひさしぶりにさわってたら、pearcmd.phpだとメッセージを出してちゃんと終わってくれるものが、同じことをやっているのにPEAR/Commandを直で叩いたらfatalが出る。
なんだっけこれ? って調べてるうちにPEARのエラーハンドラにdisplayFatalErrorとdisplayErrorのどっちのcallbackを指定しているかの違いだった。
8月ごろに調べたことがほとんど忘れかけてて、ちょっと時間をとられてしまった。ということで、忘れないようにメモ。ちなみに以降の内容が大きくまちがってたら、おんなじまちがいがどっかに潜んでるはずです...。
phpのエラーの取り扱い
PEARとかのことは忘れて、素のphpだけを見ると、parse errorのような組み込みのエラー発生装置と、組み込みのエラーハンドラがついているようです。あんま深入りするとzend engineがどうこうとなりそうなので、よく使いそうなとこだけ。
- error_reporting()
- 組み込みのエラーハンドラに対して、E_NOTICEとかのエラーの種類によってエラーを表示するかどうかを設定する、もしくは設定値を取得する
- trigger_error()
- 組み込みのエラー発生装置にエラーを発生させる
- set_error_handler() / restore_error_handler()
- setのほうは組み込みのエラーハンドラを変更する。スタックっぽく変更履歴をのこしているそうで、restoreでset前に戻すことができる
- error_log()
- @演算子
- @がついた1文だけerror_reporting(0)だと思って実行する
「よくわかんないけどエラーは出てほしくない!」ってときは error_reporting() を小さい値に設定するのもアリですが、適切なエラーハンドラ関数を作ってset_error_handler()してあげるのがいいんでしょうね。error_reportingの値を見つつ、こっそりerror_log()してくれるのが、比較的「行儀のいい」俺エラーハンドラ、ってことになるのかな。
PEARのエラーの取り扱い
PEARのエラーはちょっと複雑な仕組みになってます。良く使うのは
といったとこでしょうか。
PEARでは、エラーをPEAR_Errorクラスのインスタンスとして扱います。言わばPEARライブラリが作り上げた舞台上でエラーとして振る舞うだけで、舞台裏(素のphp)から見れば、たんにあるクラスのインスタンスでしかないです。ただ、それだけだとphpさんにエラーが起きてることが分からないので、PEAR_Errorクラスのコンストラクタでtrigger_error()によってエラー通知をしたり、しなかったりします。
PEAR_Errorさんの一生をちょっと追いかけてみましょう。行数はぜんぶ PEAR.php です。
- PEAR::raiseError() が呼び出される
- new PEAR_Error() でエラー誕生 (line 563)
- ちなみにPEAR_Error以外のクラスをエラーオブジェクトにすることもできます
- PEAR_Errorのコンストラクタが実行される (line 851)
- $mode に従ってエラーが発生してどうするのかを決める。実質的なエラーハンドリングをしてる部分。
- $mode は次の論理和です。
PEAR_ERROR_PRINT | 画面に表示する (printfしてるよ!) |
PEAR_ERROR_TRIGGER | trigger_error()でphpのエラーハンドラに通知 |
PEAR_ERROR_DIE | die() しちゃいます |
PEAR_ERROR_CALLBACK | あらかじめ指定されたcallbackを実行する |
PEAR_ERROR_RETURN | なんもしない |
pearコマンドでのPEAR_Errorの使われかた
この $mode とかはどうやって設定すればいいんでしょ、というのが pearcmd.php でよく見掛ける
です。名前が php のほうの set_error_handler() に似ていますが、まったく関係ないです。あいかわらず舞台の上での世界です。set/restore_error_handler() に似たようなエラーハンドラの設定手段を提供してる感じだと思います。
$modeのデフォルト値などはみんな$GLOBALS['_PEAR_hogehoge']に突っこまれています。$modeとかを意識しないでraiseError()するときの値に使われます。
- $GLOBALS['_PEAR_default_error_mode']
- $modeのdefault値。
- そのさらにdefaultはPEAR_ERROR_RETURN。単純にはraiseError()しただけじゃ何も起きなくて、isError()の判定でメッセージを表示したりと、エラーハンドリングを全部自分でやってしまうことも多いでしょう。
- $GLOBALS['_PEAR_default_error_options']
- trigger_error()されるときに使うエラーの種類(E_USER_NOTICEとか)
- $GLOBALS['_PEAR_error_handler_stack']
- さっきのpush/pop用のスタック
そんなかんじで、想定内のエラーについては
PEAR::pushErrorHandling(PEAR_ERROR_RETURN); なんかエラーが出ちゃうかもな処理; PEAR::popErrorHandling();
と書いたりしてます。RETURNなので、エラーが起きても何も表示もしません。
想定外のエラーについては、pearcmd.phpでは
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, array($ui, 'displayFatalError'));
としています。$uiはPEAR_Frontend_CLIのインスタンスです。displayFatalErrorのほうは、PEAR/Frontend/CLI.phpのline 151にて、エラー出すだけ出してexit(1)しちゃってます。
ついでですが、他にも $GLOBALS['_PEAR_shutdown_funcs'] なんてのがあります。これはcallbackの配列で、これを順に実行する関数(_PEAR_call_destructors())がPEARクラスのコンストラクタでregister_shutdown_functionされます。
恐ろしいことに、PEARの一部のコードはshutdown functionsにregisterされたcallbackの中でwarningを出したりします!! 最後にちょろっと (/tmp/hogefuga が見付かりませんとか) warningが出て、調べてもどこで発生してるのか分からないときは、shutdown functionsも疑ってみましょう。
Ethnaのエラーの取り扱い
Ethnaは基本的に、PEARのエラーハンドリングを継承した形になっています。EthnaはPEARの、Ethna_ErrorはPEAR_Errorの、それぞれ派生クラスです。
と思ったけど、なんか疲れたのと長くなってきたのでまた今度。
エラーのきれいな使いかた
例外の使いかたもよくわかっていないですが、処理系にエラーを出させるってことは、アルゴリズム上の想定外というよりも、考えてる世界というか公理系の想定外って気がするんですね。emacsはelispで落ちてはいけない、と。
だから理想的には、アルゴリズム上の想定外はPEAR_Errorを使って、phpのほうはデフォルトハンドラにerror_reporting(E_ALL)できれいに動いてもらいたいところです。その意味では、PEARの一部のコードってかなりいけてないですよね。あれは参考にしちゃいけないと思った。
ただ、ちゃんとしたエラーハンドリングをわざわざ高いレイヤで行うことのデメリットもあると思います。phpでfile_exists()してからfopen()するよりも、fopenしてみて問題があったら何か処理、というのをバイナリコードでやってくれたほうがいいこともあるでしょう。
まあ、時と場合によるって結論になりますけど、夢と理想だけ見ていたいじゃないですか:-)