PHPにもIteratorがある!?(2回目) opendirの実装

Iteratorインターフェイスを実装してみる

こんにちは、こたかです。

今日はIteratorインターフェイスで「opendir」の機能を実装してみました。
foreachループにIteratorインターフェイスを持つクラスのインスタンスを指定し、ループで列挙することができます。

Iterator版でのメリット

ディレクトリエントリーを小出しに列挙するため、大規模システムのファイル列挙でも列挙し終わる前に処理を開始できます。
ストリームと組み合わせると、ダウンロード開始までの時間を短縮できます。

ソースからコールバック処理が消えます。若干見やすくなります。

Iterator版でのデメリット

始めに全てのファイルを列挙するわけではないので、ファイル長に問題がある場合は、列挙時まで発見が遅れます。
列挙管理に若干オーバーヘッドがあります。しかしながらスタック構造など組み合わせて、再帰処理並みにパフォーマンスが維持できます。
使う側のソースは見やすくなりますが、機能を実装したクラス自体はIteratorの知識が無いとソースが読みにくくなります。

パフォーマンスの参考値

エクスプローラのプロパティ表示より若干高速に統計できました。そんなに遅くは無いと思います。

サンプルソース

※動作無保証。製品等に組み込む際にはご注意ください。

HsDirectoryEntry.php

<?php
namespace LibHask;

//
// HsDirectoryEntry.php
//   HA Soft kikaku 2023
//

// -----------------
// License : MIT。
// 免責事項:動作無保証です。利用者の自己責任とし、いかなる損害も補償いたしかねます。
// -----------------

// ■概要
// ディレクトリエントリー列挙用 Iterator
// ディレクトリ単位でディレクトリとファイルをすべて列挙し、
// sortで昇順にソートした状態のリストで列挙します。
// is_file / is_dir はforeachループ側で行うことを想定。
// 列挙から . / .. は除外しています。
//
//---------------------------------------
// メリット
//   初動が速い
//   パスを保存するメモリが小さくて済む
//
// デメリット
//    完了するまで、対象ファイル数は分からない
//---------------------------------------

// // ■ Usage
// $dirs = new HsDirectoryEntry('<フルパス>');
// // 列挙のみ
// foreach($dirs as $n) {
//     if (is_file($n)){
//         echo $n."\r\n";
//     }
// }
// // 添え字あり
// foreach($dirs as $i => $e) {
//     if (is_file($e)){
//         echo '['.$i.'] '.$e."\r\n";
//     }
// }



//-----------------------------------------------------
// /* メソッド */
// public current(): mixed
// public key(): mixed
// public next(): void
// public rewind(): void
// public valid(): bool

// Iterator::current — 現在の要素を返す
// Iterator::key — 現在の要素のキーを返す
// Iterator::next — 次の要素に進む
// Iterator::rewind — イテレータの最初の要素に巻き戻す
// Iterator::valid — 現在位置が有効かどうかを調べる
//-----------------------------------------------------

// 1つのディレクトリ内の列挙
class HsDirectoryEntriesArr implements \Iterator{
    public $entries=[];
    public $pos=0;
    public $count=0;
    public $base_dir='';

    private function get_entrys($dir,$opt){
        $entrys=[];
        if (is_dir($dir)) {
            if ($dh = opendir($dir)) {
                $this->base_dir=$dir.'/';
                while (($file = readdir($dh)) !== false) {
                    // 最初が '.'のファイルがほとんど無いので速いと期待
                    if ('.'==$file[0] && ('.'==$file || '..'==$file)){
                        continue;
                    }
                    $entrys[]=$file;
                }
                closedir($dh);
            }
        }
        if (null!=$opt){
            sort($entrys,$opt);
        }
        return $entrys;
    }

    public function __construct($dir='',$opt=SORT_NATURAL | SORT_FLAG_CASE) {
        $this->pos=0;
        $this->count=0;

        $dir=rtrim($dir,'/');// 末端'/'なしにする
        if (is_dir($dir)){
            $this->entries=$this->get_entrys($dir,$opt);
            $this->count=count($this->entries);
            return;
        }
        // echo 'ERROR: dir not found. : '.$dir."\r\n";// [debug]
    }

