読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

SplFileInfoのメソッドとファイルシステム関数の比較調査メモ

PHP SPL

PHPマニュアルの説明を読んでも違いがよく分からない SplFileInfo のファイル名やパスを取得する類のメソッドを、ファイルシステム関数と比較調査したメモです。

SplFileInfo::__construct() の説明には "file_name で指定したファイル用の新しい SplFileInfo オブジェクトを作成します。 ファイルが存在する必要はなく、また読み込み可能である必要もありません。" とあります。
オブジェクトの生成にはファイルの有無は関係ないことを念頭におきつつ、以下のパターンでいくつかのメソッドの結果を調査しました。

(1) 絶対パス、ファイル SplFileInfo(__FILE__)
(2) 絶対パス、ディレクトリ SplFileInfo(__DIR__)
(3) 相対パス、ファイル SplFileInfo('splfileinfo.test.php')
(4) 相対パス、ディレクトリ SplFileInfo('./')
(5) 存在しないファイル SplFileInfo('/path/to/file.php')
(6) 存在しないディレクトリ SplFileInfo('/path/to/')
(7) 非ASCII文字のファイル SplFileInfo(__DIR__ . DIRECTORY_SEPARATOR . '日本語.txt')
(8) 非ASCII文字のディレクトリをパスに含むファイル SplFileInfo(__DIR__ . DIRECTORY_SEPARATOR . '日本語' . DIRECTORY_SEPARATOR . 'test.txt')

調査した環境は以下の通りです。
Windows7 PHP5.4.8
Windows7 PHP5.3.8 (XAMPP)
Linux PHP5.4.7
Linux PHP5.3.14

SplFileInfo::getPathname()

コンストラクタで与えられた引数がほぼそのまま返されます。ただし、終端のディレクトリセパレータは削除されるようです。..は展開されません。ファイルの実体は不要です。

SplFileInfo::getPath()

コンストラクタで与えられた引数のうち、ディレクトリセパレータで区切られた最後のセグメントを取り除いた結果が返されます。..は展開されません。ファイルの実体は不要です。

SplFileInfo::getFilename()

コンストラクタで与えられた引数のうち、ディレクトリセパレータで区切られた最後のセグメントが返されます。ファイルの実体は不要です。

SplFileInfo::getBasename()

getFilename()との違いがよく分からないのですが、LinuxのPHP5.3.14では(7)のケースでファイル名 "日本語.txt" ではなく ".txt"が返され、Windows版PHP5.3.8では空文字が返されました。
Linux版PHP5.4.7とWindows版PHP5.4.8とも問題なく "日本語.txt" が返されたので、おそらくバグではないかと思います。

また、ここでは basename() の結果は載せていませんが、(8)のケースでWindows版PHP5.4.8では basename() の方で "日本語\test.txt" が返されていました。
ちなみにWindows版PHP5.3.8では SplFileInfo::getBasename() と同じく "test.txt" が返されます。

SplFileInfo::getRealPath()

コンストラクタで与えられた引数の.や..が展開されたフルパスが返されます。ファイルやディレクトリが存在しない場合はFALSEが返されます。

(7)および(8)のケースで、Windows版PHP5.3.8およびWindows版PHP5.4.8ではFALSEが返されました。
前述の basename() の件といい、Windows環境での非ASCII文字を含むパスの扱いに問題があるのかもしれません。

SplFileInfo::getType()

ディレクトリの場合は "dir" 、ファイルの場合は "file" が返されます。存在しない場合は RuntimeException がスローされます。
Windows環境でパスに非ASCII文字が含まれる場合のように、SplFileInfo::getRealPath()が期待する結果を返さないケースでも、RuntimeException がスローされます。

SplFileInfo::isDir()

ディレクトリの場合はTRUE、ファイルの場合または存在しない場合はFALSEが返されます。
Windows環境でパスに非ASCII文字が含まれる場合のように、SplFileInfo::getRealPath()が期待する結果を返さないケースでも、FALSE が返されます。

SplFileInfo::isFile()

ファイルの場合はTRUE、ディレクトリの場合または存在しない場合はFALSEが返されます。
Windows環境でパスに非ASCII文字が含まれる場合のように、SplFileInfo::getRealPath()が期待する結果を返さないケースでも、FALSE が返されます。

SplFileInfo::isReadable()

ファイルまたはディレクトリが読み込み可能な場合はTRUE、読み込み不可または存在しない場合はFALSEが返されます。
Windows環境でパスに非ASCII文字が含まれる場合のように、SplFileInfo::getRealPath()が期待する結果を返さないケースでも、FALSE が返されます。

SplFileInfo::getPathInfo()

getPath()で返される値を引数に生成された、SplFileInfoオブジェクトが返されるようです。

SplFileInfo::__toString()

SplFileInfo::getPathname()と同じ結果が返されるようです。


以上の結果から推測すると、おそらく SplFileInfo::getPathname(), SplFileInfo::getPath(), SplFileInfo::getFilename(), SplFileInfo::getBasename() についてはディレクトリセパレータで文字列処理を行なっているだけではないかと思います。
ただし、Windows版で非ASCII文字を含むパスの basename() がおかしい、同じ環境での SplFileInfo::getBasename() と違う結果になるのは謎です。(Windowsの場合だけ何か特殊な処理をしてるんでしょうか…)

