PHP 內建 rmdir() 僅能刪除**空目錄**,刪除含檔案/子目錄的資料夾需自行遞迴實作。本文提供健壯、可重用的刪除函式,含錯誤處理、符號連結保護、權限檢查。
<?php
/**
* 遞迴刪除目錄及其所有內容
*
* @param string $dirPath 目錄絕對路徑
* @param bool $deleteRoot 是否刪除根目錄本身 (false 則僅清空內容)
* @return bool 成功與否
* @throws InvalidArgumentException 路徑無效
* @throws RuntimeException 刪除失敗
*/
function deleteDirectory(string $dirPath, bool $deleteRoot = true): bool {
// 1. 正規化路徑,移除尾端斜線
$dirPath = rtrim($dirPath, DIRECTORY_SEPARATOR);
// 2. 基本驗證
if (!file_exists($dirPath)) {
throw new InvalidArgumentException("目錄不存在: $dirPath");
}
if (!is_dir($dirPath)) {
throw new InvalidArgumentException("路徑非目錄: $dirPath");
}
if (!is_readable($dirPath)) {
throw new RuntimeException("目錄不可讀: $dirPath");
}
// 3. 取得目錄控制代碼
$handle = opendir($dirPath);
if ($handle === false) {
throw new RuntimeException("無法開啟目錄: $dirPath");
}
// 4. 遞迴處理每個項目
while (($entry = readdir($handle)) !== false) {
// 跳過 . 和 ..
if ($entry === '.' || $entry === '..') {
continue;
}
$fullPath = $dirPath . DIRECTORY_SEPARATOR . $entry;
// 5. 處理符號連結:只刪除連結本身,不跟隨刪除目標
if (is_link($fullPath)) {
if (!unlink($fullPath)) {
closedir($handle);
throw new RuntimeException("無法刪除符號連結: $fullPath");
}
continue;
}
// 6. 遞迴刪除子目錄
if (is_dir($fullPath)) {
if (!deleteDirectory($fullPath, true)) {
closedir($handle);
throw new RuntimeException("子目錄刪除失敗: $fullPath");
}
continue;
}
// 7. 刪除檔案 (處理唯讀屬性)
if (is_file($fullPath)) {
// Windows 唯讀檔案需先移除屬性
if (!is_writable($fullPath)) {
@chmod($fullPath, 0666);
}
if (!unlink($fullPath)) {
closedir($handle);
throw new RuntimeException("無法刪除檔案: $fullPath");
}
}
}
closedir($handle);
// 8. 刪除根目錄本身
if ($deleteRoot) {
if (!rmdir($dirPath)) {
throw new RuntimeException("無法刪除目錄: $dirPath (可能非空或權限不足)");
}
}
return true;
}
<?php
try {
// 刪除 /var/www/uploads/temp 及其所有內容
deleteDirectory('/var/www/uploads/temp');
echo "刪除成功";
} catch (Exception $e) {
error_log("刪除失敗: " . $e->getMessage());
echo "操作失敗: " . $e->getMessage();
}
/**
* 進階刪除:支援保留特定檔案/目錄
*
* @param string $dirPath
* @param array $excludePatterns 要排除的 glob 模式 (相對於 $dirPath)
* @param bool $deleteRoot
* @return int 刪除的檔案/目錄總數
*/
function deleteDirectoryAdvanced(string $dirPath, array $excludePatterns = [], bool $deleteRoot = true): int {
$dirPath = rtrim($dirPath, DIRECTORY_SEPARATOR);
$deletedCount = 0;
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dirPath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST // 先處理子項目,再處理父目錄
);
foreach ($items as $item) {
$path = $item->getPathname();
$relative = substr($path, strlen($dirPath) + 1);
// 檢查排除規則
$excluded = false;
foreach ($excludePatterns as $pattern) {
if (fnmatch($pattern, $relative) || fnmatch($pattern, $item->getFilename())) {
$excluded = true;
break;
}
}
if ($excluded) {
continue;
}
if ($item->isLink() || $item->isFile()) {
if (@unlink($path)) {
$deletedCount++;
}
} elseif ($item->isDir()) {
if (@rmdir($path)) {
$deletedCount++;
}
}
}
if ($deleteRoot && @rmdir($dirPath)) {
$deletedCount++;
}
return $deletedCount;
}
// 使用:刪除 temp 但保留 .gitkeep 和 logs 目錄
deleteDirectoryAdvanced('/var/www/temp', ['.gitkeep', 'logs*']);
exec('rm -rf ...') (僅限受信任路徑、Linux 環境)。is_link() 檢測並只刪除連結本身,避免誤刪系統關鍵目錄。realpath() 正規化,並限制在允許根目錄下。<?php
// 僅限受信任、已驗證的絕對路徑
$path = escapeshellarg('/var/www/uploads/temp');
exec("rm -rf $path", $output, $returnCode);
if ($returnCode !== 0) {
throw new RuntimeException("系統指令刪除失敗");
}
警告:exec() 有注入風險,**嚴禁**直接使用使用者輸入,必須經 escapeshellarg() 並驗證路徑在允許範圍內。
遞迴刪除目錄是檔案管理常見需求,掌握 RecursiveDirectoryIterator 或手寫遞迴函式,並注意符號連結、權限、路徑安全,即能建立可靠的清理工具。生產環境建議封裝為 Service 類別,搭配日誌記錄與例外處理,便於追蹤與維運。