register_shutdown_function で捕捉できるエラーについて

phpでは[set_exception_handler](http://php.net/manual/ja/function.set-exception-handler.php)や[set_error_handler](http://php.net/manual/ja/function.set-error-handler.php)を利用することでエラーを捕捉することができますが、この２つを利用しても補足できないものが存在します。

それらについては[register_shutdown_function](http://php.net/manual/ja/function.register-shutdown-function.php)を利用することで取り扱うことができるので、その方法をここに記載しました。

## register_shutdown_function を利用しないと捕捉できないエラーはどんなものがあるか？

### 例1.メモリが足りないとき
php はだいたいの環境で memory_limit が設定されていて一定上のメモリを消費すると処理が中断されます。

```
ore@mypc:~$ php -r 'ini_set("memory_limit", "12M");str_repeat("aaa", 50000000);'
PHP Fatal error:  Allowed memory size of 12582912 bytes exhausted (tried to allocate 150000032 bytes) in Command line code on line 1
PHP Stack trace:
PHP   1. {main}() Command line code:0
PHP   2. str_repeat() Command line code:1```
```

### 例2.実行時間が長すぎたとき
cli モードでは max_execution_time が設定されていることがあまりないと思いますが、Webサーバーとして運用しているときにこのリミットによってスクリプトが中断されてしまうことがあります。

```
ore@mypc:~$ php -r 'ini_set("max_execution_time", "1");for(;;){};'
PHP Fatal error:  Maximum execution time of 1 second exceeded in Command line code on line 1
PHP Stack trace:
PHP   1. {main}() Command line code:0
``` 

### 例3. php 5.6以下のみ。存在しないものを呼んでしまったとき。

```

// クラスが存在しない。
ore@mypc:~/$ php -r 'new NaNaShi();'
PHP Fatal error:  Class 'NaNaShi' not found in Command line code on line 1

// 関数が存在しない
ore@mypc:~/$ php -r 'aaa();'
PHP Fatal error:  Call to undefined function aaa() in Command line code on line 1

```

## register_shutdown_function を使ってみる
以下のような感じで記述します。

```php
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error === null) {
        return;
    }
    // 何かログを出したりする処理
    $error_string = $error['message'] . ' ' . $error['file'] . ' ' . $error['line'];
    file_put_contents(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php_error_log.txt', $error_string, FILE_APPEND | LOCK_EX);
});
```

## 仕組み 
register_shutdown_function はスクリプトが終了した後に呼ばれるコールバック関数を設定できる関数で、そのコールバック関数の中で [error_get_last](http://php.net/manual/ja/function.error-get-last.php)を呼び出すことで最後に発生したエラーを引っ張り出してきています。

スクリプトが終了したあとに呼ばれているだけなので、例えば以下のように finally やデストラクタなどを使用していたとしても、それらが呼ばれることはありません。

```php
class MyClass {
    public function __destruct() {
        // ここは呼ばれない
    }
}

try {
    $a = new MyClass();

    // ここで Fatal Error が発生する
    ini_set("memory_limit", "12M");str_repeat("aaa", 50000000);
} catch (Error $e) {
    // php7 以降 Error をcatch することで多くのエラーをハンドリングできるようになったが、
    // それでもこのケースは無理。この catch は呼ばれない
} finally {
    // ここも呼ばれない
}
```

## 制限
register_shutdown_function で呼ばれたコールバック関数はスクリプトが終了したあとに呼ばれているだけなので、スタックトレースはエラー発生した箇所ではなくなってしまいます。

## 捕捉できないもの
register_shutdown_function を使っても php スクリプトにシンタックスエラーがある場合は登録前(register_shutdown_function が呼ばれる前)にスクリプトが終了してしまうため捕捉できません。


## php7 から変わったこと
php7 以前のバージョンは register_shutdown_function を利用しないと捕捉できないエラーが多かったですが、php7 以降 [Error例外](http://php.net/manual/ja/class.error.php)として throw されるものが出てきたため、使用しなくても捕捉できるエラーが増えました。

ただ残念ながら増えただけで補足できないものはまだまだあるので必要であることには変わりはありません。


## SplFileObject の謎

SplFileObject はコンストラクタに指定されたファイルが存在しない時に RuntimeException が throw されます。これを catch しても何故か error_get_last() した結果に「ファイルが存在しないこと」がエラーとして含まれおり、register_shutdown_function でエラーが報告されてしまいます。


```php
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error === null) {
        return;
    }
    var_dump($error);
});

try {
    new SplFileObject('gggg.php'); // 存在しないファイル
} catch (RuntimeException $e) {
    echo "catched!!" . PHP_EOL;
}
```

実行結果

```
catched!!
array(4) {
  ["type"]=>
  int(2)
  ["message"]=>
  string(86) "SplFileObject::__construct(gggg.php): failed to open stream: No such file or directory"
  ["file"]=>
  string(31) "/home/ore/test.php"
  ["line"]=>
  int(18)
}
```

これは動作が謎なので私が関わっていたプロジェクトでは SplFileObject を使わないことで解決するという判断に至りました。

## 最後に
* 早く register_shutdown_function を使わなくてもエラーを補足できる仕組みなってほしい