PHP SPL(Standard PHP Library)提供了一組強大的迭代器,能以物件導向、記憶體高效、可組合的方式處理目錄遍歷、檔案過濾、資料流處理等任務。本文以遞歸查找指定副檔名圖片為例,深度解析 RecursiveDirectoryIterator、RecursiveIteratorIterator、FilterIterator 的組合用法。
RecursiveDirectoryIterator (實現 RecursiveIterator)
│
▼
RecursiveIteratorIterator (實現 Iterator, OuterIterator)
│ ├─ 遍歷遞歸結構,展平為線性序列
│ └─ 模式:LEAVES_ONLY / SELF_FIRST / CHILD_FIRST
▼
FilterIterator (抽象類,實現 Iterator)
│ ├─ accept():決定是否保留當前元素
│ └─ 可鏈式組合多個過濾器
▼
你的具體過濾類 (extends FilterIterator)
<?php
class RecursiveFileFilterIterator extends FilterIterator {
/** @var string[] 允許的副檔名(小寫) */
protected array $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
public function __construct(string $path) {
// 1. 目錄遞歸迭代器
$dirIter = new RecursiveDirectoryIterator(
$path,
RecursiveDirectoryIterator::SKIP_DOTS // 忽略 . 和 ..
);
// 2. 遞歸迭代器展平器(只取葉子節點 = 檔案)
$recursiveIter = new RecursiveIteratorIterator(
$dirIter,
RecursiveIteratorIterator::LEAVES_ONLY
);
// 3. 父類 FilterIterator 接收外層迭代器
parent::__construct($recursiveIter);
}
/** 核心過濾邏輯:只保留檔案且副檔名符合 */
public function accept(): bool {
/** @var SplFileInfo $current */
$current = $this->getInnerIterator()->current();
if (!$current->isFile()) return false;
$ext = strtolower($current->getExtension());
return in_array($ext, $this->allowedExtensions, true);
}
/** 可選:自訂鍵名(預設為路徑字串) */
public function key(): string {
return $this->getInnerIterator()->key(); // 完整路徑
}
/** 可選:自訂當前值(預設為 SplFileInfo 物件) */
public function current(): SplFileInfo {
return $this->getInnerIterator()->current();
}
}
// 使用範例
$iterator = new RecursiveFileFilterIterator('/var/www/uploads');
foreach ($iterator as $fileInfo) {
// $fileInfo 是 SplFileInfo 物件,豐富 API:
echo "檔案: {$fileInfo->getPathname()}\n";
echo "大小: {$fileInfo->getSize()} bytes\n";
echo "修改時間: " . $fileInfo->getMTime() . "\n";
echo "副檔名: {$fileInfo->getExtension()}\n";
echo "MIME 類型: " . mime_content_type($fileInfo->getPathname()) . "\n";
echo "---\n";
}
?>
<?php
class CompositeFileFilterIterator extends FilterIterator {
public function __construct(Iterator $iterator, private array $filters) {
parent::__construct($iterator);
}
public function accept(): bool {
foreach ($this->filters as $filter) {
if (!$filter($this->current())) return false;
}
return true;
}
}
// 定義獨立過濾器(可複用、可測試)
$filters = [
fn(SplFileInfo $f) => $f->isFile(),
fn(SplFileInfo $f) => in_array(strtolower($f->getExtension()), ['jpg','png']),
fn(SplFileInfo $f) => $f->getSize() > 1024, // > 1KB
fn(SplFileInfo $f) => $f->getMTime() > strtotime('-7 days'), // 7 天內修改
];
$baseIter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/var/www/uploads', RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
$filtered = new CompositeFileFilterIterator($baseIter, $filters);
foreach ($filtered as $file) {
// 處理符合所有條件的檔案
}
?>
| 類別 | 用途 | 關鍵方法/常數 |
|---|---|---|
DirectoryIterator | 單層目錄遍歷 | isFile(), isDir(), getExtension() |
RecursiveDirectoryIterator | 遞歸目錄遍歷 | SKIP_DOTS, FOLLOW_SYMLINKS |
RecursiveIteratorIterator | 展平遞歸結構 | LEAVES_ONLY, SELF_FIRST, CHILD_FIRST, getDepth() |
FilterIterator | 過濾迭代器元素 | 抽象 accept() |
CallbackFilterIterator | 用閉包快速過濾 | new CallbackFilterIterator($it, fn($v) => ...) |
RegexIterator | 正則過濾路徑/檔名 | new RegexIterator($it, '/\.jpg$/i') |
LimitIterator | 分頁/切片 | new LimitIterator($it, $offset, $count) |
NoRewindIterator | 禁止重置(單向流) | 包裝不可重置的資源 |
AppendIterator | 合併多個迭代器 | $it->append($otherIter) |
foreach 實際迭代時才讀取檔案系統,不會一次性載入所有檔名到陣列。break 即可停止遍歷,釋放資源。| 問題 | 解決 |
|---|---|
| 符號連結導致無限遞歸 | 建構子傳入 RecursiveDirectoryIterator::FOLLOW_SYMLINKS 旗標(預設不跟隨),或自行在 accept() 中檢查 $current->isLink() |
| 權限不足拋出例外中斷遍歷 | 包裝 try-catch 在 accept() 內,或使用 CallbackFilterIterator 捕獲 |
| Windows 路徑分隔符不一致 | 使用 $fileInfo->getPathname() 取得系統原生路徑,或 DIRECTORY_SEPARATOR |
大量檔案 getMTime() 等呼叫慢 | SplFileInfo 會快取 stat 結果;若需極致效能可改用 FilesystemIterator::CURRENT_AS_FILEINFO 旗標 |
// Symfony Finder(更語義化、鏈式 API)
use Symfony\Component\Finder\Finder;
$files = Finder::create()
->files()
->in('/var/www/uploads')
->name('*.jpg')
->name('*.png')
->size('> 1K')
->date('since 7 days ago')
->depth('< 5');
foreach ($files as $file) {
// $file 是 SplFileInfo
}
// League Flysystem(抽象儲存層:本地、S3、FTP、SFTP 統一 API)
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
$adapter = new LocalFilesystemAdapter('/var/www/uploads');
$fs = new Filesystem($adapter);
$files = $fs->listContents('', true); // 遞歸列表,回傳陣列
SPL 迭代器是 PHP 處理檔案系統、資料流、集合操作的標準化、高效能、可組合基礎設施。掌握 RecursiveDirectoryIterator → RecursiveIteratorIterator → FilterIterator 這條組合鏈,即能以幾行代碼實現複雜的遞歸查找、過濾、轉換邏輯,且天然支援惰性求值與常數記憶體。建議將常用過濾器封裝為獨立類別/閉包,構建專案級的 FileSystemFinder 服務,複用於上傳掃描、日誌清理、靜態資源收集等場景。