專案常需掃描目錄結構:建立檔案索引、產生網站地圖、批次處理檔案等。本文提供功能完整的 `DirectoryScanner` 類別,支援資料夾列表、檔案列表、樹狀結構三種模式,含錯誤處理、效能優化與現代 PHP 寫法。
<?php
/**
* 目錄掃描器
* 支援: 資料夾列表、檔案列表、樹狀結構
*
* 使用範例:
* $scanner = new DirectoryScanner();
* $dirs = $scanner->getDirectories('/path/to/dir');
* $files = $scanner->getFiles('/path/to/dir');
* $tree = $scanner->getTree('/path/to/dir');
*/
class DirectoryScanner {
// 掃描選項常數
public const SKIP_DOTS = 1; // 跳過 . 和 ..
public const SKIP_HIDDEN = 2; // 跳過隱藏檔 (以 . 開頭)
public const FULL_PATH = 4; // 返回完整路徑
public const RECURSIVE = 8; // 遞迴掃描 (僅 getTree)
public const SORT_BY_NAME = 16; // 按名稱排序
public const SORT_BY_TIME = 32; // 按修改時間排序
public const SORT_DESC = 64; // 降序
public const ONLY_FILES = 128; // 僅檔案
public const ONLY_DIRS = 256; // 僅資料夾
private $defaultOptions;
public function __construct(int $options = 0) {
$this->defaultOptions = $options ?: (self::SKIP_DOTS | self::SORT_BY_NAME);
}
/**
* 取得資料夾列表 (單層)
*/
public function getDirectories(string $path, int $options = 0): array {
return $this->scan($path, $options | self::ONLY_DIRS);
}
/**
* 取得檔案列表 (單層)
*/
public function getFiles(string $path, int $options = 0): array {
return $this->scan($path, $options | self::ONLY_FILES);
}
/**
* 取得樹狀結構 (遞迴)
*/
public function getTree(string $path, int $options = 0): array {
$options |= self::RECURSIVE | self::FULL_PATH;
return $this->scan($path, $options);
}
/**
* 核心掃描邏輯
*/
private function scan(string $path, int $options): array {
$path = rtrim($path, DIRECTORY_SEPARATOR);
if (!is_dir($path)) {
throw new InvalidArgumentException("路徑不存在或非目錄: {$path}");
}
if (!is_readable($path)) {
throw new RuntimeException("目錄不可讀: {$path}");
}
$results = [];
$handle = @opendir($path);
if ($handle === false) {
throw new RuntimeException("無法開啟目錄: {$path}");
}
try {
while (($entry = readdir($handle)) !== false) {
// 過濾 . 和 ..
if (($options & self::SKIP_DOTS) && ($entry === '.' || $entry === '..')) {
continue;
}
// 過濾隱藏檔
if (($options & self::SKIP_HIDDEN) && $entry[0] === '.') {
continue;
}
$fullPath = $path . DIRECTORY_SEPARATOR . $entry;
$isDir = is_dir($fullPath);
// 類型過濾
if (($options & self::ONLY_FILES) && $isDir) continue;
if (($options & self::ONLY_DIRS) && !$isDir) continue;
$item = ($options & self::FULL_PATH) ? $fullPath : $entry;
// 遞迴掃描子目錄 (僅 getTree)
if ($isDir && ($options & self::RECURSIVE)) {
$item = [
'name' => $entry,
'path' => $fullPath,
'type' => 'directory',
'children' => $this->scan($fullPath, $options)
];
} elseif (!$isDir) {
// 檔案可選擇附加中繼資料
if ($options & self::FULL_PATH) {
$item = [
'name' => $entry,
'path' => $fullPath,
'type' => 'file',
'size' => filesize($fullPath),
'mtime' => filemtime($fullPath),
'ext' => pathinfo($entry, PATHINFO_EXTENSION)
];
}
}
$results[] = $item;
}
} finally {
closedir($handle);
}
// 排序
if ($options & (self::SORT_BY_NAME | self::SORT_BY_TIME)) {
$this->sortResults($results, $options);
}
return $results;
}
private function sortResults(array &$results, int $options): void {
$desc = ($options & self::SORT_DESC) !== 0;
$byTime = ($options & self::SORT_BY_TIME) !== 0;
usort($results, function($a, $b) use ($byTime, $desc, $options) {
// 處理樹狀結構 (陣列) vs 簡單字串
$nameA = is_array($a) ? $a['name'] : $a;
$nameB = is_array($b) ? $b['name'] : $b;
$timeA = is_array($a) && isset($a['mtime']) ? $a['mtime'] :
(is_array($a) ? filemtime($a['path']) : filemtime($a));
$timeB = is_array($b) && isset($b['mtime']) ? $b['mtime'] :
(is_array($b) ? filemtime($b['path']) : filemtime($b));
if ($byTime) {
$cmp = $timeA <=> $timeB;
} else {
$cmp = strnatcasecmp($nameA, $nameB); // 自然排序
}
return $desc ? -$cmp : $cmp;
});
}
// 靜態快捷方法
public static function dirs(string $path, int $options = 0): array {
return (new self())->getDirectories($path, $options);
}
public static function files(string $path, int $options = 0): array {
return (new self())->getFiles($path, $options);
}
public static function tree(string $path, int $options = 0): array {
return (new self())->getTree($path, $options);
}
}
?>
<?php
require_once 'DirectoryScanner.php';
// 1. 簡單資料夾列表
$dirs = DirectoryScanner::dirs('./example');
print_r($dirs);
/*
Array
(
[0] => example_one
[1] => example_two
)
*/
// 2. 簡單檔案列表
$files = DirectoryScanner::files('./example/example_one');
print_r($files);
/*
Array
(
[0] => example.sql
[1] => example.txt
)
*/
// 3. 完整樹狀結構
$tree = DirectoryScanner::tree('./example',
DirectoryScanner::FULL_PATH |
DirectoryScanner::SORT_BY_TIME |
DirectoryScanner::SORT_DESC
);
print_r($tree);
/*
Array
(
[0] => Array
(
[name] => example_one
[path] => ./example/example_one
[type] => directory
[children] => Array
(
[0] => Array
(
[name] => example.sql
[path] => ./example/example_one/example.sql
[type] => file
[size] => 1024
[mtime] => 1640000000
[ext] => sql
)
[1] => ...
)
)
[1] => ...
)
*/
// 4. 進階:僅掃描 PHP 檔案,遞迴,按時間倒序
$phpFiles = [];
$scanner = new DirectoryScanner(DirectoryScanner::RECURSIVE | DirectoryScanner::SORT_BY_TIME | DirectoryScanner::SORT_DESC);
$all = $scanner->getTree('./src');
function collectPhpFiles(array $tree, array &$result): void {
foreach ($tree as $node) {
if ($node['type'] === 'file' && $node['ext'] === 'php') {
$result[] = $node;
} elseif ($node['type'] === 'directory') {
collectPhpFiles($node['children'], $result);
}
}
}
collectPhpFiles($all, $phpFiles);
print_r($phpFiles);
?>
PHP 5.3+ 內建 RecursiveDirectoryIterator,無需自建類別:
<?php
// 簡單遞迴列表
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('./example', RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $path => $fileInfo) {
$prefix = $fileInfo->isDir() ? '[D] ' : '[F] ';
echo $prefix . $fileInfo->getPathname() . "\n";
}
// 過濾 PHP 檔案
$filter = new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('./src', RecursiveDirectoryIterator::SKIP_DOTS)
),
'/\.php$/i'
);
foreach ($filter as $file) {
echo $file->getPathname() . "\n";
}
// 取得檔案資訊陣列
$files = iterator_to_array(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('./example', RecursiveDirectoryIterator::SKIP_DOTS)
),
false
);
// 只取檔案 (非目錄)
$filesOnly = array_filter($files, fn($f) => $f->isFile());
?>
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 自建類別 (opendir/readdir) | 完全可控、輕量、無依賴、易擴充中繼資料 | 需自行處理遞迴、排序、錯誤 | 特定格式輸出、效能敏感、舊 PHP 版本 |
| SPL RecursiveDirectoryIterator | 標準庫、物件導向、支援過濾器鏈、功能豐富 | 物件開銷較大、深度遞迴可能較慢 | 現代專案、複雜過濾、標準化需求 |
| glob() | 簡單、支援模式匹配 | 非遞迴 (需 GLOB_BRACE)、記憶體載入所有結果 | 單層模式匹配、小目錄 |
| scandir() | 簡單、返回陣列 | 非遞迴、含 . 和 ..、無過濾 | 簡單單層列表 |
<?php
// 生成器版本:記憶體友善
function scanDirectoryGenerator(string $path, int $maxDepth = -1, int $currentDepth = 0): Generator {
if ($maxDepth >= 0 && $currentDepth > $maxDepth) return;
$handle = @opendir($path);
if (!$handle) return;
try {
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$fullPath = $path . DIRECTORY_SEPARATOR . $entry;
$isDir = is_dir($fullPath);
yield [
'name' => $entry,
'path' => $fullPath,
'type' => $isDir ? 'directory' : 'file',
'depth' => $currentDepth
];
if ($isDir) {
yield from scanDirectoryGenerator($fullPath, $maxDepth, $currentDepth + 1);
}
}
} finally {
closedir($handle);
}
}
// 使用:逐個處理,不佔用大量記憶體
foreach (scanDirectoryGenerator('./huge_directory', 3) as $item) {
// 處理單項
if ($item['type'] === 'file' && $item['depth'] <= 2) {
// ...
}
}
?>
根據專案規模、PHP 版本、功能需求選擇適當方案。現代專案優先考慮 SPL 迭代器或生成器模式,兼顧標準化與效能。