專案部署、快取清理、臨時檔管理常需「刪除目錄下所有內容」。單純 rmdir() 僅能刪空目錄,unlink() 僅能刪檔案。本文提供生產級 delDir() 函式,支援副檔名過濾、符號連結保護、錯誤處理與測試案例。
/**
* 遞迴刪除目錄內容(可選擇保留目錄本身)
* @param string $dir 目標目錄絕對路徑
* @param array|string|null $filter 副檔名過濾:'php'、['php','log']、null(全刪)
* @param bool $deleteSelf 是否刪除目錄本身(預設 true)
* @return array ['deleted' => int, 'errors' => array]
*/
function delDir($dir, $filter = null, $deleteSelf = true) {
$result = ['deleted' => 0, 'errors' => []];
if (!is_dir($dir)) {
$result['errors'][] = "Not a directory: $dir";
return $result;
}
$dir = rtrim($dir, '/\\');
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$path = "$dir/$item";
// 符號連結:只刪連結,不追蹤目標
if (is_link($path)) {
if (@unlink($path)) $result['deleted']++;
else $result['errors'][] = "Unlink symlink failed: $path";
continue;
}
if (is_dir($path)) {
// 遞迴處理子目錄
$sub = delDir($path, $filter, true);
$result['deleted'] += $sub['deleted'];
$result['errors'] = array_merge($result['errors'], $sub['errors']);
} elseif (is_file($path)) {
// 檔案過濾邏輯
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$match = false;
if ($filter === null) {
$match = true;
} elseif (is_array($filter)) {
$match = in_array($ext, array_map('strtolower', $filter));
} elseif (is_string($filter)) {
$match = ($ext === strtolower($filter));
}
if ($match) {
if (@unlink($path)) $result['deleted']++;
else $result['errors'][] = "Unlink file failed: $path";
}
}
}
// 刪除空目錄本身
if ($deleteSelf) {
if (@rmdir($dir)) $result['deleted']++;
else $result['errors'][] = "Rmdir failed: $dir";
}
return $result;
}
// 1. 刪除整個快取目錄
$r = delDir('/var/www/cache');
echo "Deleted {$r['deleted']} items. Errors: " . count($r['errors']);
// 2. 僅刪除 .tmp 和 .log 檔案,保留目錄結構
$r = delDir('/var/www/storage', ['tmp', 'log'], false);
// 3. 刪除單一檔案(傳入檔案路徑也支援)
$r = delDir('/tmp/test.php'); // 內部會判斷是檔案直接 unlink
| 風險 | 防護措施 |
|---|---|
| 誤刪專案核心目錄 | 呼叫前務必 realpath() 正規化,並與白名單比對 |
| 符號連結跳出預期目錄 | is_link() 僅刪連結檔,不遞迴進入目標 |
| 權限不足導致部分殘留 | 回傳 errors 陣列供記錄/告警,不靜默失敗 |
| 併發執行競態條件 | 關鍵目錄加 flock() 或使用獨佔鎖檔 |
| 超大目錄遞迴超時/記憶體溢出 | 改用 RecursiveDirectoryIterator + 生成器分批處理 |
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
function delDirModern($dir, $filter = null) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item) {
if ($filter && !$item->isDir()) {
$ext = strtolower($item->getExtension());
if (!in_array($ext, (array)$filter)) continue;
}
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
rmdir($dir);
}
舊版相容用遞迴函式,新專案建議使用 SPL 迭代器,程式碼更簡潔、記憶體效率更高、自動處理 CHILD_FIRST 順序。