在 PHP 中刪除非空目錄是常見需求。由於 rmdir() 僅能刪除空目錄,我們必須結合 opendir()、readdir() 遞迴遍歷並刪除所有檔案與子目錄。本文提供生產環境可用的完整實作,包含錯誤處理、符號連結防護與效能優化。
opendir())readdir()),略過 . 和 ..unlink() 刪除closedir())rmdir())function deldir(string $dir): bool {
// 確保路徑以目錄分隔符結尾
$dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (!is_dir($dir)) {
return false;
}
$dh = opendir($dir);
if ($dh === false) {
return false; // 無法開啟目錄(權限不足等)
}
while (($file = readdir($dh)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$fullPath = $dir . $file;
if (is_dir($fullPath)) {
// 遞迴刪除子目錄
if (!deldir($fullPath)) {
closedir($dh);
return false;
}
} else {
// 刪除檔案
if (!@unlink($fullPath)) {
closedir($dh);
return false;
}
}
}
closedir($dh);
return rmdir($dir);
}
增加符號連結檢測、例外處理、權限嘗試修復、進度回呼:
class DirectoryCleaner {
private int $deletedFiles = 0;
private int $deletedDirs = 0;
private array $errors = [];
public function delete(string $dir, ?callable $progressCallback = null): bool {
$this->resetCounters();
$result = $this->deleteRecursive($dir, $progressCallback);
return $result && empty($this->errors);
}
private function deleteRecursive(string $dir, ?callable $progressCallback): bool {
$dir = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (!is_dir($dir)) {
$this->errors[] = "路徑不是目錄或不存在:{$dir}";
return false;
}
// 檢查是否為符號連結,避免刪除連結目標
if (is_link($dir)) {
$this->errors[] = "跳過符號連結:{$dir}";
return false;
}
$dh = @opendir($dir);
if ($dh === false) {
$this->errors[] = "無法開啟目錄(權限不足):{$dir}";
return false;
}
while (($file = readdir($dh)) !== false) {
if ($file === '.' || $file === '..') continue;
$fullPath = $dir . $file;
// 處理符號連結
if (is_link($fullPath)) {
if (!@unlink($fullPath)) {
$this->errors[] = "無法刪除符號連結:{$fullPath}";
} else {
$this->deletedFiles++;
$progressCallback?.($fullPath, 'symlink');
}
continue;
}
if (is_dir($fullPath)) {
if (!$this->deleteRecursive($fullPath, $progressCallback)) {
closedir($dh);
return false;
}
$this->deletedDirs++;
$progressCallback?.($fullPath, 'dir');
} else {
// 嘗試修復唯讀檔案權限
if (!is_writable($fullPath)) {
@chmod($fullPath, 0644);
}
if (!@unlink($fullPath)) {
$this->errors[] = "無法刪除檔案:{$fullPath}";
closedir($dh);
return false;
}
$this->deletedFiles++;
$progressCallback?.($fullPath, 'file');
}
}
closedir($dh);
// 刪除當前目錄
if (!@rmdir($dir)) {
// 嘗試修復目錄權限
@chmod($dir, 0755);
if (!@rmdir($dir)) {
$this->errors[] = "無法刪除目錄:{$dir}";
return false;
}
}
$this->deletedDirs++;
$progressCallback?.($dir, 'dir');
return true;
}
public function getStats(): array {
return [
'deleted_files' => $this->deletedFiles,
'deleted_dirs' => $this->deletedDirs,
'errors' => $this->errors
];
}
private function resetCounters(): void {
$this->deletedFiles = 0;
$this->deletedDirs = 0;
$this->errors = [];
}
}
// 使用範例
$cleaner = new DirectoryCleaner();
$success = $cleaner->delete('/path/to/target', function($path, $type) {
echo "已刪除 {$type}:{$path}
";
});
if ($success) {
$stats = $cleaner->getStats();
echo "完成!刪除檔案:{$stats['deleted_files']},刪除目錄:{$stats['deleted_dirs']}";
} else {
echo "刪除過程發生錯誤:
";
foreach ($cleaner->getStats()['errors'] as $err) {
echo "- {$err}
";
}
}
RecursiveDirectoryIterator + RecursiveIteratorIterator 提供更優雅的遞迴遍歷:
function deleteDirectoryModern(string $dir): bool {
if (!is_dir($dir)) return false;
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先處理子項目,再處理父目錄
);
foreach ($files as $file) {
$path = $file->getPathname();
if ($file->isDir()) {
if (!@rmdir($path)) return false;
} else {
if (!@unlink($path)) return false;
}
}
return @rmdir($dir);
}
| 方法 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 原生遞迴函數 | 相容性最好(PHP 4+),記憶體效率高,可精細控制 | 程式碼較長 | 舊專案、嵌入式環境、需精細錯誤處理 |
| SPL 迭代器 | 程式碼簡潔、可讀性高、內建 CHILD_FIRST 排序 | 建立迭代器物件有額外開銷,大量檔案時記憶體佔用較高 | 現代 PHP 專案(5.3+)、中小型目錄 |
系統指令 exec('rm -rf') |
速度最快,支援所有檔案系統特性 | 安全風險極高(命令注入)、跨平台相容性差、需 shell 執行權限 | 極不推薦用於 Web 應用 |
realpath() 解析真實路徑,防止目錄遍歷攻擊(../../etc)PHP 刪除非空目錄的核心在於「深度優先遞迴刪除」。基礎版適合簡單場景,增強版提供生產級的錯誤處理與監控,SPL 版則是現代專案的首選。無論採用哪種方式,路徑驗證與權限控制都是安全的前提。