エラーハンドリング

しばらくサボってた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()
    • 多くの場合エラーといっしょに考えることになるログですが、エラーとは関係なくとにかくphpのログを出したいときに使う。「ログを出す」のが具体的に何をするのかはphp.iniとかで決めるのかな。
  • @演算子
    • @がついた1文だけerror_reporting(0)だと思って実行する

「よくわかんないけどエラーは出てほしくない!」ってときは error_reporting() を小さい値に設定するのもアリですが、適切なエラーハンドラ関数を作ってset_error_handler()してあげるのがいいんでしょうね。error_reportingの値を見つつ、こっそりerror_log()してくれるのが、比較的「行儀のいい」俺エラーハンドラ、ってことになるのかな。

PEARのエラーの取り扱い

PEARのエラーはちょっと複雑な仕組みになってます。良く使うのは

  • PEAR::raiseError()
  • PEAR::isError()
  • $err_obj->getMessage()

といったとこでしょうか。

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 なんもしない
  • コンストラクトが終わったらどっかにreturnされたり
  • return先でPEAR::isError()でエラー判定を受ける
    • is_a($data, 'PEAR_Error') で判定してる (line 275)
    • ってことは、エラークラスをPEAR_Error以外にするときは、PEAR_Errorを継承しましょう、ってことですね。

pearコマンドでのPEAR_Errorの使われかた

この $mode とかはどうやって設定すればいいんでしょ、というのが pearcmd.php でよく見掛ける

  • PEAR::pushErrorHandling()
  • PEAR::popErrorHandling()
  • PEAR::setErrorHandling()

です。名前が 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のエラーハンドリングを継承した形になっています。EthnaPEARの、Ethna_ErrorはPEAR_Errorの、それぞれ派生クラスです。

と思ったけど、なんか疲れたのと長くなってきたのでまた今度。

エラーのきれいな使いかた

例外の使いかたもよくわかっていないですが、処理系にエラーを出させるってことは、アルゴリズム上の想定外というよりも、考えてる世界というか公理系の想定外って気がするんですね。emacselispで落ちてはいけない、と。

だから理想的には、アルゴリズム上の想定外はPEAR_Errorを使って、phpのほうはデフォルトハンドラにerror_reporting(E_ALL)できれいに動いてもらいたいところです。その意味では、PEARの一部のコードってかなりいけてないですよね。あれは参考にしちゃいけないと思った。

ただ、ちゃんとしたエラーハンドリングをわざわざ高いレイヤで行うことのデメリットもあると思います。phpでfile_exists()してからfopen()するよりも、fopenしてみて問題があったら何か処理、というのをバイナリコードでやってくれたほうがいいこともあるでしょう。

まあ、時と場合によるって結論になりますけど、夢と理想だけ見ていたいじゃないですか:-)