マニュアルの記述からも分かるとは思いますが、SplFileInfo::getRealPath() についてはそのメソッド名の通り、パスを展開して実際のファイルを検出しているようです。
SplFileInfo::getType(), SplFileInfo::isDir(), SplFileInfo::isFile(), SplFileInfo::isReadable() のようにファイルの属性を見るメソッドも、実際のファイルが検出されない場合は期待とは異なる結果が返されています。


ファイルシステム関数の中からそれぞれ対応していると思われる関数(いくつか抜粋)

SplFileInfo::getPath() → dirname($path), pathinfo($path, PATHINFO_DIRNAME)
SplFileInfo::getBasename() → basename($path), pathinfo($path, PATHINFO_BASENAME)
SplFileInfo::getExtension() → pathinfo($path, PATHINFO_EXTENSION)
SplFileInfo::isDir() → is_dir($path)
SplFileInfo::isFile() → is_file($path)
SplFileInfo::isReadable() → is_readable($path)
SplFileInfo::isWritable() → is_writable($path)

注意したいのは SplFileInfo::getFilename() と pathinfo($path, PATHINFO_FILENAME) で、メソッド名から何となく同じ処理かと考えてしまいますが、pathinfo() の例の通り、後者は拡張子を除いたファイル名が返されます。
(むしろ、SplFileInfo::getFilename() と SplFileInfo::getBasename() の違いが分からない…)

また、普段シンボリックリンクPHPから扱うことはほとんどないので今回は調べていませんが、シンボリックリンクの場合はまた注意すべきところがあるかもしれません。

SplFileInfo には今でもよく使う file_exists() に相当するメソッドはないようなので、同様の処理を行いたい場合は SplFileInfo::getRealPath() と SplFileInfo::isFile() や SplFileInfo::isDir() を併用すればいいでしょうか。
また、このクラスはファイル操作用ではなく "各ファイルの情報を取得するための上位レベルのオブジェクト指向インターフェイス" ということで、ファイルの新規作成や属性の変更、読み書きについては従来通りファイルシステム関数を利用するか、SplFileInfo::openFile() が返す SplFileObject を使う必要があります。

あと意外と役立つケースがありそうなのが SplFileInfo::getPathInfo() の存在で、あるファイルに対して何か操作するに当たって、親(ディレクトリ)の情報を取得したい場合に面倒なパスの文字列操作が不要になり、かなり簡潔に書けます。
また、SplFileInfo::setInfoClass() でSplFileInfoを継承したクラスの名前を指定しておけば SplFileInfo::getPathInfo() が、SplFileInfo::setFileClass() でSplFileObjectを継承したクラスの名前を指定しておけば SplFileInfo::openFile() が、それぞれ指定したオブジェクトを返してくれるようです。

ともあれ、SplFileInfo は RecursiveDirectoryIterator や Symfony の Finder コンポーネントを通じて扱うことがあるので、ファイルシステム関数との違いはしっかり把握しておかないとですね。
併せて、非ASCII文字を含むパスやファイルを正常に扱えるかどうかも、実際の環境で確認しておく必要がありそうです。

Windows環境における非ASCII文字を含むパスについて [2012-10-22 追記]

再検証したところ、PHPのバグでなく、ファイル名の文字コードLinux版の場合とは異なってSJIS(というかCP932?)が利用されることが原因のようです。
Windows環境で SplFileInfo::__construct() の引数に渡すパスをUTF-8SJIS-winに変換して試したところ、正常に結果が返されることを確認しました。
(それにしても、UTF-8で出力するならその都度変換しないといけないので、これ結構面倒ですね…Windows環境への対応が必要な場合、SplFileInfoを継承したクラスを作る方が楽な気が…)

ただし、Linux版PHP5.3.14およびWindows版PHP5.3.8の両環境において、期待する結果が返されないのは変わりませんでした。
具体的には (7)の日本語ファイル名の SplFileInfo::getBasename(), basename(), pathinfo($file, PATHINFO_BASENAME), pathinfo($file, PATHINFO_FILENAME) の結果がNGです。
SplFileInfo::getRealPath() はOKなので、basename() がマルチバイトを考慮していないようです。(検索してみたところ、どうも5.2の頃から既知の不具合のようで…)
Windows,Linuxとも5.4以前の環境では basename() や pathinfo() で非ASCII文字を含むパスを扱う際は要注意ということですね。

ちなみに、厳密にはWindowsでもOSカーネル内部ではUnicodeで管理されていて、アプリケーションに渡す時にSJIS (CP932?)に変換しているそうです。
参考 Linux at IBM | Windows のファイル名の文字コードについて

最後に検証用コードを。


ちなみに、SplFileInfo::openFile() が返す SplFileObject はCSVを扱うためのメソッドを備えていて、これとPHP5.4で追加された CallbackFilterIterator を使ってCSVファイルのデータをフィルタしつつ処理する方法を、過去の記事に書いてます。
SplFileObjectとPHP5.4のCallbackFilterIteratorでCSV処理

[2014-09-03 追記]

この記事内容は誤りを含んでいる可能性が高いです。

Windows環境(Windows7 + PHP 5.6.0)のSplInfoでの日本語ファイルパスの扱いについて、記事を書きました。

Windows環境のPHPで日本語ファイルパスを扱う場合の注意点 - k-holyのPHPとか諸々メモ