首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 遞迴刪除目錄完整實作:處理非空目錄的標準方案

     編輯:PHP 相關知識

rmdir 限制與遞迴刪除:徹底清理目錄樹的正確姿勢

PHP 的 rmdir() 只能刪除空目錄。要刪除包含檔案、子目錄的非空目錄,必須實作遞迴演算法:先刪除所有檔案,再由最深層目錄向上刪除。

核心遞迴演算法

<?php
/**
 * 遞迴刪除目錄及其所有內容
 * 
 * @param string $dirPath 目錄路徑
 * @param bool $deleteSelf 是否刪除目錄本身(false 僅清空內容)
 * @return bool 成功與否
 */
function deleteDirectory(string $dirPath, bool $deleteSelf = true): bool {
    // 正規化路徑
    $dirPath = rtrim($dirPath, '/\\');
    
    if (!is_dir($dirPath)) {
        trigger_error("路徑不存在或非目錄:$dirPath", E_USER_WARNING);
        return false;
    }
    
    $items = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dirPath, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST // 關鍵:子優先(深度優先)
    );
    
    foreach ($items as $item) {
        $path = $item->getPathname();
        try {
            if ($item->isDir()) {
                if (!rmdir($path)) {
                    throw new RuntimeException("無法刪除目錄:$path");
                }
            } else {
                if (!unlink($path)) {
                    throw new RuntimeException("無法刪除檔案:$path");
                }
            }
        } catch (Throwable $e) {
            error_log($e->getMessage());
            return false;
        }
    }
    
    if ($deleteSelf) {
        return @rmdir($dirPath);
    }
    return true;
}
?>

使用範例

<?php
// 刪除整個目錄樹
if (deleteDirectory('/var/www/cache')) {
    echo "快取目錄已清除";
} else {
    echo "清除失敗,請查看錯誤日誌";
}

// 僅清空目錄內容,保留目錄本身
deleteDirectory('/var/www/temp', false);
?>

進階版:支援符號連結、錯誤收集、進度回呼

<?php
function deleteDirectoryAdvanced(string $dirPath, array $options = []): array {
    $options = array_merge([
        'delete_self' => true,
        'follow_symlinks' => false,     // 不跟隨符號連結
        'on_progress' => null,          // callable($current, $total, $path)
        'on_error' => null,             // callable($path, $error)
        'max_depth' => null,            // 最大深度限制
    ], $options);
    
    $results = [
        'deleted_files' => 0,
        'deleted_dirs' => 0,
        'errors' => [],
        'skipped' => []
    ];
    
    $dirPath = rtrim($dirPath, '/\\');
    if (!is_dir($dirPath)) {
        $results['errors'][] = "路徑不存在:$dirPath";
        return $results;
    }
    
    // 收集所有項目(深度優先)
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator(
            $dirPath, 
            RecursiveDirectoryIterator::SKIP_DOTS | 
            ($options['follow_symlinks'] ? 0 : RecursiveDirectoryIterator::FOLLOW_SYMLINKS)
        ),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    
    $allItems = iterator_to_array($iterator);
    $total = count($allItems);
    $current = 0;
    
    foreach ($allItems as $item) {
        $current++;
        $path = $item->getPathname();
        
        // 深度檢查
        if ($options['max_depth'] !== null && $item->getDepth() > $options['max_depth']) {
            $results['skipped'][] = $path;
            continue;
        }
        
        // 進度回呼
        if ($options['on_progress']) {
            ($options['on_progress'])($current, $total, $path);
        }
        
        try {
            if ($item->isLink() || ($item->isFile() && !$item->isDir())) {
                if (unlink($path)) {
                    $results['deleted_files']++;
                } else {
                    throw new RuntimeException("unlink 失敗");
                }
            } elseif ($item->isDir()) {
                if (rmdir($path)) {
                    $results['deleted_dirs']++;
                } else {
                    throw new RuntimeException("rmdir 失敗");
                }
            }
        } catch (Throwable $e) {
            $error = "刪除失敗 [$path]:" . $e->getMessage();
            $results['errors'][] = $error;
            if ($options['on_error']) {
                ($options['on_error'])($path, $e);
            }
        }
    }
    
    // 刪除根目錄
    if ($options['delete_self']) {
        if (@rmdir($dirPath)) {
            $results['deleted_dirs']++;
        } else {
            $results['errors'][] = "無法刪除根目錄:$dirPath";
        }
    }
    
    return $results;
}

// 使用範例
$results = deleteDirectoryAdvanced('/var/www/old_project', [
    'on_progress' => fn($c, $t, $p) => echo "[$c/$t] $p\n",
    'on_error' => fn($p, $e) => error_log("刪除錯誤:$p - " . $e->getMessage())
]);

echo "刪除檔案:{$results['deleted_files']}, 目錄:{$results['deleted_dirs']}";
if ($results['errors']) {
    echo "錯誤:" . implode('; ', $results['errors']);
}
?>

替代方案:系統指令(效能更好)

<?php
// 適用大量檔案、生產環境部署腳本
function deleteDirectorySystem(string $path): bool {
    $path = escapeshellarg(rtrim($path, '/\\'));
    
    // Linux/Unix
    if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
        $cmd = "rm -rf $path";
    } else {
        // Windows: rmdir /s /q
        $cmd = "rmdir /s /q $path";
    }
    
    exec($cmd, $output, $returnCode);
    return $returnCode === 0;
}
?>

常見陷阱與解決

問題 原因 解決
權限拒絕PHP 執行用戶無寫入/執行權限調整目錄擁有者/權限、或用 sudo 執行
目錄非空用 rmdir 直接刪非空目錄必須先清空內容(遞迴)
符號連結誤刪遞迴進入連結指向的目錄用 SKIP_DOTS + 不跟隨連結、或檢查 isLink()
掛載點刪除遞迴進入網路磁碟/掛載點檢查裝置號、限制 max_depth
記憶體耗盡iterator_to_array 載入百萬檔案直接迭代、或分批處理

最佳實務

  1. 優先使用 RecursiveIteratorIterator::CHILD_FIRST 確保深度優先
  2. 區分「檔案」與「目錄」刪除函數(unlink vs rmdir)
  3. 大量檔案(>10萬)考慮系統指令 rm -rf / rmdir /s
  4. 生產環境加入乾跑模式、備份機制、操作日誌
  5. 處理符號連結時明確決定:刪除連結本身、還是連結目標
  6. 錯誤處理要詳細記錄,便於事後追查

正確實作目錄遞迴刪除,是 PHP 開發者處理檔案系統操作的基本功,也是建立可靠部署、清理、快取管理工具的關鍵。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved