本文提供一個輕量級的 PHP 目錄遞歸掃描函數,支持子目錄遍歷、文件過濾、自定義處理回調,可作為文件管理、批量處理的基礎工具。
<?php
/**
* 遞歸掃描目錄文件
* @param string $dir 目錄路徑
* @param callable|null $callback 處理回調 function($filePath, $isDir)
* @param array $options 選項 ['skip_dots'=>true, 'follow_links'=>false]
* @return array 文件路徑列表 (無回調時返回)
*/
function readFileFromDir($dir, $callback = null, $options = []) {
$defaults = [
'skip_dots' => true,
'follow_links' => false,
'max_depth' => -1, // -1 無限制
];
$options = array_merge($defaults, $options);
if (!is_dir($dir)) {
return false;
}
$results = [];
$handle = opendir($dir);
if (!$handle) {
return false;
}
// 使用棧模擬遞歸,避免深度過大導致棧溢出
$stack = [[$dir, 0]];
while (!empty($stack)) {
[$currentDir, $depth] = array_pop($stack);
if ($options['max_depth'] >= 0 && $depth > $options['max_depth']) {
continue;
}
$dh = @opendir($currentDir);
if (!$dh) continue;
while (($file = readdir($dh)) !== false) {
if ($options['skip_dots'] && ($file === '.' || $file === '..')) {
continue;
}
$fullPath = $currentDir . DIRECTORY_SEPARATOR . $file;
$isDir = is_dir($fullPath);
// 符號鏈接處理
if ($isDir && !$options['follow_links'] && is_link($fullPath)) {
continue;
}
if ($callback) {
$callback($fullPath, $isDir, $depth);
} else {
$results[] = $fullPath;
}
if ($isDir) {
$stack[] = [$fullPath, $depth + 1];
}
}
closedir($dh);
}
return $callback ? true : $results;
}
?>
$files = readFileFromDir('/home/www/test');
print_r($files);
readFileFromDir('/var/log', function($path, $isDir, $depth) {
$indent = str_repeat(' ', $depth);
$type = $isDir ? '[DIR]' : '[FILE]';
echo "{$indent}{$type} {$path}\n";
});
// 僅掃描前 2 層
$files = readFileFromDir('/data', null, ['max_depth' => 2]);
<?php
class DirectoryScanner {
private $options = [
'skip_dots' => true,
'follow_links' => false,
'max_depth' => -1,
];
private $results = [];
public function setOptions(array $options) {
$this->options = array_merge($this->options, $options);
return $this;
}
public function scan(string $dir, callable $callback = null): array {
$this->results = [];
$this->scanRecursive($dir, 0, $callback);
return $callback ? [] : $this->results;
}
private function scanRecursive(string $dir, int $depth, callable $callback = null) {
if ($this->options['max_depth'] >= 0 && $depth > $this->options['max_depth']) {
return;
}
$handle = @opendir($dir);
if (!$handle) return;
while (($file = readdir($handle)) !== false) {
if ($this->options['skip_dots'] && in_array($file, ['.', '..'])) continue;
$fullPath = $dir . DIRECTORY_SEPARATOR . $file;
$isDir = is_dir($fullPath);
if ($isDir && !$this->options['follow_links'] && is_link($fullPath)) continue;
if ($callback) {
$callback($fullPath, $isDir, $depth);
} else {
$this->results[] = [
'path' => $fullPath,
'name' => $file,
'is_dir' => $isDir,
'depth' => $depth,
'size' => $isDir ? 0 : filesize($fullPath),
'mtime' => filemtime($fullPath),
];
}
if ($isDir) {
$this->scanRecursive($fullPath, $depth + 1, $callback);
}
}
closedir($handle);
}
public function getResults(): array {
return $this->results;
}
}
// 使用
$scanner = new DirectoryScanner();
$scanner->setOptions(['max_depth' => 3]);
$files = $scanner->scan('/var/www');
foreach ($files as $f) {
echo "{$f['path']} ({$f['size']} bytes)\n";
}
| 方法 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
scandir() | 內置、簡單 | 單級、一次性載入所有、大目錄佔內存 | 小目錄快速列表 |
glob() | 支持模式匹配 | 性能較差、大目錄慢 | 特定模式匹配 |
RecursiveDirectoryIterator | SPL 標準、面向對象、支持過濾器 | 語法較繁瑣 | 現代項目標準選擇 |
| 自定義遞歸 | 完全可控、內存友好(回調模式) | 需自行處理邊界情況 | 特殊需求、舊版本兼容 |
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/path', RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
echo $file->getPathname() . "\n";
}
}
// 限制深度
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/path'),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($iterator->getDepth() > 3) continue;
// ...
}
推薦使用 SPL 迭代器,代碼更規範、功能更強、性能更優。