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

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

本稿では、PHPのWarningやNoticeを`set_error_handler`を局所的に用いたエラー補足の方法を紹介する。「エラー」という言葉には曖昧さがあるため、本稿ではPHPのWarningやNoticeなどは便宜上「ネイティブエラー」と呼ぶことにする。

## 伝統的なネイティブエラーの補足方法

ネイティブエラーの補足には`error_get_last`を使う方法がよく用いられる。この関数は、直前で発生したネイティブエラーの内容を取得することができる。合わせてエラー抑制演算子`@`をネイティブエラーが発生する関数につけることで、画面にネイティブエラーが表示されるのを回避する。この2つの組み合わせで、ネイティブエラーの補足を実現するのが一般的だ。

```php
<?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()`が制御構造でないため、ネイティブエラー発生とネイティブエラー補足の関連性がコードから読み取りにくいというのが考えられる。

また、注意して書かないと取りたいエラーとは関係ないエラーを取ってしまうバグを生みやすい。

```php
@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は局所的に使う方法](https://qiita.com/mpyw/items/470f5f660080835f55a0)に従えば、安全に`set_error_handler`を使うことができる。

### ネイティブエラーを例外に変換する関数

ネイティブエラーを例外に変換する関数の実装例を示す:

```php
/**
 * @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`を補足するようにすれば、ネイティブエラーを例外として受け取ることができる。

```php
$errorMessage = '';
try {
    captureNativeErrorAndThrowIt(
        function () {
            \trigger_error('something wrong');
        }
    );
} catch (ErrorException $e) {
    $errorMessage = $e->getMessage();
}
assert($errorMessage === 'something wrong');
```

戻り値を返す関数の場合は、`captureNativeErrorAndThrowIt`の戻り値を使うようにする。

```php
$result = captureNativeErrorAndThrowIt(
    function () {
        return 'success';
    }
);

assert($result === 'success');
```

## 関連情報

* [set_error_handler と ErrorException を組み合わせると幸せになるかも - Qiita](https://qiita.com/suin/items/14c455691a0391ee265c)
* [PHP: どのエラーハンドラが使われているか調べる方法 - Qiita](https://qiita.com/suin/items/e16c552669d5d091be8f)