    // Iterator I/F
    public function key(){
        return $this->pos;
    }

    // Iterator I/F
    public function next():void{
        $this->pos++;
    }

    // Iterator I/F
    public function current(){
        return $this->base_dir.$this->entries[$this->pos];
    }

    // Iterator I/F
    // イテレータを初期化する。
    public function rewind() : void{
        $this->pos=0;
    }    

    // Iterator I/F
    public function valid():bool{
        return ($this->pos < $this->count);
    }
};

// ディレクトリエントリーを列挙して、foreachで列挙できるようにするクラス
class HsDirectoryEntry implements \Iterator {

    private $entries=null;
    private $itr_offset=0;// 通し番号
    private $parents=null;// HsDirectoryEntriesArr[] ... stack
    private $base_dir='';
    private $opt;

    public function __construct($dir,$opt=SORT_NATURAL | SORT_FLAG_CASE) {
        $this->entries = new HsDirectoryEntriesArr();
        $this->opt=$opt;
        $dir=rtrim($dir,'/').'/';// 末端を'/'にする
        if (is_dir($dir)){
            $this->base_dir=$dir;
        }
    }

    // Iterator I/F
    public function current(){
        return $this->entries->current();
    }//  mixed

    // Iterator I/F
    public function key(){
        return $this->itr_offset;
    }//: mixed

    // 子ディレクトリのループへ進める
    private function enter_dir($dir){
        $childs = new HsDirectoryEntriesArr($dir,$this->opt);
        if ($childs->valid()){
            if (null!=$this->entries){
                $this->parents->push($this->entries);
            }
            $this->entries = $childs;
            return true;
        }
        return false;
    }

    // 親ディレクトリのループへ戻る
    private function leave_dir(){
        if (0<$this->parents->count()){
            $this->entries=$this->parents->pop();
            return true;
        }
        // もうディレクトリ無い。
        return false;
    }

    // Iterator I/F
    // 繰り返し処理を次に進める。各繰り返しの最後に呼び出される。
    public function next() : void{
        $this->itr_offset++;
        // 現在の要素がディレクトリの場合は、子ディレクトリを走査する
        $e=$this->entries->current();
        if (is_dir($e)){
            // 子ディレクトリ走査
            if ($this->enter_dir($e)){
                // 子ディレクトリ内にエントリーがあった
                return;
            }
        }
        // 次のエントリーへ
        $this->entries->next();
        if ($this->entries->valid())
        {
            // 次のエントリーが見つかった
            return;
        }
        // 次が無い場合は、親ディレクトリに戻す
        while($this->leave_dir()){
            // 次のエントリーがあるか
            $this->entries->next();
            if ($this->entries->valid()){
                // 次のエントリーが見つかった
                return;
            }
        }
        // 全て検索した
        return;
    }

    // Iterator I/F
    // イテレータを初期化する。
    public function rewind() : void{
        unset($this->parents);
        $this->parents=new \SplStack();
        $this->itr_offset=0;
        $this->enter_dir($this->base_dir);
    }

    // Iterator I/F
    // 次の要素が存在すれば true、存在しなければ false を返す。各繰り返しの前に呼び出される。
    public function valid() : bool{
        if ($this->entries->valid()){
            // カレントディレクトリにまだエントリーがある
            return true;
        }
        // 親のエントリーチェック
        foreach($this->parents as $e){
            if ($e->valid()){
                // 親のいずれかにまだエントリーがある
                return true;
            }
        }
        return false;
    }
};

test.php

<?php

//
// HsDirectoryEntry 呼び出し例
//

require_once 'HsDirectoryEntry.php';

$dirs = new \LibHask\HsDirectoryEntry(__DIR__);
// 全て表示
foreach($dirs as $n) {
    echo $n."\r\n";
}

foreach($dirs as $i => $e) {
    echo '['.$i.'] '.$e."\r\n";
}

まとめ

ファイル列挙はよく行う処理で、再帰処理するか、まとめて列挙するかに分かれます。今回はスタックを活用した再帰処理となります。
ディレクトリエントリーを読みながら応答できる点は、コールバックに近いものの、ループ内に処理が書けるので、PHPでは相性が良いかもしれません。

それではまた、次の記事でお会いしましょう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です