PHPは例外がありながら、rename
関数やmkdir
関数などは歴史的な経緯により、操作が失敗しても例外ではなくNotice
やWarning
エラーを発生させる。発生したエラーはデフォルトで画面に表示され、例外のようにエラーを補足することはやや面倒である。
本稿では、PHPのWarningやNoticeをset_error_handler
を局所的に用いたエラー補足の方法を紹介する。「エラー」という言葉には曖昧さがあるため、本稿ではPHPのWarningやNoticeなどは便宜上「ネイティブエラー」と呼ぶことにする。
伝統的なネイティブエラーの補足方法
ネイティブエラーの補足にはerror_get_last
を使う方法がよく用いられる。この関数は、直前で発生したネイティブエラーの内容を取得することができる。合わせてエラー抑制演算子@
をネイティブエラーが発生する関数につけることで、画面にネイティブエラーが表示されるのを回避する。この2つの組み合わせで、ネイティブエラーの補足を実現するのが一般的だ。
<?php
@trigger_error('something worng');
var_dump(error_get_last());
上のコードの出力結果は次のようになる。
array(4) {
["type"]=>
int(1024)
["message"]=>
string(15) "something worng"
["file"]=>
string(82) ".../Untitled 7.php"
["line"]=>
int(2)
}
伝統的な補足方法の問題点
伝統的な補足方法の問題点としては、error_get_last()
が制御構造でないため、ネイティブエラー発生とネイティブエラー補足の関連性がコードから読み取りにくいというのが考えられる。
また、注意して書かないと取りたいエラーとは関係ないエラーを取ってしまうバグを生みやすい。
@touch('/path/to/file'); // 失敗
// ... ここに何行かあり touch と mkdir のつながりがコード上見えないものする
@mkdir('/path/to/dir'); // 成功
$mkdirError = error_get_last(); // touchのエラーがmkdirのエラーとして使われてしまうバグ
局所的な set_error_handler
でネイティブエラーを例外に変換する
set_error_handler
関数はネイティブエラーをハンドリングする関数を登録する関数で、ネイティブエラーが発生した際に、画面に出力される代わりに、この関数に渡したエラーハンドラーがエラーを処理するようにできる。
フレームワークなどでネイティブエラーが画面に表示されず、ログにだけ出るようになっているのは、set_error_handler
を使っているからである。
set_error_handler
を使い、エラーハンドラでネイティブエラーを例外に変換してあげれば、モダンなtry-catch構文でエラーを補足できるようになり、error_get_last
よりまともなエラー補足ができるようになるわけだ。
注意点もある。エラーハンドラーは同時に一つしかアクティブにできないという制約があるため、set_error_handler
は本来アプリケーションの中で1回だけ呼ばれるべきである。あちこちでset_error_handler
してしまうと、フレームワークが提供しているエラーハンドラを潰してしまう恐れがある。
この点に注意しつつ、@mpywさんが紹介しているset_error_handlerは局所的に使う方法に従えば、安全にset_error_handler
を使うことができる。
ネイティブエラーを例外に変換する関数
ネイティブエラーを例外に変換する関数の実装例を示す:
/**
* @param callable $operation
* @return mixed
*/
function captureNativeErrorAndThrowIt(callable $operation)
{
$errorReportingLevel = \error_reporting(\E_ALL);
\set_error_handler(
function (
int $severity,
string $message,
string $file,
int $line
): void {
throw new ErrorException($message, 0, $severity, $file, $line);
}
);
try {
return $operation();
} finally {
\restore_error_handler();
\error_reporting($errorReportingLevel);
}
}
@mpywさんの投稿にも同じようなコードがあるが、少し改造してErrorException
投げるようにした。
error_reporting
に0
セットされている環境、つまり、エラーをはかない設定していると、set_error_handler
してもエラーが拾えないので、一時的にE_ALL
をはくようにする。
captureNativeErrorAndThrowIt
の基本的な使い方は、ネイティブエラーが発生する処理をクロージャーにし、captureNativeErrorAndThrowIt
の引数に渡す。captureNativeErrorAndThrowIt
は例外を投げるので、try-catchで囲み、ErrorException
を補足するようにすれば、ネイティブエラーを例外として受け取ることができる。
$errorMessage = '';
try {
captureNativeErrorAndThrowIt(
function () {
\trigger_error('something wrong');
}
);
} catch (ErrorException $e) {
$errorMessage = $e->getMessage();
}
assert($errorMessage === 'something wrong');
戻り値を返す関数の場合は、captureNativeErrorAndThrowIt
の戻り値を使うようにする。
$result = captureNativeErrorAndThrowIt(
function () {
return 'success';
}
);
assert($result === 'success');