Skip to content

目录操作

概述

PHP 提供了完整的目录操作函数集,包括创建、删除、遍历目录等功能。mkdir()rmdir()scandir()glob() 是最常用的目录操作函数。递归遍历目录树是文件系统操作中的常见需求。

适用场景

  • 创建项目目录结构
  • 遍历日志/缓存目录
  • 文件管理器实现
  • 自动化部署脚本

基础概念

核心目录函数

函数功能返回值
mkdir()创建目录bool
rmdir()删除空目录bool
scandir()列出目录内容array|false
glob()模式匹配文件路径array|false
readdir()从目录句柄读取条目string|false
opendir()打开目录句柄resource|false
closedir()关闭目录句柄void
rewinddir()重置目录指针void

PHP 8.0+ 变更

mkdir()rmdir()$permissions 参数已弃用仅传递而不传递 $context 的用法。

语法与代码示例

创建目录

php
<?php

// 基本创建
$created = mkdir('/tmp/mydir');

if (!$created) {
    throw new RuntimeException('目录创建失败');
}

// 创建嵌套目录(递归)
$created = mkdir('/tmp/project/src/controllers', 0755, true);

// 设置权限
$created = mkdir('/tmp/restricted', 0700); // 仅所有者可读写执行

删除目录

php
<?php

// rmdir 只能删除空目录
$deleted = rmdir('/tmp/mydir');

if (!$deleted) {
    throw new RuntimeException('目录删除失败(可能目录不为空)');
}

// 递归删除非空目录
function removeDirectory(string $dir): void
{
    if (!is_dir($dir)) {
        return;
    }

    $items = scandir($dir);
    if ($items === false) {
        throw new RuntimeException("无法读取目录: {$dir}");
    }

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') {
            continue;
        }

        $path = $dir . DIRECTORY_SEPARATOR . $item;
        is_dir($path) ? removeDirectory($path) : unlink($path);
    }

    rmdir($dir);
}

scandir 列出目录

php
<?php

// 列出目录所有文件(包含 . 和 ..)
$files = scandir('/tmp');

if ($files === false) {
    throw new RuntimeException('目录读取失败');
}

print_r($files);
// [0] => .  [1] => ..  [2] => file1.txt  [3] => file2.txt

// 按降序排列
$files = scandir('/tmp', SCANDIR_SORT_DESCENDING);

// 按自然排序
$files = scandir('/tmp', SCANDIR_SORT_NONE);

// 排除 . 和 ..
$files = array_values(array_filter(
    scandir('/tmp'),
    fn(string $item) => !in_array($item, ['.', '..'])
));

glob 模式匹配

php
<?php

// 获取所有 PHP 文件
$phpFiles = glob('/tmp/project/*.php');

// 递归获取所有 PHP 文件(PHP 7.2+ ** 操作符)
$allPhpFiles = glob('/tmp/project/**/*.php', GLOB_BRACE);

// 多种扩展名匹配
$codeFiles = glob('/tmp/project/*.{php,js,css}', GLOB_BRACE);

// 获取隐藏文件
$allFiles = glob('/tmp/project/{,.}*', GLOB_BRACE);

// 获取目录
$dirs = glob('/tmp/project/*', GLOB_ONLYDIR);

glob 性能

glob() 在包含大量文件的目录中性能较差,对于深度递归搜索建议使用 RecursiveDirectoryIterator(SPL)。

opendir/readdir 逐条读取

php
<?php

$handle = opendir('/tmp');

if ($handle === false) {
    throw new RuntimeException('无法打开目录');
}

while (($entry = readdir($handle)) !== false) {
    // 跳过 . 和 ..
    if ($entry === '.' || $entry === '..') {
        continue;
    }

    $fullPath = '/tmp/' . $entry;
    $type = is_dir($fullPath) ? '[DIR]' : '[FILE]';
    echo "{$type} {$entry}\n";
}

closedir($handle);

实战示例

递归目录遍历器

php
<?php

declare(strict_types=1);

class DirectoryScanner
{
    /**
     * 递归获取所有文件
     * @return string[]
     */
    public static function scanFiles(string $dir, string $pattern = '*'): array
    {
        $result = [];

        if (!is_dir($dir)) {
            return $result;
        }

        $items = glob($dir . DIRECTORY_SEPARATOR . $pattern);

        foreach ($items as $item) {
            if (is_file($item)) {
                $result[] = $item;
            }
        }

        // 递归遍历子目录
        $subDirs = glob($dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR);
        foreach ($subDirs as $subDir) {
            $result = array_merge($result, self::scanFiles($subDir, $pattern));
        }

        return $result;
    }

    /**
     * 获取目录树结构
     */
    public static function tree(string $dir, int $maxDepth = 0, int $depth = 0): string
    {
        if (!is_dir($dir)) {
            return '';
        }

        if ($maxDepth > 0 && $depth >= $maxDepth) {
            return '';
        }

        $output = '';
        $items = scandir($dir);

        if ($items === false) {
            return $output;
        }

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $prefix = str_repeat('  ', $depth);
            $path = $dir . DIRECTORY_SEPARATOR . $item;
            $icon = is_dir($path) ? '+' : '-';
            $output .= "{$prefix}{$icon} {$item}\n";

            if (is_dir($path)) {
                $output .= self::tree($path, $maxDepth, $depth + 1);
            }
        }

        return $output;
    }
}

// 使用示例
$files = DirectoryScanner::scanFiles('/tmp/project', '*.php');
echo count($files) . " 个 PHP 文件\n";

echo DirectoryScanner::tree('/tmp/project', 3);
/*
输出示例:
+ src
  - index.php
  + controllers
    - UserController.php
    - OrderController.php
  + models
    - User.php
- composer.json
- README.md
*/

