PHPを7.2にバージョンアップしたら正規表現でマッチしない現象に出くわした

PHP5.6からPHP7.2にバージョンを上げた際に出くわした正規表現の制限の話です。

# TL;DL
- 正規表現の検索対象の文字数には気をつけよう
- 正規表現でうまくいかない現象に出くわしたらエラーコードを調べよう

# 正規表現でうまくマッチしない現象に出くわす
長めのテキストをDBに保存しているLaravelのプロジェクトがあり、その長めのテキストはテンプレートとして機能していました。
文章の中に、`<% tmpl_start %>``<% tmpl_finish %>`があり、その2つの間に`<% hogehoge %>` みたいな文字列があったら `ほげ` に置換、`<% piyopiyo %>` みたいな文字列があったら `ぴよ` に置換、といった具合です。コードで表すとこんな感じ。

```php
$hoge     = 'ほげ';
$piyo     = 'ぴよ';
$longText = '<% tmpl_start %> これは<% hogehoge %><% piyopiyo %>。 <% tmpl_finish %>';
preg_match('/<% tmpl_start %>(.|[\r\n])*?<% tmpl_finish %>/', $longText, $matches);

if (count($matches)) {
    $parsedText = str_replace('<% tmpl_start %>', 'スタート:', $matches[0]);
    $parsedText = str_replace('<% hogehoge %>', $hoge, $parsedText);
    $parsedText = str_replace('<% piyopiyo %>', $piyo, $parsedText);
    $parsedText = str_replace('<% tmpl_finish %>', ':終了', $parsedText);
    $longText   = preg_replace('/<% tmpl_start %>(.|[\r\n])*?<% tmpl_finish %>/', $parsedText, $longText);
}

echo $longText; // `スタート: これはほげぴよ。 :終了` と出力される
```

そのテンプレートはエンジニア以外でもいじれるようにしており、文章の長さの制限はしていませんでした。
なので、`<% tmpl_start %>`と`<% tmpl_finish %>` の間に数千文字入る可能性がある状態です。

その状態でPHP5.6からPHP7.2にバージョンアップしたところ、PHP7では最初のpreg_matchでマッチせず、置換がなされずテンプレートのまま出力されてしまうという事象が発生していました。

```php
// PHP5.6
`スタート: これはほげぴよ。 :終了` と出力される

// PHP7.2
`<% tmpl_start %> これは<% hogehoge %><% piyopiyo %>。 <% tmpl_finish %>` と出力される
```

ステージング環境では問題なく、本番環境で突如発生した問題でした。

# 何がいけなかったのか
よくよく調べてみると、ステージング環境と本番環境で、テキストの中身が若干違うことに気づきました。そこで本番のテキストをステージングで入れてみたところ、再現ができ問題を発見することができました。

しかし、何が違うのか最初はまったくわかりませんでした。diffをとると`**************`といった文字列が加えられていたので、ワイルドカードを文字列でいくつ以上入れたらおかしくなるんだろうかとか、そんなことを考えていました。実際はそんなことはなく、単純に文字数に問題があるんだということに気づきました。

# ちゃんとした原因・調査
今回はたまたま事象を特定できましたが、原因がわからないことには正しい対処をとることができません。

ということで調べたのですが、phpでは正規表現処理でエラーがあった場合にエラーコードを返してくれる `preg_last_error` という関数があります。

http://php.net/manual/ja/function.preg-last-error.php

ここを参考にデバッグしたところ、`PREG_JIT_STACKLIMIT_ERROR` の `6` が返されてきました。

そして [定義済定数](http://php.net/manual/ja/pcre.constants.php) のページを見るとこんな文章が。

```
The new PREG_JIT_STACKLIMIT_ERROR constant introduced with PHP 7.0.0 has got a value of 6.

I experienced this error code when parsing a 112KB file. preg_match_all failed with this error. Interesting was: The matches array contained some entries, but not all as the command failed (I missed to check the return value).

Unfortunately you can not configure the stack-size of the PCRE JIT. The only way out was - at least for me - to disable the PCRE JIT via php.ini (pcre.jit=0).
```

なるほど、対象のサイズが大きすぎたんですね。そして`pcre.jit=0`をphp.iniに設定すれば解消されるよ、と。
`pcre.jit`は PCREのjust-in-timeコンパイルを使用するかどうかのフラグの設定で、PHP7で追加されました。デフォルトは`1`になっています。

[実行時設定](http://jp2.php.net/manual/ja/pcre.configuration.php#ini.pcre.backtrack-limit)

`pcre.jit=0`にすれば確かに解消するかもしれませんが、そもそもたくさんの文字列にマッチしたりするのはパフォーマンス的に非常によろしくない状態です。なので、同じような現象に出くわしたら`pcre.jit=0`にするより処理の仕方をそもそも見直すのが良いでしょう。

# まとめ
正規表現は便利ですが、やはり一歩間違えるときついなーと感じます。今回の現象でデバッグ方法もわかったのはよかったですが、正規表現を使うときは気をつけないといけないですね。(というかPHP側でエラー出して処理止めてくれ)

## 参考
- [【php】正規表現ってそんなに負荷が高いの？（その３）](https://www.softel.co.jp/blogs/tech/archives/908)
- [PHP7の正規表現について](https://codepen.io/menrui/post/php_php7_preg)
- [正規表現のパフォーマンスの話をされても全くピンと来なかった僕は、backtrackに出会いました。](https://qiita.com/mochizukikotaro/items/d36e61e56220da5f95d1)
- [[セキュリティ]マッチするはずの正規表現がマッチしない現象](http://d.hatena.ne.jp/teracc/touch/20100410/1270885661)