PHP is_dir() 函数

定义和用法

is_dir() 函数检查指定的文件是否是一个目录。如果文件存在并且是一个目录,则返回 TRUE,否则返回 FALSE

这个函数在文件系统操作中非常有用,特别是当你需要区分目录和常规文件,或者在遍历目录结构时需要验证路径类型。

注意:is_dir() 函数的结果会被缓存。如果你在一个请求中多次检查同一个文件,只有第一次会真正调用文件系统,后续调用会使用缓存结果。使用 clearstatcache() 可以清除缓存。

语法

is_dir(string $filename): bool

参数

参数 描述
filename

必需。要检查的文件的路径。

可以是相对路径或绝对路径。相对路径将相对于当前工作目录进行解析。

返回值

返回值 描述
TRUE 指定的文件存在并且是一个目录
FALSE 文件不存在、不是目录、或者发生权限错误

示例

示例1:基本用法

<?php
// 检查常见的目录
$paths = [
    '/tmp',
    '/var/www',
    '/etc/passwd',
    '/nonexistent',
    '.',        // 当前目录
    '..',       // 父目录
    __DIR__,    // 当前脚本所在目录
    __FILE__    // 当前脚本文件
];

foreach ($paths as $path) {
    if (is_dir($path)) {
        echo "$path 是一个目录<br>";
    } else {
        echo "$path 不是一个目录<br>";
    }
}
?>

示例2:遍历目录并区分文件和目录

<?php
/**
 * 列出目录中的文件和子目录
 * @param string $directory 要遍历的目录
 */
function listDirectoryContents($directory) {
    // 首先检查是否为目录
    if (!is_dir($directory)) {
        echo "错误: $directory 不是有效的目录";
        return;
    }

    echo "目录: $directory<br>";
    echo "<ul>";

    // 打开目录并读取内容
    if ($handle = opendir($directory)) {
        while (false !== ($entry = readdir($handle))) {
            // 跳过 . 和 ..
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $fullPath = $directory . '/' . $entry;

            if (is_dir($fullPath)) {
                echo "<li><strong>[$entry]</strong> (目录)</li>";
            } else {
                echo "<li>$entry (文件)</li>";
            }
        }
        closedir($handle);
    }

    echo "</ul>";
}

// 使用示例
listDirectoryContents('/tmp');
?>

示例3:递归统计目录信息

<?php
/**
 * 递归统计目录中的文件和子目录数量
 * @param string $directory 要统计的目录
 * @return array 统计信息
 */
function countDirectoryItems($directory) {
    $stats = [
        'directories' => 0,
        'files' => 0,
        'size' => 0
    ];

    // 检查是否为目录
    if (!is_dir($directory)) {
        return $stats;
    }

    // 打开目录
    if ($handle = opendir($directory)) {
        while (false !== ($entry = readdir($handle))) {
            // 跳过 . 和 ..
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $fullPath = $directory . '/' . $entry;

            if (is_dir($fullPath)) {
                $stats['directories']++;
                // 递归统计子目录
                $subStats = countDirectoryItems($fullPath);
                $stats['directories'] += $subStats['directories'];
                $stats['files'] += $subStats['files'];
                $stats['size'] += $subStats['size'];
            } else {
                $stats['files']++;
                $stats['size'] += filesize($fullPath);
            }
        }
        closedir($handle);
    }

    return $stats;
}

// 使用示例
$directory = '/var/www';
$stats = countDirectoryItems($directory);

echo "目录统计: $directory<br>";
echo "目录数量: " . $stats['directories'] . "<br>";
echo "文件数量: " . $stats['files'] . "<br>";
echo "总大小: " . formatBytes($stats['size']) . "<br>";

// 辅助函数:格式化字节大小
function formatBytes($bytes, $precision = 2) {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);
    $bytes /= pow(1024, $pow);
    return round($bytes, $precision) . ' ' . $units[$pow];
}
?>

示例4:创建目录前的检查

<?php
/**
 * 安全地创建目录(如果不存在)
 * @param string $directory 要创建的目录路径
 * @param int $permissions 目录权限,默认 0755
 * @return bool 是否成功创建或已存在
 */
function createDirectoryIfNotExists($directory, $permissions = 0755) {
    // 检查目录是否已存在
    if (is_dir($directory)) {
        echo "目录已存在: $directory<br>";
        return true;
    }

    // 尝试创建目录
    if (mkdir($directory, $permissions, true)) {
        echo "目录创建成功: $directory<br>";
        return true;
    } else {
        echo "无法创建目录: $directory<br>";
        return false;
    }
}

// 使用示例
$directories = [
    '/tmp/test1',
    '/tmp/test2/subdir',
    '/var/www/uploads/images',
    '/nonexistent/parent/child'  // 这个可能因权限问题失败
];

