1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】Str::isでは改行を扱えませんよという話

Posted at

環境

PHP 8.0
Laravel 8.x
Docker(Laravel Sailで構築した環境を使っています)

目的

Laravelのヘルパ関数Str::isを使っていてハマった時の備忘録にすること

Str::isって何よ?

ある文字列Aの中に、別の文字列Bが含まれているかどうか判定してくれるヘルパ関数です。いくつか例を挙げておきます。

use Illuminate\Support\Str;

$matches = Str::is('foo*', 'foobar');

// true

$matches = Str::is('baz*', 'foobar');

// false

引用元(一部改変):Laravel 8.x ヘルパ Str::is

ある日のこと...

しゃっちょさん「キーワードの判定が改行に反応してくれないんだけど...」

私が参画していたプロジェクトでは問題のStr::is関数を使って、キーワードが特定の文字列に含まれているかどうかを判定していました。そこで以下のような事象が上がってきたのです。

use Illuminate\Support\Str;

$matches = Str::is('foo*', 'foo\nbar');

// false
// !?

キーワード判定される文字列に改行があると正しくキーワード判定されないのです。これは問題です。先ほどの引用元のドキュメントにも書いてありますが、アスタリスクはワイルドカードとして扱われるのでtrueが返って良さそうなものです。

さぁ調査だ

なぜtrueが返らないのでしょうか?ドキュメントの記述はえらいあっさりしていて参考にならなかったのでLaravel本体のコードを確認してみることに。Str::is関数のコードを抜粋します。

    public static function is($pattern, $value)
    {
        $patterns = Arr::wrap($pattern);

        if (empty($patterns)) {
            return false;
        }

        foreach ($patterns as $pattern) {
            if ($pattern == $value) {
                return true;
            }

            $pattern = preg_quote($pattern, '#');
            $pattern = str_replace('\*', '.*', $pattern);

// 【重要】このif文の条件式でキーワード判定をしている
            if (preg_match('#^'.$pattern.'\z#u', $value) === 1) {
                return true;
            }
        }

        return false;
    }

コードを見ると内部的にはStr::isはPHP組み込みのpreg_match関数を使ってキーワード判定をしているようです。どうやらこの辺に落とし穴がありそう。

解決策

解決策をbefore->afterで掲載します。

before(再掲)

use Illuminate\Support\Str;

$matches = Str::is('foo*', 'foo\nbar');

// false
// !?

after(文字列結合が冗長ですが分かりやすくする為にあえてこう書いています)

use Illuminate\Support\Str;

$matches = preg_match('/[\s\S]*'.'foo'.'[\s\S]*/', 'foo\nbar');

// true

参考:エスケープシーケンス「\s\S」を利用する

なぜこれで解決するのか(原因も含めて)

問題のpreg_match関数が改行に反応しなかったのは先ほどのStr::is関数のコードの中のこの部分が原因でした。抜粋して再掲します。

$pattern = str_replace('\*', '.*', $pattern);

元の$patternに入っている\*.*へ置換しています。PHPの正規表現では.は任意の1文字を表し、*は0回以上の繰り返しを表します。少なくとも正規表現をかじった程度の私のような人はそういう認識でしょう。よって最終的な$pattern変数では元々のLaravelでのコード内でのワイルドカード(*)が適切な正規表現(この場合は.*)に変換されているように見えます。

しかし、PHPの正規表現では.改行を除く任意の1文字を表すのです。
すなわち.*したとしてもその先に1つでも改行があればマッチしないと判定されるということです。

解決策のコードではワイルドカードの部分で

  • 空白、タブ、フォーム フィードなどの任意の空白文字を表す\s
  • 空白以外の任意の文字を表す\S

これらを組み合わせることで改行を含めた全ての文字に対応する文字通りの「ワイルドカード」を実現しています。あとはそれを*で繰り返すだけです。

最後に

間違いのご指摘や少しでも気になったことはコメントを頂けますととても嬉しいです!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?