19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPのWarningやNoticeを局所的に取得する方法

Last updated at Posted at 2019-01-16

PHPは例外がありながら、rename関数やmkdir関数などは歴史的な経緯により、操作が失敗しても例外ではなくNoticeWarningエラーを発生させる。発生したエラーはデフォルトで画面に表示され、例外のようにエラーを補足することはやや面倒である。

本稿では、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_reporting0セットされている環境、つまり、エラーをはかない設定していると、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');

関連情報

19
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?