php のエラー発生時に処理をはさみたい

php のエラー処理の方法について調べると細かい部分の話は出てきますが、最終的にどう実装すればいいのか、何故そのように実装を行う必要があるのかというのを記載しているものがなかったので、実装例と詳細な理由、ハマリポイントをまとめたものをここに記載しました。

フレームワークを利用しているのであれば、いい感じに設定されていると思いますが、実装の背景を把握しておくとトラブルの解決に役に立ったり、使用するライブラリの選定基準に役に立つので読んでおいて損はないと思います。

記事が長くなりすぎて見通したが悪くなったので、いくつかの記事に分けました。この記事の中にリンクが貼られていますので、そちらも参照して頂けると幸いです。

## この記事が対象としている phpのバージョン
php7.* も含む php 5.4 以上を前提として書かれてます。多くの部分が 5.4 未満でも通用すると思います。php7 からは結構挙動が変化しているので、それについても記載しています。

## はじめに必ず理解していないとハマること

php のエラー処理は使用している環境の設定(php.ini)によって挙動が変わってくるので、Web上にあるコードを実行しても自分の環境だけ同じ結果にならなかったり、別の環境では異なる動きをして混乱することが多いです。
そこでまずは以下の記事を読んで環境によって動作が異なってくる部分を理解しておきましょう。