foreach ($directories as $dir) {
    createDirectoryIfNotExists($dir);
}

// 实际应用:为上传文件创建目录
function prepareUploadDirectory($baseDir, $subDir = null) {
    $uploadDir = $baseDir;

    if ($subDir) {
        $uploadDir = $baseDir . '/' . trim($subDir, '/');
    }

    // 检查并创建目录
    if (!is_dir($uploadDir)) {
        if (!mkdir($uploadDir, 0755, true)) {
            throw new Exception("无法创建上传目录: $uploadDir");
        }
    }

    // 确保目录可写
    if (!is_writable($uploadDir)) {
        throw new Exception("上传目录不可写: $uploadDir");
    }

    return $uploadDir;
}

try {
    $uploadPath = prepareUploadDirectory('/var/www/uploads', '2023/images');
    echo "上传目录准备完成: $uploadPath<br>";
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "<br>";
}
?>

示例5:使用缓存和清除缓存

<?php
// 演示 stat 缓存的影响
$file = '/tmp/test_file.txt';

// 创建测试文件
file_put_contents($file, '测试内容');

echo "初始检查:<br>";
echo "is_dir(): " . (is_dir($file) ? 'TRUE' : 'FALSE') . "<br>";
echo "is_file(): " . (is_file($file) ? 'TRUE' : 'FALSE') . "<br><br>";

// 删除文件
unlink($file);

echo "删除文件后(未清除缓存):<br>";
echo "is_dir(): " . (is_dir($file) ? 'TRUE' : 'FALSE') . "<br>";
echo "is_file(): " . (is_file($file) ? 'TRUE' : 'FALSE') . "<br><br>";

// 清除 stat 缓存
clearstatcache();

echo "清除缓存后:<br>";
echo "is_dir(): " . (is_dir($file) ? 'TRUE' : 'FALSE') . "<br>";
echo "is_file(): " . (is_file($file) ? 'TRUE' : 'FALSE') . "<br><br>";

// 实际应用:监控目录变化
class DirectoryMonitor {
    private $directory;
    private $lastCheck = [];

    public function __construct($directory) {
        $this->directory = $directory;
        $this->refresh();
    }

    public function refresh() {
        clearstatcache(); // 清除缓存以确保获取最新状态

        $contents = [];
        if (is_dir($this->directory)) {
            if ($handle = opendir($this->directory)) {
                while (false !== ($entry = readdir($handle))) {
                    if ($entry !== '.' && $entry !== '..') {
                        $fullPath = $this->directory . '/' . $entry;
                        $contents[$entry] = [
                            'is_dir' => is_dir($fullPath),
                            'size' => filesize($fullPath),
                            'modified' => filemtime($fullPath)
                        ];
                    }
                }
                closedir($handle);
            }
        }

        $this->lastCheck = $contents;
    }

    public function getChanges() {
        $oldContents = $this->lastCheck;
        $this->refresh();
        $newContents = $this->lastCheck;

        $changes = [
            'added' => [],
            'removed' => [],
            'modified' => []
        ];

        // 检查新增的文件
        foreach ($newContents as $name => $info) {
            if (!isset($oldContents[$name])) {
                $changes['added'][$name] = $info;
            }
        }

        // 检查删除的文件
        foreach ($oldContents as $name => $info) {
            if (!isset($newContents[$name])) {
                $changes['removed'][$name] = $info;
            }
        }

        // 检查修改的文件
        foreach ($newContents as $name => $newInfo) {
            if (isset($oldContents[$name])) {
                $oldInfo = $oldContents[$name];
                if ($newInfo['modified'] !== $oldInfo['modified'] ||
                    $newInfo['size'] !== $oldInfo['size']) {
                    $changes['modified'][$name] = [
                        'old' => $oldInfo,
                        'new' => $newInfo
                    ];
                }
            }
        }

        return $changes;
    }
}

// 使用示例
$monitor = new DirectoryMonitor('/tmp');
echo "初始目录状态已记录<br><br>";

// 模拟一些变化
file_put_contents('/tmp/new_file.txt', '新文件内容');
mkdir('/tmp/new_dir');

$changes = $monitor->getChanges();
echo "检测到的变化:<br>";
print_r($changes);
?>

示例6:检查符号链接

<?php
/**
 * 检查符号链接指向的是否为目录
 */
function checkSymbolicLink($path) {
    // 首先检查是否为符号链接
    if (is_link($path)) {
        echo "$path 是一个符号链接<br>";

        // 获取链接目标
        $target = readlink($path);
        echo "链接目标: $target<br>";

        // 检查目标是否为目录
        if (is_dir($target)) {
            echo "符号链接指向一个目录<br>";
        } else {
            echo "符号链接不指向目录<br>";
        }

        return true;
    } else {
        // 不是符号链接,直接检查是否为目录
        if (is_dir($path)) {
            echo "$path 是一个普通目录<br>";
        } else {
            echo "$path 不是目录<br>";
        }

        return false;
    }
}

