目录操作
概述
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');