肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

PHP 5.3 : RecursiveDirectoryIteartor の current が SplFileInfo を返すから isDot とか使えない

夜も遅くにこんばんは.

SPL関連でいうと,ちょっと調べてたら妙な挙動を発見した.

DirectoryIterator

DirectoryIterator は,ディレクトリをイテレートするためのイテレータで,例えば,以下のようなコードで,ディレクトリの中身を出力することができる.以下はマニュアルに載っているコード.*1

<?php
$it = new DirectoryIterator(dirname(__FILE__));

foreach ($it as $f) {
    if (!$f->isDot()) {
        echo $f->getFilename() . "\n";
    }
}  

ところで,このクラスは,SplFileInfo を継承しているので,SplFileInfoの持っているメソッドは全部使える,が,SplFileInfo には isDot というメソッドがないため,どうやら DirectoryIterator で実装されている.

RecursiveDirectoryIterator

で,DirecoryIterator を継承している(はず ((http://jp2.php.net/manual/ja/class.recursivedirectoryiterator.php))) RecursiveDirectoryIterator でも同じことができるよねってことで,やってみたところエラーがでた.

<?php
$it = new RecursiveDirectoryIterator(dirname(__FILE__));
foreach ($it as $f) {
    if (!$f->isDot()) {
        echo $f->getFilename(), "\n";                                                                                                                                                                                                       
    }
}
Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/test.php on line 12

おうふ?
どうやら RecursiveDirectoryIterator の current が,自分自身ではなく,SplFileInfo を返してきているから isDot なんてないよってエラーになる.

RecursiveDirectoryIterator が本当に継承しているもの

は,実は DirectoryIterator ではなく,FilesystemIterator でした.

2668     REGISTER_SPL_SUB_CLASS_EX(RecursiveDirectoryIterator, FilesystemIterator, spl_filesystem_object_new, spl_RecursiveDirectoryIterator_functions);
2669     REGISTER_SPL_IMPLEMENTS(RecursiveDirectoryIterator, RecursiveIterator);

で,この変更は,PHP 5.3 からのもの.FilesystemIterator は 5.3 で入りましたからね.FilesystemIterator の current は,フラグによってその返り値が変わるんだけど,このデフォルトが CURRENT_AS_FILEINFO になっているので,SplFileInfo が返ってきてしまう.

なので,望みどおりの動きをさせるには,

<?php
$it = new RecursiveDirectoryIterator(dirname(__FILE__), RecursiveDirectoryIterator::CURRENT_AS_SELF);
foreach ($it as $f) {
    if (!$f->isDot()) {
        echo $f->getFilename(), "\n";                                                                                                                                                                                                       
    }
}

としてあげればいいんですけど,この情報はとりあえずマニュアルにも書いてなかったし,後方非互換の変更なんじゃないかなあ,などと思いつつソースの履歴を追ってみた.
最初は,当然 PHP 5.3 になって, FilesystemIterator が導入されたからそいつを継承させるように変更したら,FilesystemIterator の current がデフォルトで CURRENT_AS_FILEINFO なので,DirectoryIterator と挙動がかわってしまった ... !という類の,ボンミスかとおもった.

が,調べてみたらちょっと違う気がしてきた.

この変更がどこで入ったか

実は,phpallで検証すると,PHP 5.1.2 からこんな挙動になっている.

<?php                                                                                                                                                                                                                                       
$directory = '.';
$itt = new RecursiveDirectoryIterator($directory);
foreach ($itt as $i) {
    $i->isDot();
    break;
}
% phpall rd_test.php 2>/dev/null
php-5.0.0: 
php-5.0.1: 
php-5.0.2: 
php-5.0.3: 
php-5.0.4: 
php-5.0.5: 
php-5.1.0: 
php-5.1.1: 
php-5.1.2:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.1.3:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.1.5:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.1.6:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.0:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.1:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.2:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.3:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.4:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.5:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.6:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.8:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.9:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.10:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.2.11:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26
php-5.3.0:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26 Call Stack: 0.0005 333208 1. {main}() /home/sotarok/working/php/spl/rd_test.php:0
php-5.3.1:  Fatal error: Call to undefined method SplFileInfo::isDot() in /home/sotarok/working/php/spl/rd_test.php on line 26 Call Stack: 0.0003 333208 1. {main}() /home/sotarok/working/php/spl/rd_test.php:0

なんでだろうと思って一応,継承元の変更点がいつなのかも調べてみた.

<?php                                                                                                                                                                                                                                       
$directory = '.';
$itt = new RecursiveDirectoryIterator($directory);
var_dump(class_parents($itt));
% phpall rd_test.php 2>/dev/null                                                                                                                                                                [~/working/php/spl (master)]
php-5.0.0: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.0.1: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.0.2: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.0.3: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.0.4: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.0.5: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.1.0: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.1.1: array(1) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" }
php-5.1.2: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.1.3: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.1.5: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.1.6: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.0: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.1: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.2: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.3: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.4: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.5: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.6: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.8: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.9: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.10: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.2.11: array(2) { ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.3.0: array(3) { ["FilesystemIterator"]=> string(18) "FilesystemIterator" ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }
php-5.3.1: array(3) { ["FilesystemIterator"]=> string(18) "FilesystemIterator" ["DirectoryIterator"]=> string(17) "DirectoryIterator" ["SplFileInfo"]=> string(11) "SplFileInfo" }


んー.一応継承元は 5.3 から FilesystemIterator に切り替わったみたいなんだけど,5.1.2 で SplFileInfo を基底にもつようになってから current の挙動が変わったんですかね.そのうえ,継承元が変わったのは 5.3 だからあたかもこの CURRENT_AS_FILEINFO や CURRENT_AS_SELF が FilesystemIteraotr の定数かのように見えるけど,実は,↑のCURRENT_AS_SELFフラグをつけたものは, PHP 5.1.2 から動く (つまり SplFileInfoに関する変更が入ったときから動いてる).
ただし,5.3.0 でこの定数定義が RecursiveDirectoryIterator のものではなく FilesystemIterator のものに変更されている.

どのへんが問題なのかよくわからないから一旦まとめ

  • とりあえずドキュメントの情報は正しくない
    • RecursiveIteratorIterator が DirectoryIterator を継承してるという情報は, 5.3.0 からは,偽.実際は FilesystemIterator
    • RecursiveIteartorIterator には 5.1.2 からいくつかフラグが用意されていた!

cf.

% php-5.1.2 --re spl
...
    Class [ <internal:SPL> <iterateable> class RecursiveDirectoryIterator extends DirectoryIterator implements Traversable, Iterator, RecursiveIterator ] {

      - Constants [8] {
        Constant [ integer CURRENT_MODE_MASK ] { }
        Constant [ integer CURRENT_AS_PATHNAME ] { }
        Constant [ integer CURRENT_AS_FILEINFO ] { }
        Constant [ integer CURRENT_AS_SELF ] { }
        Constant [ integer KEY_MODE_MASK ] { }
        Constant [ integer KEY_AS_PATHNAME ] { }
        Constant [ integer KEY_AS_FILENAME ] { }
        Constant [ integer NEW_CURRENT_AND_KEY ] { }
      }
...
  • RecursiveDirectoryIterator が SplFileInfo をデフォルトで返すという挙動は正しいのか (CURRENT_AS_SELF じゃないと isDot とかつかえねーじゃん.*2 )
    • ちょっとわからん.5.1.2 の変更のときにそういうバグとか報告なかったのか,ちらっと探してみたんだけど,みあたらなかった*3.仕様だとしたらしかたないんだけど,バグだったら ... 5.1.2 って 2006 年だよ,でたの ...


というわけで,これが,ダメだと思う人〜?
バグ報告すべきかな?

*1:http://jp2.php.net/manual/ja/directoryiterator.isdot.php

*2:飛ばすだけなら,SKIP_DOTS フラグが使えるけどw

*3:ちらっとすぎたのかもしれん?