[php のエラーが設定によって挙動が変化することを理解する](https://qiita.com/notona/items/bcd45b8c4dd9f30864c1)


# で、どう実装したらいい？

```php


set_error_handler(function($error_no, $error_msg, $error_file, $error_line, $error_vars) {
    if (error_reporting() === 0) {
        return;
    }
    throw new ErrorException($error_msg, 0, $error_no, $error_file, $error_line);
});

set_exception_handler(function($throwable) {
    // 開発環境ならエラーログを標準出力出すようにしてしまったほうが使い勝手がよさそうです（なくてもいい）
    if ($development === true) {
        echo $throwable;
    }
    send_error_log($throwable);
});

register_shutdown_function(function() {
    $error = error_get_last();
    if ($error === null) {
        return;
    }
    // fatal error の場合はすでに何らかの出力がされているはずなので、何もしない
    send_error_log(new ErrorException($error['message'], 0, 0, $error['file'], $error['line']));
});

ini_set('display_errors', 'Off');

function send_error_log($throwable) {
    // 何かエラーをどこかに渡すコードをここに。
    // この例ではテンポラリファイルディレクトリを取得してそこに php_error_log.txt という名前のファイルに追記していくような処理にした。
    file_put_contents(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php_error_log.txt', $throwable->__toString(), FILE_APPEND | LOCK_EX);
}

// ここからやりたい処理を書く

```

こんな感じ。長い・・。
あくまで一つの例なので、プロジェクトの要件に合わせて修正してください。

## エラーが発生したときの処理を書きたいだけなのに、なんでゴチャゴチャしているの？
4つタイプのエラーを考えているためです。
・コード上で catch されなかった例外（Exception）
・E_WARNING など php で発生するエラー
・register_shutdown_function で捕捉できる Fatal Error
・php のコード上で処理できない Fatal Error

## コード上で catch されなかった例外（Exception）
これは以下のような感じで誰にも catch されなかった例外のための処理です。

```php
throw new Exception("error!!!!");
```

誰にも catch されなった例外があると [set_exception_handler](http://php.net/manual/ja/function.set-exception-handler.php) に登録されたコールバック関数が呼ばれます。
上記のコードでは set_exception_handler にクロージャを渡して開発環境だったら標準出力に何か出しつつ、send_error_log を読んでログを書き出しているだけです。

## E_WARNING など php で発生するエラー
php では多くのエラーが例外という形ではなく、そのエラーが発生したタイミングで標準出力または標準エラー出力にエラーメッセージを書き出すという動きになります。

これだとエラー発生時に特定の場所にログを書き出す、処理を中断してエラーページを表示するなどができなくて困ります。そこで上記のコードでは [set_error_handler](http://php.net/manual/ja/function.set-error-handler.php) というエラー発生時に呼ばれるハンドラを設定できる関数を利用してエラーした箇所で [ErrorException](http://php.net/manual/ja/class.errorexception.php) がthrowされるようにすることで適切なエラー処理ができるようにしています。

上記のコードだと if (error_reporting() === 0) という不思議な if 文がありますが、これにエラー制御演算子対応のためのコードになります。詳細は以下に記載しました。
[エラー制御演算子について理解する](https://qiita.com/notona/items/02404987261f09937a6a)

## register_shutdown_function で捕捉できる Fatal Error
シンタックスエラー以外の Fatal Error については [register_shutdown_function](http://php.net/manual/ja/function.register-shutdown-function.php)を利用すると捕捉することができます。

詳細は以下の記事に記載したのでこちらを参照してください。
[register_shutdown_function で捕捉できるエラーについて](https://qiita.com/notona/items/0fffd9456c88bcb2d495)


## php のコード上で処理できない Fatal Error
これは恐らくシンタックスエラーぐらいしかないはず。

シンタックスエラーはエラーハンドラが設定される前なので、display_errors が Off だとブラウザから開いていたときに何もレスポンスが返ってこなくて、どこにシンタックスエラーがあるのか分かりません。

開発環境だけ php.ini の display_errors を On にしておいて、エラーハンドラが設定された後に
上記のコードで ini_set('display_errors', 'Off'); というコードを一番上に入れることでシンタックスエラーがあったとしても開発環境ではエラーメッセージが出るようにするためにこのようしていました。

これは別になくても問題はないですが、あるとちょっと親切かもしれません。

# デフォルトで例外を投げないもの
デフォルトで例外を投げない設定になっていて、ハマりやすいものをここに記載しておきました。

### PDO
データベースにアクセスする際に [PDO](http://php.net/manual/ja/class.pdo.php) を利用しますが、デフォルトだと例外を投げず [PDO::errorInfo](http://php.net/manual/ja/pdo.errorinfo.php)などを使用して取得しないとエラー内容が分からないです。これは不便なので、以下のように setAttribute を使用して例外を投げるように変更する必要があります。

```php
$pdo = new PDO($dsn, $user, $pwd);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
```

### DateTime
[DateTime](http://php.net/manual/ja/class.datetime.php)は日付処理を行うときに使用します。

コンストラクタである程度チェックされてエラーを投げてくるようになっていますが、一部条件下で例外にならず、getLastErrors() を呼ばないと検知できないことがあります。
そういうケースの例とハンドリング方法を以下に記載しました。

```php

// $datetime = new DateTime('abcde');            // これは例外が throw される 
$datetime = new DateTime('2010-04-00 00:00:00'); // この日付はおかしいけど、例外は投げてこない

// おかしなフォーマットになっているなら、エラーログに流して処理は継続する
$errors = $datetime->getLastErrors();
if (($errors['warning_count'] + $errors['error_count']) != 0) {
    throw new RuntimeException(var_export($errors, true)); // このような形にして初めてエラーがあることがわかる
}
```

### json_decode
json_decode はエラーが発生すると null を返すだけなので、以下のようにして例外を発行するようにすると扱いやすくなると思います。

```php
$json_string = "}";
$json = json_decode($json_string, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException(json_last_error_msg());
}
```

[こちら](https://wiki.php.net/rfc/json_throw_on_error)によると php 7.3 からは JSON_THROW_ON_ERROR をオプションとして指定することで、JsonException をエラー時に発行させることができるようになるそうです。

```php
try {
  json_decode("{", true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
  // 何か処理
}
```


## php7 以降から注意しないといけないこと
例外の種類は問わず例外起きたらとにかく catch して処理がしたい、というときは Throwable を使う必要があるので注意する必要があります。例えば以下のようなコードですね。

```php
try {
    $data = new SonzaishinaiKurasu();
} catch (Throwable $e) { // ここに注意。catch するのは Exception ではなく、Throwable。
    // 何か処理
}

```

php7 では一部のエラーが [Error例外](http://php.net/manual/ja/class.error.php) として throw されるようになりました。そのため Error と ErrorException がどちらも実装しているインターフェースになっている [Throwable](http://php.net/manual/ja/class.throwable.php) でキャッチする必要が出てきたため、このようになりました。

## 最後に
* 実はエラーについての[公式のドキュメント](http://php.net/manual/ja/language.errors.php)があります。
* でも、情報が全然足りない・・。