專案部署、備份、快取清理等場景常需「整個目錄樹」複製。PHP 無內建單一函數完成此任務,需自行實現遞歸遍歷。本文提供健壯的遞歸複製函數,並解析常見陷阱(權限、符號連結、路徑分隔符)。
<?php
/**
* 遞歸複製目錄
* @param string $src 來源目錄絕對路徑
* @param string $dst 目標目錄絕對路徑
* @param bool $preservePermissions 是否保留權限(預設 true)
* @return bool 成功與否
*/
function recursiveCopy(string $src, string $dst, bool $preservePermissions = true): bool
{
// 1. 規範化路徑:移除尾部斜線,統一用 DIRECTORY_SEPARATOR
$src = rtrim($src, DIRECTORY_SEPARATOR);
$dst = rtrim($dst, DIRECTORY_SEPARATOR);
// 2. 來源必須存在且為目錄
if (!is_dir($src)) {
trigger_error("來源非目錄或不存在: $src", E_USER_WARNING);
return false;
}
// 3. 建立目標目錄(遞歸建立,權限 0755)
if (!is_dir($dst)) {
if (!mkdir($dst, 0755, true) && !is_dir($dst)) {
trigger_error("無法建立目標目錄: $dst", E_USER_ERROR);
return false;
}
}
// 4. 開啟來源目錄控制代碼
$dirHandle = opendir($src);
if (!$dirHandle) {
trigger_error("無法開啟來源目錄: $src", E_USER_WARNING);
return false;
}
$success = true;
while (($entry = readdir($dirHandle)) !== false) {
// 跳過 . 和 ..
if ($entry === '.' || $entry === '..') continue;
$srcPath = $src . DIRECTORY_SEPARATOR . $entry;
$dstPath = $dst . DIRECTORY_SEPARATOR . $entry;
if (is_dir($srcPath)) {
// 遞歸處理子目錄
if (!recursiveCopy($srcPath, $dstPath, $preservePermissions)) {
$success = false;
break; // 可選:遇錯立即停止
}
} elseif (is_file($srcPath) || is_link($srcPath)) {
// 複製檔案或符號連結
if (!@copy($srcPath, $dstPath)) {
trigger_error("複製失敗: $srcPath -> $dstPath", E_USER_WARNING);
$success = false;
break;
}
// 可選:保留檔案修改時間
if ($preservePermissions) {
@touch($dstPath, filemtime($srcPath));
@chmod($dstPath, fileperms($srcPath));
}
}
// 忽略 socket、FIFO 等特殊檔案
}
closedir($dirHandle);
return $success;
}
// 使用範例
if (recursiveCopy('/var/www/project', '/backup/project_20240520')) {
echo "目錄樹複製完成";
} else {
echo "複製過程中發生錯誤";
}
?>
DIRECTORY_SEPARATOR 常數,確保 Windows/Linux 相容。copy() 不保留權限與時間,需額外 chmod()/touch()。is_link() 偵測連結,copy() 會複製連結指向的內容而非連結本身。若需保留連結結構,需用 readlink() + symlink() 處理。@,改用 try-catch 或檢查 error_get_last() 記錄詳細錯誤。RecursiveDirectoryIterator) 或系統指令 (cp -r/robocopy)。<?php
function recursiveCopySpl(string $src, string $dst): bool
{
if (!is_dir($src)) return false;
if (!is_dir($dst) && !mkdir($dst, 0755, true)) return false;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$target = $dst . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
if ($item->isDir()) {
if (!is_dir($target) && !mkdir($target, 0755, true)) return false;
} else {
if (!@copy($item->getPathname(), $target)) return false;
}
}
return true;
}
?>
SPL 版本碼更簡潔、自動處理遞歸順序(SELF_FIRST 先建目錄再複製檔案),是現代 PHP 專案的推薦寫法。