RecursiveRegexIteratorでファイル名を前方一致で絞り込む

はじめまして、ZYYX新卒入社1年目のエンジニアです。<br>
今回はPHPの`RecursiveRegexIterator`でファイル名を前方一致検索した時に、躓いた点を共有したいと思います。<br>

# 検証環境
- macOS Ventura 13.0.1
- PHP 8.1.6
- Xdebug 3.1.6
- Visual Studio Code 1.74.0

```Shell:環境情報
$ sw_vers
ProductName:            macOS
ProductVersion:         13.0.1
BuildVersion:           22A400

$ php -v
PHP 8.1.6 (cli) (built: May 16 2022 02:55:18) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.6, Copyright (c) Zend Technologies
    with Xdebug v3.1.6, Copyright (c) 2002-2022, by Derick Rethans

$ osascript -e 'version of app "Visual Studio Code"'
1.74.0
```

# そもそもRecursiveRegexIteratorって何？
phpマニュアルには以下のように書かれています。
> *この再帰イテレータは、別の再帰イテレータを正規表現でフィルタリングすることができます。*
> [PHP RecursiveRegexIterator - Manual](https://www.php.net/manual/ja/class.recursiveregexiterator.php)

つまり、以下のように`RecursiveDirectoryIterator`などで取得した要素から、正規表現にマッチする要素を取り出すことができます。
[参考リンク：PHP RecursiveDirectoryIterator - Manual](https://www.php.net/manual/ja/class.recursivedirectoryiterator.php)

```Text:ディレクトリ構造
\- __DIR__
        \- search
            |- fileA
            |- fileB
            |- fileC
            |- fileD
            \- fileE
```

```PHP:RecursiveDirectoryIteratorのみ使用
<?php
    const BASE_DIR = __DIR__ . "/search";
    $result = [];

    $directoryIterator = new RecursiveDirectoryIterator(BASE_DIR, FilesystemIterator::SKIP_DOTS);
    
    // 取得した要素名を出力
    foreach ($directoryIterator as $file) {
        $result[] = $file->getFileName();
    }
    var_export($result)

/* 実行結果
    array (
        0 => 'fileD',
        1 => 'fileC',
        2 => 'fileB',
        3 => 'fileE',
        4 => 'fileA',
    )
*/
?>
```

```PHP: RecursiveDirectoryIterator + RecursiveRegexIteratorを使用
<?php
    const BASE_DIR = __DIR__ . "/search";
    $result = [];

    $directoryIterator = new RecursiveDirectoryIterator(BASE_DIR, FilesystemIterator::SKIP_DOTS);

    // 正規表現で絞り込みを行う
    $regex = "/fileA/";
    $regexIterator = new RecursiveRegexIterator($directoryIterator, $regex);

    // 取得した要素名を出力
    foreach ($regexIterator as $file) {
        $result[] = $file->getFileName();
    }
    var_export($result)

/* 実行結果
    array (
        0 => 'fileA',
    )
*/
?>
```

`RecursiveRegexIterator`を使用することにより、`search`ディレクトリに存在する5つのファイルのうち、正規表現（`fileA`）にマッチした1つのファイルを取得することができました。

# 本題
## RecursiveDirectoryIterator + RecursiveRegexIteratorでファイル名の前方一致検索ができない
それでは、`RecursiveRegexIterator`を使用してファイル名を前方一致で絞り込みをしてみましょう。
今回はファイル名が`search_`で始まるファイルを取得します。
`search_fileA`ファイルのみが取得できるはずです。

```Text:ディレクトリ構造
\- __DIR__
        \- search
            |- search_fileA
            |- not_match_search_fileB
            |- not_match_search_fileC
            |- not_match_search_fileD
            \- not_match_search_fileE
```

```PHP: RecursiveRegexIteratorで前方一致検索
<?php
    const BASE_DIR = __DIR__ . "/search";
    $result = [];

    $directoryIterator = new RecursiveDirectoryIterator(BASE_DIR, FilesystemIterator::SKIP_DOTS);

    // 正規表現で絞り込みを行う
    $regex = "/^search_/";
    $regexIterator = new RecursiveRegexIterator($directoryIterator, $regex);

    // 取得した要素名を出力
    foreach ($regexIterator as $file) {
        $result[] = $file->getPathName();
    }
    var_export($result);


/*  実行結果
    array ()
*/
?>
```
取得できませんでした...orz

## 原因
上記のような絞り方を`RecursiveRegexIterator`で行うと期待した結果を得ることができません。

これは`RecursiveRegexIterator`でチェックする文字列がデフォルトでは**ファイル名ではなくファイルの絶対パス**であることが原因です。

## 解決策１（ファイルの絶対パスで検索を行う）
解決策の1つ目は、ファイルの絶対パスを検索する文字列として指定する方法です。<br>
以下のように正規表現に`ファイルの絶対パス + 抽出したい文字列`を指定すると期待した結果を得ることができました。

```Text:ディレクトリ構造
\- __DIR__
        \- search
            |- search_fileA
            |- not_match_search_fileB
            |- not_match_search_fileC
            |- not_match_search_fileD
            \- not_match_search_fileE
```
```php: 検索条件をファイルパスに変更
<?php
    const BASE_DIR = __DIR__ . "/search";
    $result = [];

    $directoryIterator = new RecursiveDirectoryIterator(BASE_DIR, FilesystemIterator::SKIP_DOTS);

    // 正規表現で絞り込みを行う
    $regex = "{^" . BASE_DIR . "/search_}";
    $regexIterator = new RecursiveRegexIterator($directoryIterator, $regex);

    // 取得した要素名を出力
    foreach ($regexIterator as $file) {
        $result[] = $file->getFileName();
    }
    var_export($result);

/*  実行結果
    array (
        0 => 'search_fileA',
    )
*/
?>
```
:::note info
使用できるデリミタ（区切り文字）<br>
PHPでデリミタには英数字、`バックスラッシュ・空白文字以外の任意の文字`が使用できます。
つまり、`{}, <>, ##, []`などがデリミタとして使用することができます。
[参考URL: PHP デリミタ - Manual](https://www.php.net/manual/ja/regexp.reference.delimiters.php)
:::

## 解決策２（検索をファイル名で行うようにする）
解決策2つ目は[FilesystemIterator::CURRENT_AS_SELF](https://www.php.net/manual/ja/class.filesystemiterator.php#filesystemiterator.constants.current-as-self)フラグを使用する方法です。<br>
デフォルトでは、`FilesystemIterator::current()`が [SplFileInfo](https://www.php.net/manual/ja/class.splfileinfo.php) オブジェクトを返すようになっていますが([FilesystemIterator::CURRENT_AS_FILEINFO](https://www.php.net/manual/ja/class.filesystemiterator.php#filesystemiterator.constants.current-as-fileinfo))、`RecursiveDirectoryIterator`を返すようにすることにより、`RecursiveRegexIterator`でファイル名で検索できるようになります。

```php:FileSystemIterator::CURRENT_AS_SELFフラグを使用する
<?php
    const BASE_DIR = __DIR__ . "/search";
    $result = [];

    $directoryIterator = new RecursiveDirectoryIterator(
        BASE_DIR,
        FilesystemIterator::CURRENT_AS_SELF
    );

    // 正規表現で絞り込みを行う
    $regex = "/^search_/";
    $regexIterator = new RecursiveRegexIterator($directoryIterator, $regex);

    // 取得した要素名を出力
    foreach ($regexIterator as $file) {
        $result[] = $file->getFileName();
    }
    var_export($result);

/* 実行結果
    array (
        0 => 'search_fileA',
    )
*/
?>
```

:::note warn
このやり方だとRecursiveRegexIteratorから得られるオブジェクトは`SplFileInfo`から`RecursiveDirectoryIterator`に変化します。なので解決策１と同じオブジェクトを得ようとすると$file->getFileInfo();の処理を入れる必要があります。
:::


# まとめ
`RecursiveDirectoryIterator` + `RecursiveRegexIterator`を用いた時に前方一致でファイル名検索を行うには、
1. `ファイルの絶対パス + 検索したい文字列`で検索を行う
2. RecursiveDirectoryIteratorで`FilesystemIterator::CURRENT_AS_SELF`フラグを使用する

の二つの方法があることがわかりました。

最後まで読んでいただきありがとうございました。