PHP の Windows 版でのみ発生する is_dir の奇妙な現象

PHP Advent Calendar 2013 in Adventar の16日目です。


下記のコード、Windows で実行するとどうなると思いますか?

<?php
//$dir = __DIR__ . DIRECTORY_SEPARATOR . 'xxx' . DIRECTORY_SEPARATOR;
$dir = __DIR__ . DIRECTORY_SEPARATOR . 'xxx';
mkdir($dir);
$iterator = new FilesystemIterator($dir);
foreach ($iterator as $file) {}

clearstatcache();
var_dump(is_dir($dir));

rmdir($dir);
unset($iterator);

clearstatcache();
var_dump(is_dir($dir));

結果は true false です。ディレクトリを削除しているのだから当たり前です。

では 12 行目の unset をコメントアウトするとどうなるでしょうか?

<?php
//$dir = __DIR__ . DIRECTORY_SEPARATOR . 'xxx' . DIRECTORY_SEPARATOR;
$dir = __DIR__ . DIRECTORY_SEPARATOR . 'xxx';
mkdir($dir);
$iterator = new FilesystemIterator($dir);
foreach ($iterator as $file) {}

clearstatcache();
var_dump(is_dir($dir));

rmdir($dir);
//unset($iterator);

clearstatcache();
var_dump(is_dir($dir));

同じ結果になりそうですが・・・結果は true true です。イテレータを unset しないと is_dir が true を返しました。 これは opendir で開きっぱなしにしても同じです。


それでは次に、2行目と3行目のコメントを入れ替えてディレクトリ名の終端に \ を付与するとどうなるでしょうか?

<?php
$dir = __DIR__ . DIRECTORY_SEPARATOR . 'xxx' . DIRECTORY_SEPARATOR;
//$dir = __DIR__ . DIRECTORY_SEPARATOR . 'xxx';
mkdir($dir);
$iterator = new FilesystemIterator($dir);
foreach ($iterator as $file) {}

clearstatcache();
var_dump(is_dir($dir));

rmdir($dir);
//unset($iterator);

clearstatcache();
var_dump(is_dir($dir));

終端に \ があろうがなかろうが結果は同じような気がしますが・・・ 実際には ts (Thread Safe) なのか nts (Non Thread Safe) なのかによって結果が変わります。

  • nts の場合 ... true false
  • ts の場合 ... true true

謎です。

原因

これだけだと内容がショボイので詳しく調べました。

Non Thread Safe の場合

is_dir は WinAPI の GetFileAttributesEx を呼んで成功したらその結果を使い、失敗したら stat を呼んでその結果を使います。

イテレータをアンセットしていない場合(ディレクトリを開きっぱなしの場合)、GetFileAttributesEx は失敗しますが stat は成功します。

ただし、stat はディレクトリ名の終端が \ で終わっていると必ず失敗します。

日本語訳だと \ を含むとダメっぽいですが、原文だと末尾の \ だけがダメとなっています。

そのため・・・

  • ディレクトリ終端が \ でない → true
  • ディレクトリ終端が \ である → false

という結果になりました。

is_dir から stat が呼ばれるまでの流れ・・・

Thread Safe の場合

一方 Thread Safe の場合は GetFileAttributesEx や stat が呼ばれる前に FindFirstFile でディレクトリ名を正規化しています。

そのため、ディレクトリ名の終端に \ が付いていても正規化で取り除かれるため、結果は変わりません。

is_dir から FindFirstFile で正規化するまでの流れ・・・

さいごに

ディレクトリを開きっぱなしにしない限り発生しないので滅多に問題にはならないと思いますが、 opendir なら普通は不用になったら即 closedir するのに対して、イテレータだとデストラクタが良きに計らってくれるため、あまり気にしないと思います。

イテレータでディレクトリを走査し、そのディレクトリを削除して、同じスコープ(あるいはそのスコープからの呼び出し先)でディレクトリの存在確認をする場合は注意が必要です。

なお、手元にあった Linux(CentOS6 with remi-php55) の php-5.5.7-nts で試したところ、どのケースでも true false でした。