PHP:正規表現が正しいかどうかチェックしたかったが、思った以上に大変だった

## ゴール

PHPの`preg_match`に渡す正規表現が正しいかどうかをチェックしたい。

`is_valid_regex`という関数に正規表現を渡したら、その正規表現の構文が正しいとか、`preg_match`で処理できる正規表現になっているかなどをチェックした上で、結果値を返す関数がほしい。

```php
is_valid_regex('/a/') == new Valid();
is_valid_regex('/(a/') == new Invalid('Compilation failed: nothing to repeat at offset 0');
```

## 正規表現が正しいかどうかをチェックする関数はない

PHPの組み込み関数で、上記の`is_valid_regex`ような振る舞いをする関数は存在しない(PHP7.2時点)。

したがって、`preg_match`で正規表現を実行してみる他ない。

## `preg_match`の仕様

### `preg_match`は正規表現が正しくないとき、Warningを出す

[`preg_match`](http://php.net/manual/ja/function.preg-match.php)は第一引数に与えられた正規表現に問題があるとき、Warningを出す仕様になっている。

```php
preg_match('/*invalid-regular-expression/', '');
// Warning: preg_match(): Compilation failed: nothing to repeat at offset 0
```

### Warningを出すときは、戻り値が`false`になる

Warningが出るとき、`preg_match`の戻り値は`false`になる。なお、下記コードはエラー抑制演算子`@`をつけることで、Warningが出力されることを防いでいる。

```php
$result = @preg_match('/*invalid-regular-expression/', '');
assert($result === false);
```

## `preg_match`の仕様を踏まえた上で

### `error_get_last`関数でエラーを取ることができるか？

できる。

```php
@preg_match('/*invalid-regular-expression/', '');
assert(
    [
        'type' => E_WARNING,
        'message' => 'preg_match(): Compilation failed: nothing to repeat at offset 0',
        'file' => __FILE__,
        'line' => __LINE__ - 6,
    ] === error_get_last()
);
```

### `preg_last_error`関数

正規表現のエラーを取得する関数に`preg_last_error`もあるが、`error_get_last`に比べるとずっと情報量が少ない。

```php
@preg_match('/*invalid-regular-expression/', '');
assert(PREG_INTERNAL_ERROR === preg_last_error());
```

### 一見`error_get_last`でエラーを拾うのが良さそうだが

一見`error_get_last`でエラーを拾うのが良さそうだが、エラーハンドラーがセットされていると、Warningの情報はエラーハンドラーに流れてしまい、`error_get_last`でWarningの中身を取ることはできない。エラー抑制演算子`@`をつけていても変わらない。

```php
$hasErrorHandlerInvoked = false;
// if error handler exists,
set_error_handler(
    function () use (&$hasErrorHandlerInvoked) {
        restore_error_handler();
        $hasErrorHandlerInvoked = true;
    }
);
// error_get_last can't get the warning.
@preg_match('/*invalid-regular-expression/', '');
assert($hasErrorHandlerInvoked === true);
assert(error_get_last() === null);
```

`error_reporting`を無効化しても`error_handler`がセットされているとうまくいかない。

```php
$hasErrorHandlerInvoked = false;
set_error_handler(
    function () use (&$hasErrorHandlerInvoked) {
        $hasErrorHandlerInvoked = true;
    }
);
$errorReporting = ini_get('error_reporting');
ini_set('error_reporting', 'Off');
$result = @preg_match('/*invalid-regular-expression/', '');
assert($result === false);
assert($hasErrorHandlerInvoked === true);
ini_set('error_reporting', $errorReporting);
```

### Warningを取るには一時的なエラーハンドラーをセットする

フレームワークなどがエラーハンドラーをセットしてくる環境を想定すると、Warningを取るために一時的なエラーハンドラーをセットする方法が考えられる。[set_error_handlerは局所的にも使える - Qiita](https://qiita.com/mpyw/items/470f5f660080835f55a0)を参考にした。

```php
$isValidRegex = function (string $regex): bool {
    set_error_handler(
        function ($severity, $message) {
            throw new \RuntimeException($message);
        }
    );
    try {
        preg_match($regex, '');
    } catch(\RuntimeException $e) {
         return false;
    } finally {
        restore_error_handler();
    }

    return true;
};

assert($isValidRegex('/a/'));
assert($isValidRegex('/*a/'));
```

## 結論

* Warningを取るには一時的なエラーハンドラーをセットする必要がある
* 思った以上に面倒な実装になる