2012年4月5日木曜日

PHPの組み込み関数で例外を発生させる方法

このエントリではPHPの組み込み関数でエラー時に例外を発生させる方法を紹介します。デフォルト状態では、PHPの組み込み関数の大半はエラー時に例外を発生させません。

前のエントリで、PHPのheader関数は戻り値を返さず、エラー時に例外も発生させないことを紹介しました。これは酷い仕様だと思うのですが、どうすればエラーハンドリングできるかを考えてみました。

header関数の場合、エラー(警告)そのものは出ているので、以下の二つの方法が候補として考えられます。
  • error_get_last関数で直近のエラーを取得してエラー処理する
  • set_error_handlerで定義したエラーハンドラ関数でエラー処理する
どちらもモダンな書き方とはほど遠い感じです。
前者は、BASICのon error resume nextを連想させますし、直近のエラーがどの箇所で起こったかは簡単には識別できないので、過去のエラーを捕捉してしまいそうです。行番号はとれますが、行番号で判断するのはよくないでしょう。これもBASICみたいですね。
一方、エラーハンドラでエラー処理するのもよくありません。エラーハンドラにはあらゆるエラーが飛んでくるので、ログを吐いてプログラムを終了させるくらいしか現実にはできないでしょう。

そういう問題意識で「パーフェクトPHP」を読んでおりましたら、組み込み関数から例外を発生させる方法がちゃんと書いてありました(同書P160)。
また、エラーハンドラを設定することにより、PHPの標準のエラーを例外に変換して投げることもできます。エラーを例外に変換するには、次のようにエラーハンドラを設定します。例外の種類には、定義済みのErrorExceptionを利用しています。
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
PHPのパワープログラマには常識なのかもしれませんが、私は「なるほどねぇ~」と思いました。ブログ記事などでも見かけないようです。ただし、PHPのマニュアルには書いてありました

これを使って、header関数の例外処理のサンプルを書いてみました。
<?php
// エラーを例外に変換
set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
// リダイレクト関数…ただし、エラーになる
function redirect() {
  header("Location: http://www.yahoo.co.jp\nSet-Cookie: aaa=bbb");
}
try {
  redirect();
} catch (ErrorException $e) {
  echo 'Redirect error: ' . htmlspecialchars($e->getMessage(), ENT_COMPAT, 'UTF-8');
  // 必要な後始末
}
結果は、「Redirect error: Header may not contain more than a single header, new line detected.」と表示されます。
ということで、エラーを例外に変換する方法を紹介しました。パーフェクトPHPは本当に良い本ですね。
ただし、これをやると、警告を含む全てのエラーで例外が発生するので、ちまたでよく見るような、エラー処理もろくにしていないヌルい書き方はできなくなり、プログラム全体できちんと例外の設計をする必要があります。本来そうあるべきですけどね。

追記

はてなブックマークコメントで「$errno == E_WARNING のときは例外投げないようにすれば」というコメントを頂戴しましたが、そうもいかないように思います。
そもそもの発端だったheader関数のエラーはE_WARNINGです。しかし、ヘッダを送信しようとしてできなかったという事象は「WARNING」という語感とは裏腹な、かなり深刻なものだと思います。また、E_WARNINGより重いエラーであるE_ERRORは「重大な実行時エラー。これは、メモリ確保に関する問題のように復帰で きないエラーを示します。スクリプトの実行は中断されます。」(マニュアル)とあるように、捕捉する前に終了してしまいます(実験で確認しました)。
ということですので、色々工夫の余地はあるとは思いますが、E_WARNINGレベルのエラーは捕捉するべきだと思います。

※ $e->getMessage()の箇所にHTMLエスケープが抜けていたので追記しました(2015/4/26)

[PR]
パーフェクトPHP+徳丸本セットを抽選で1名に差し上げちゃうキャンペーン」やってます
安全なWebアプリケーションの作り方」電子書籍版販売しています。電子版はこちら

0 件のコメント:

コメントを投稿

フォロワー