使用 SPL 迭代器遍历目录

php
<?php

declare(strict_types=1);

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use FilesystemIterator;
use RegexIterator;

// RecursiveDirectoryIterator 递归遍历
$dirIterator = new RecursiveDirectoryIterator(
    '/tmp/project',
    FilesystemIterator::SKIP_DOTS // 跳过 . 和 ..
);

$iterator = new RecursiveIteratorIterator(
    $dirIterator,
    RecursiveIteratorIterator::SELF_FIRST // 先处理父目录
);

foreach ($iterator as $file) {
    /** @var SplFileInfo $file */
    $indent = str_repeat('  ', $iterator->getDepth());
    $type = $file->isDir() ? '[DIR]' : '[FILE]';
    $size = $file->isFile() ? ' (' . $file->getSize() . ' bytes)' : '';
    echo "{$indent}{$type} {$file->getFilename()}{$size}\n";
}

// 使用正则过滤
$dirIterator = new RecursiveDirectoryIterator(
    '/tmp/project',
    FilesystemIterator::SKIP_DOTS
);
$flatIterator = new RecursiveIteratorIterator($dirIterator);
$regexIterator = new RegexIterator($flatIterator, '/\.php$/i');

foreach ($regexIterator as $file) {
    echo $file->getPathname() . PHP_EOL;
}

目录大小统计

php
<?php

declare(strict_types=1);

function getDirectorySize(string $dir): int
{
    $size = 0;

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::LEAVES_ONLY
    );

    foreach ($iterator as $file) {
        if ($file->isFile()) {
            $size += $file->getSize();
        }
    }

    return $size;
}

function formatBytes(int $bytes): string
{
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $index = (int)floor(log(max($bytes, 1), 1024));
    $index = min($index, count($units) - 1);
    return round($bytes / pow(1024, $index), 2) . ' ' . $units[$index];
}

$size = getDirectorySize('/tmp/project');
echo "目录大小: " . formatBytes($size) . PHP_EOL;

注意事项

权限问题

php
<?php

// 检查目录是否可写
if (!is_writable('/tmp/cache')) {
    throw new RuntimeException('缓存目录不可写');
}

// mkdir 权限受 umask 影响
// 实际权限 = 0777 & ~umask
// 例如 umask=022,mkdir(path, 0777) 实际权限为 0755
$oldUmask = umask(0);
mkdir('/tmp/newdir', 0777);
umask($oldUmask);

umask 影响

mkdir() 的权限参数会被系统的 umask 掩码影响。如果需要精确权限,先 umask(0) 再创建目录。

符号链接处理

php
<?php

// glob 不跟随符号链接
$files = glob('/tmp/link/*'); // 读取链接目标的内容

// 判断是否为符号链接
if (is_link('/tmp/mylink')) {
    $target = readlink('/tmp/mylink');
    echo "链接目标: {$target}\n";
}

// RecursiveDirectoryIterator 中的符号链接
$flags = FilesystemIterator::SKIP_DOTS;
$flags |= FilesystemIterator::FOLLOW_SYMLINKS; // 跟随符号链接
$iterator = new RecursiveDirectoryIterator('/tmp', $flags);

最佳实践

1. 使用 SPL 替代原生函数

php
<?php

// 原生方式 —— 功能有限
$files = scandir('/tmp');
foreach ($files as $file) {
    if ($file === '.' || $file === '..') continue;
    // ...
}

// SPL 方式 —— 功能强大
$iterator = new DirectoryIterator('/tmp');
foreach ($iterator as $fileInfo) {
    if ($fileInfo->isDot()) continue;
    echo $fileInfo->getFilename() . PHP_EOL;
    echo $fileInfo->getSize() . PHP_EOL;
    echo $fileInfo->getMTime() . PHP_EOL;
}

2. 安全的目录操作

php
<?php

// 检查路径是否在允许的根目录内
function isWithinBaseDir(string $path, string $baseDir): bool
{
    $realPath = realpath($path);
    $realBase = realpath($baseDir);

    if ($realPath === false || $realBase === false) {
        return false;
    }

    return str_starts_with($realPath, $realBase . DIRECTORY_SEPARATOR);
}

// 使用示例
if (!isWithinBaseDir('/tmp/project/../../../etc/passwd', '/tmp/project')) {
    throw new RuntimeException('路径越权');
}

3. 目录操作封装

php
<?php

declare(strict_types=1);

class DirHelper
{
    public static function ensure(string $path, int $permissions = 0755): void
    {
        if (!is_dir($path) && !mkdir($path, $permissions, true)) {
            throw new RuntimeException("目录创建失败: {$path}");
        }
    }

    public static function clean(string $path, bool $keepDir = true): void
    {
        if (!is_dir($path)) {
            return;
        }

        $items = new DirectoryIterator($path);
        foreach ($items as $item) {
            if ($item->isDot()) continue;

            $itemPath = $item->getPathname();
            $item->isDir() ? self::clean($itemPath, false) : unlink($itemPath);
        }

        if (!$keepDir) {
            rmdir($path);
        }
    }

    public static function copy(string $source, string $dest): void
    {
        if (!is_dir($source)) {
            throw new RuntimeException("源目录不存在: {$source}");
        }

        self::ensure($dest);

        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            $destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathname();
            $item->isDir() ? mkdir($destPath) : copy($item->getPathname(), $destPath);
        }
    }
}

// 使用
DirHelper::ensure('/tmp/cache/logs');
DirHelper::clean('/tmp/cache/temp');
DirHelper::copy('/tmp/project', '/tmp/project-backup');

参考链接