try catch のエラーと例外

ちょっとファイル参照の用があって、一度試して無かったら別のところを見に行くとかしたかった。キャッシュ見に行って無かったら本体とか、ローカルを見に行って無かったらネットを参照とか、そういう感じ。
ファイル参照は file_get_contents() PHP: file_get_contents - Manual を使えば良いでしょう。一度試すというの、PHP に try catch という枠組み PHP: 例外(exceptions) - Manual があるというのでちょっと検討する。
結論から言うと、try catch を使うのではなく、 file_get_contents() の結果を === false でチェックすれば良いのでした。 try catch のエラーと例外 - hs9587’s diary

try catch と例外(exceptions)

PHP: 例外(exceptions) - Manual より、PHP にも try catch という枠組みがあるということでこういうコードを書いてみる。

<?php$photo_local  $photo_s3 適宜 」

try {
  $photo = file_get_contents($photo_local);
} catch (Exception $e)  {
  $photo = file_get_contents($photo_s3);
}

  header('Content-Type: image/jpeg');
  echo $photo;
?>

うまくいかない。
エラーは発生するが、catch はされない、catch方面の二度目のファイル参照はなく、続行し 200 OK になるが何も出てこない。

PHP Warning: file_get_contents(「何か」): failed to open stream: No such file or directory in 「どこか」

PHP マニュアル前掲、例外(exceptions) のところに注意書きがあるが、まさにその事態だ。

注意:
PHP の内部関数の多くは エラー報告 を使っており、例外を使っているのは新しい オブジェクト指向 の拡張モジュールのみです。 しかし、ErrorException を使えば簡単にエラーを例外に変換することができます。 この変換テクニックが使えるのは、致命的でないエラーに限ります。

そこにサンプルも書いてある

<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
    throw new ErrorException($message, 0, $severity, $filename, $lineno);
}

set_error_handler('exceptions_error_handler');
?>

さっきのやつ、冒頭にこのコードを書き足すと catch 出来るようになる。

PHP Fatal error: Uncaught ErrorException: file_get_contents(asdf): failed to open stream: No such file or directory in /var/www/html/g「ええと」:15
Stack trace:
#0 [internal function]: exceptions_error_handler(2, 'file_get_conten...', '/var/www/html/g...', 15, Array)
#1 /var/www/html/g「ええと」(15): file_get_contents('「別の」')
#2 {main}
thrown in /var/www/html/g「ええと」 on line 15

15行目というのは二つ目の方、そっちまで来るようになった。
catch節でまた例外になり、特に補足してないので Fatal になる。

このコードでは全てのエラーを例外に throw してる、ちょっと従来のエラーの枠組みから離れすぎてるように感じるのでもう少しコードを整理する。

set_error_handler() と restore_error_handler()

PHPマニュアルより

filename が見つからない場合、maxlength がゼロより小さい場合、あるいはストリーム内での指定した offset へのシークが失敗した場合に E_WARNING レベルのエラーが発生します。

というわけで

  • 例外への変換を限局する
    • エラーレベル、折角 E_WARNING とわかってるのでそうする
    • set_error_handler() による変換の登録はファイル参照の直前にする
      • もう try の中でいいや
    • catch したらすぐに restore_error_handler() で例外への変換をやめる
    • catch できなくても finallyに restore_error_handler() で例外への変換をやめる
  • 折角 ErrorException を投げてるので catch もそれに限定する

の様な方針でコードを整理する。

<?php
  # https://www.php.net/manual/ja/language.exceptions.php 注意 エラーを例外に
  function exceptions_error_handler($severity, $message, $filename, $lineno) {
    throw new ErrorException($message, 0, $severity, $filename, $lineno);
  }
?>
<?php$photo_local  $photo_s3 適宜 」

  try { # catch (ErrorException $e)
      set_error_handler('exceptions_error_handler', E_WARNING);
      # 次行の失敗に備えてエラーを例外に変換して throwするようセットする
      $photo = file_get_contents($photo_local);
  } catch (ErrorException $e) {
      restore_error_handler(); # catch したのでエラーの例外を throwするの止める
      error_log($e);
      #error_log(var_export($e, true));
      #error_log(var_export(get_class($e), true));
      error_log("Try: file_get_contents($photo_s3)");
      $photo = file_get_contents($photo_s3);
  } finally {
      restore_error_handler(); # いずれにしてもエラーの例外を throwするの止める
  } # catch (ErrorException $e)

  header('Content-Type: image/jpeg');
  echo $photo;
?>

これで望んだように動く。
それでエラー見ながら様子をみてると、そうはいってもファイル参照失敗ではプログラム続行してて、200 OK 返す、まあ echo の中味なくてブラウザでは何も出て来ないが、或いは画像が壊れてるとか。

file_get_contents()

さて、前掲この関数の説明によると

読み込んだデータを返します。失敗した場合に false を返します。
警告
この関数は論理値 false を返す可能性がありますが、false として評価される値を返す可能性もあります。 詳細については 論理値の セクションを参照してください。この関数の返り値を調べるには ===演算子 を 使用してください。

ということでした。
失敗しても値を返してくれるので、それをチェックすれば try catch の俎上に載せる必要なかったか。

=== false

それならこれでいいや

<?php$photo_local  $photo_s3 適宜 」

  function error_log_mine($message, $line) {
    error_log("PHP Mine. $message in " .  __FILE__ . " on line $line");
  }

  error_log_mine("Try: file_get_contents($photo_local)", __LINE__+1);
  $photo = file_get_contents($photo_local);
  if (! $photo === false) {
    error_log_mine("Success: file_get_contents($photo_local)", __LINE__-2);
  } # if (! $photo === false)

  if ($photo === false) {
    error_log_mine("Try: file_get_contents($photo_s3)", __LINE__+1);
    $photo = file_get_contents($photo_s3);
    if (! $photo === false) {
      error_log_mine("Success: file_get_contents($photo_s3)", __LINE__-2);
    } # if (! $photo === false)
  } # if ($photo === false)

  header('Content-Type: image/jpeg');
  echo $photo;
?>

ここでは注意書き通り === で false と比較した。
読み込み先が空ファイルだったりすると、正常に読み込んだ結果が空文字列になり PHPの文脈では偽という事になる。それを再読み込み側に回したいときはその様にチェックしましょう。

error_log のメッセージはちょっと書き過ぎてるかも、想定環境で多いほうの分岐とか、事前(Try)、事後(Success)のどちらかとか、適宜コメントアウトしましょう。

ちなみに