PHP 內建 rmdir() 僅能刪除空目錄。刪除非空目錄需先遞歸清空子文件/子目錄,再刪除目錄本身。本文提供一個健壯、可復用的遞歸刪除函數,處理了編碼兼容、符號鏈接、權限錯誤、循環引用等邊界情況,並給出生產環境使用建議。
<?php
/**
* 遞歸刪除目錄及其所有內容
* @param string $dir 目錄絕對路徑
* @param bool $deleteSelf 是否刪除目錄本身(false 時僅清空內容)
* @return bool 成功返回 true,失敗返回 false
*/
function deleteDirectory(string $dir, bool $deleteSelf = true): bool {
// 1. 規範化路徑,去除尾部斜槓
$dir = rtrim($dir, DIRECTORY_SEPARATOR);
// 2. 基礎校驗
if (!is_dir($dir)) {
trigger_error("目錄不存在: {$dir}", E_USER_WARNING);
return false;
}
if (!is_readable($dir) || !is_writable($dir)) {
trigger_error("目錄不可讀/寫,權限不足: {$dir}", E_USER_WARNING);
return false;
}
// 3. 打開目錄句柄
$handle = @opendir($dir);
if ($handle === false) {
trigger_error("無法打開目錄: {$dir}", E_USER_WARNING);
return false;
}
$success = true;
// 4. 遍歷目錄條目
while (($entry = readdir($handle)) !== false) {
// 跳過 . 和 ..
if ($entry === '.' || $entry === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $entry;
// 處理符號鏈接:僅刪除鏈接本身,不跟隨刪除目標
if (is_link($path)) {
if (!@unlink($path)) {
trigger_error("刪除符號鏈接失敗: {$path}", E_USER_WARNING);
$success = false;
}
continue;
}
if (is_dir($path)) {
// 遞歸刪除子目錄
if (!deleteDirectory($path, true)) {
$success = false;
}
} elseif (is_file($path)) {
// 刪除文件
if (!@unlink($path)) {
trigger_error("刪除文件失敗: {$path}", E_USER_WARNING);
$success = false;
}
}
// 忽略其他類型(socket、fifo 等)
}
closedir($handle);
// 5. 刪除目錄本身
if ($deleteSelf) {
if (!@rmdir($dir)) {
trigger_error("刪除目錄失敗(可能非空或權限不足): {$dir}", E_USER_WARNING);
$success = false;
}
}
return $success;
}
// === 使用示例 ===
// 設置編碼(舊項目兼容 GBK,新項目建議統一 UTF-8)
header('Content-Type: text/html; charset=utf-8');
$targetDir = __DIR__ . '/test_uploads'; // 使用絕對路徑
if (deleteDirectory($targetDir)) {
echo "目錄刪除成功: {$targetDir}";
} else {
echo "目錄刪除失敗,請檢查錯誤日誌";
}
| 細節 | 處理方式 | 原因 |
|---|---|---|
| 編碼兼容 | 文件系統編碼與腳本編碼一致(推薦 UTF-8),Windows 下中文路徑需注意 | 避免 opendir/unlink 因編碼不匹配找不到文件 |
| 符號鏈接 | is_link() 判斷,僅 unlink() 鏈接本身 | 防止誤刪鏈接指向的真實目標數據 |
| 權限錯誤 | 使用 @ 抑制警告 + trigger_error 記錄,不中斷流程 | 單個文件失敗不影響其他文件刪除,便於事後排查 |
| 循環引用 | 實際項目中可維護 $visited 集合記錄 inode | 防止符號鏈接形成環導致無限遞歸 |
| 大目錄性能 | 單次 readdir 遍歷,避免 scandir 全量載入內存 | 百萬文件目錄下 scandir 會導致內存溢出 |
| 併發安全 | 生產環境建議加文件鎖或使用獨佔目錄名(如臨時目錄重命名) | 防止多進程同時操作同一目錄導致競態條件 |
function deleteDirectoryAdvanced(string $dir, array $options = []): array {
$defaults = [
'deleteSelf' => true,
'followSymlinks' => false, // 是否跟隨符號鏈接刪除目標
'onFileDelete' => null, // callable(string $path): void
'onDirDelete' => null, // callable(string $path): void
'onError' => null, // callable(string $path, string $error): void
];
$opts = array_merge($defaults, $options);
$stats = ['files' => 0, 'dirs' => 0, 'errors' => 0, 'bytes' => 0];
$handler = function ($path) use (&$handler, &$stats, $opts) {
if (is_link($path)) {
if ($opts['followSymlinks'] && is_dir($path)) {
$handler($path); // 遞歸進入目標目錄
}
if (@unlink($path)) {
$stats['files']++;
$opts['onFileDelete']?.($path);
} else {
$stats['errors']++;
$opts['onError']?.($path, 'unlink symlink failed');
}
return;
}
if (is_dir($path)) {
$handle = @opendir($path);
if ($handle) {
while (($entry = readdir($handle)) !== false) {
if ($entry !== '.' && $entry !== '..') {
$handler($path . DIRECTORY_SEPARATOR . $entry);
}
}
closedir($handle);
}
if ($opts['deleteSelf'] || $path !== $dir) { // 根目錄由外層控制
if (@rmdir($path)) {
$stats['dirs']++;
$opts['onDirDelete']?.($path);
} else {
$stats['errors']++;
$opts['onError']?.($path, 'rmdir failed');
}
}
} elseif (is_file($path)) {
$stats['bytes'] += filesize($path);
if (@unlink($path)) {
$stats['files']++;
$opts['onFileDelete']?.($path);
} else {
$stats['errors']++;
$opts['onError']?.($path, 'unlink file failed');
}
}
};
$handler($dir);
// 最後刪除根目錄
if ($opts['deleteSelf'] && is_dir($dir)) {
if (@rmdir($dir)) {
$stats['dirs']++;
} else {
$stats['errors']++;
}
}
return $stats;
}
// 使用示例
$stats = deleteDirectoryAdvanced('/var/www/uploads/temp', [
'onError' => fn($p, $e) => error_log("DELETE ERROR: {$p} - {$e}"),
]);
echo "刪除統計: " . json_encode($stats);
deleted_at,定期異步清理。realpath() 對比),防止目錄遍歷攻擊(../../etc)。相關專題:PHP 遞歸操作、PHP 文件操作總結、PHP 目錄遍歷與文件處理。