健壯實現
function destroyDir(string $dir, bool $virtual = false): bool {
$dir = $virtual ? realpath($dir) : $dir;
if ($dir === false || !is_dir($dir)) return false;
// 統一路徑分隔符,移除尾部斜線
$dir = rtrim($dir, DIRECTORY_SEPARATOR);
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先刪子項
);
foreach ($items as $item) {
$path = $item->getPathname();
if ($item->isLink() || $item->isFile()) {
@unlink($path);
} elseif ($item->isDir()) {
@rmdir($path);
}
}
return @rmdir($dir);
}
傳統遞歸函數(PHP 5.2 相容)
function destroyDir_legacy(string $dir): bool {
if (!is_dir($dir)) return false;
$dh = opendir($dir);
while (($file = readdir($dh)) !== false) {
if ($file === '.' || $file === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $file;
if (is_link($path) || is_file($path)) {
@unlink($path);
} elseif (is_dir($path)) {
destroyDir_legacy($path);
}
}
closedir($dh);
return @rmdir($dir);
}
關鍵安全處理
- 符號連結:
is_link() 先於 is_dir() 判斷,避免刪除連結目標內容。
- CHILD_FIRST:先刪除檔案/子目錄,再刪除父目錄,避免 "Directory not empty"。
- 錯誤抑制:
@unlink/@rmdir 配合返回值檢查,生產環境建議記錄失敗日誌而非忽略。
- 權限檢查:操作前可用
is_writable() 預判,或捕獲 ErrorException。
進階:刪除大目錄非阻塞
function async_destroy_dir(string $dir): void {
// 寫入任務佇列 (Redis/Database),Worker 進程處理
$task = ['dir' => $dir, 'created' => time()];
$redis->lpush('dir_delete_queue', json_encode($task));
}
// Worker 腳本
while (true) {
$task = $redis->rpop('dir_delete_queue');
if ($task) {
destroyDir(json_decode($task, true)['dir']);
} else {
sleep(1);
}
}
測試用例
// 建立測試結構
mkdir('test/a/b/c', 0755, true);
file_put_contents('test/a/file1.txt', '1');
file_put_contents('test/a/b/file2.txt', '2');
symlink('/tmp', 'test/a/link_to_tmp'); // 符號連結
// 執行刪除
var_dump(destroyDir('test')); // true
// 驗證
var_dump(is_dir('test')); // false
var_dump(is_dir('/tmp')); // true (連結目標未受影響)