// 创建测试符号链接(需要适当权限)
$linkPath = '/tmp/test_link';

// 删除可能存在的旧链接
if (file_exists($linkPath)) {
    unlink($linkPath);
}

// 创建指向目录的符号链接
if (symlink('/tmp', $linkPath)) {
    echo "创建符号链接成功<br><br>";

    // 测试符号链接
    checkSymbolicLink($linkPath);
    echo "<br>";

    // 测试普通目录
    checkSymbolicLink('/tmp');
    echo "<br>";

    // 测试普通文件
    $testFile = '/tmp/test.txt';
    file_put_contents($testFile, '测试');
    checkSymbolicLink($testFile);
    unlink($testFile);
}

// 清理
if (file_exists($linkPath)) {
    unlink($linkPath);
}

// 实际应用:安全地解析符号链接
function resolvePath($path) {
    // 如果是符号链接,解析它
    if (is_link($path)) {
        $target = readlink($path);

        // 处理相对路径链接
        if ($target[0] !== '/') {
            $target = dirname($path) . '/' . $target;
        }

        // 递归解析
        return resolvePath($target);
    }

    return $path;
}

echo "<br>路径解析示例:<br>";
// 创建多级符号链接进行测试
$link1 = '/tmp/link1';
$link2 = '/tmp/link2';

if (file_exists($link1)) unlink($link1);
if (file_exists($link2)) unlink($link2);

symlink('/tmp', $link1);
symlink($link1, $link2);

echo "link2 最终指向: " . resolvePath($link2) . "<br>";
echo "是否为目录: " . (is_dir(resolvePath($link2)) ? '是' : '否') . "<br>";

// 清理
unlink($link1);
unlink($link2);
?>

is_dir() 与其他文件检查函数的比较

函数 描述 示例返回值
is_dir() 检查是否为目录 /tmp → TRUE, /etc/passwd → FALSE
is_file() 检查是否为常规文件 /etc/passwd → TRUE, /tmp → FALSE
is_link() 检查是否为符号链接 符号链接 → TRUE,其他 → FALSE
file_exists() 检查文件或目录是否存在 存在 → TRUE,不存在 → FALSE
is_readable() 检查是否可读 有读取权限 → TRUE
is_writable() 检查是否可写 有写入权限 → TRUE

is_dir() 与 file_exists() 的关系

重要关系:

  • 如果 is_dir($path) 返回 TRUE,那么 file_exists($path) 也一定返回 TRUE
  • 如果 file_exists($path) 返回 FALSE,那么 is_dir($path) 也一定返回 FALSE
  • 如果 file_exists($path) 返回 TRUE,is_dir($path) 可能返回 TRUE 或 FALSE(取决于路径类型)

最佳实践

  1. 结合错误抑制符使用:当路径可能不存在时,使用 @is_dir() 避免不必要的警告
  2. 使用前验证路径:特别是处理用户输入时,先验证和清理路径
  3. 注意缓存问题:如果在同一脚本中文件状态可能改变,使用 clearstatcache()
  4. 检查权限:is_dir() 可能因为权限不足而返回 FALSE,即使目录存在
  5. 处理符号链接:is_dir() 对符号链接返回 FALSE,即使链接指向目录。使用 is_link()readlink() 处理符号链接
  6. 使用绝对路径:相对路径可能导致意外结果,尽量使用绝对路径
  7. 考虑使用 SplFileInfo:对于面向对象的代码,SplFileInfo 类提供更丰富的功能

常见错误

错误 原因 解决方法
Warning: is_dir(): open_basedir restriction in effect 路径超出了 open_basedir 的限制 检查 open_basedir 配置,或使用允许范围内的路径
返回 FALSE 但目录存在 权限不足或符号链接问题 检查权限,或使用 is_link() 检查是否为符号链接
缓存导致的问题 文件状态已改变但未清除缓存 使用 clearstatcache() 清除缓存
相对路径问题 当前工作目录改变导致相对路径解析错误 使用绝对路径,或保存当前工作目录
符号链接返回 FALSE is_dir() 对符号链接返回 FALSE 先检查是否为符号链接,再检查链接目标

性能考虑

stat 缓存

PHP 会缓存文件系统 stat 调用结果,多次调用 is_dir() 检查同一文件通常只有第一次有实际 I/O 开销。

批量检查优化

批量检查目录时,考虑使用 scandir() 获取所有条目,然后批量处理,而不是对每个路径单独调用 is_dir()。

避免冗余检查

如果已经知道路径类型(例如从 opendir() 获取),避免重复调用 is_dir()。

使用更高效的方法

对于简单的存在性检查,file_exists() 可能比 is_dir() 稍快,但功能不同。

相关函数