文件系統操作是 PHP 開發中高頻需求。本文提供三個生產級工具函數:列出目錄下文件/子目錄、遞歸遍歷所有子目錄文件、遞歸刪除目錄及內容,並重點解決 Windows 系統下中文路徑編碼問題。
<?php
/**
* 獲取目錄下的文件列表與子目錄列表
*
* @param string $path 目錄路徑(UTF-8 編碼)
* @return array ['dirs' => [], 'files' => []]
* @throws InvalidArgumentException 路徑無效
*/
function listDirectory(string $path): array {
// 統一路徑分隔符
$path = rtrim($path, '/\\');
if (!is_dir($path)) {
throw new InvalidArgumentException("路徑不存在或不是目錄: {$path}");
}
$dirs = [];
$files = [];
// Windows 下需將 UTF-8 轉為 GBK/CP936 才能正確讀取中文路徑
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
$handle = @opendir($isWindows ? iconv('UTF-8', 'GBK//IGNORE', $path) : $path);
if ($handle === false) {
throw new RuntimeException("無法打開目錄: {$path}");
}
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
// Windows 下讀取到的文件名是 GBK,需轉回 UTF-8
$entryName = $isWindows ? iconv('GBK', 'UTF-8//IGNORE', $entry) : $entry;
$fullPath = $path . DIRECTORY_SEPARATOR . $entryName;
if (is_dir($fullPath)) {
$dirs[] = $entryName;
} elseif (is_file($fullPath)) {
$files[] = $entryName;
}
}
closedir($handle);
// 自然排序,數字順序正確(如 1.jpg, 2.jpg, 10.jpg)
natsort($dirs);
natsort($files);
return ['dirs' => array_values($dirs), 'files' => array_values($files)];
}
// 使用示例
$result = listDirectory('/var/www/uploads');
print_r($result);
/*
Array
(
[dirs] => Array ( [0] => images [1] => documents )
[files] => Array ( [0] => index.php [1] => readme.md )
)
*/
?>
<?php
/**
* 遞歸獲取目錄下所有文件的相對路徑
*
* @param string $path 起始目錄
* @param string $basePath 基準路徑(用於計算相對路徑),默認同 $path
* @return string[] 文件相對路徑數組
*/
function scanDirectoryRecursive(string $path, string $basePath = ''): array {
$basePath = $basePath ?: $path;
$path = rtrim($path, '/\\');
$basePath = rtrim($basePath, '/\\');
if (!is_dir($path)) {
throw new InvalidArgumentException("目錄不存在: {$path}");
}
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
$handle = @opendir($isWindows ? iconv('UTF-8', 'GBK//IGNORE', $path) : $path);
if (!$handle) throw new RuntimeException("無法打開目錄: {$path}");
$files = [];
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$entryName = $isWindows ? iconv('GBK', 'UTF-8//IGNORE', $entry) : $entry;
$fullPath = $path . DIRECTORY_SEPARATOR . $entryName;
$relativePath = substr($fullPath, strlen($basePath) + 1); // +1 跳過分隔符
if (is_dir($fullPath)) {
$files = array_merge($files, scanDirectoryRecursive($fullPath, $basePath));
} elseif (is_file($fullPath)) {
$files[] = $relativePath;
}
}
closedir($handle);
return $files;
}
// 使用示例
$allFiles = scanDirectoryRecursive('/var/www/project/storage');
/*
Array
(
[0] => app/config.php
[1] => logs/2024-01-15.log
[2] => uploads/用戶頭像/張三.png
...
)
*/
?>
<?php
/**
* 遞歸刪除目錄(包含子目錄和文件)
*
* @param string $path 目錄路徑
* @return bool 刪除成功返回 true
* @throws RuntimeException 刪除失敗
*/
function deleteDirectory(string $path): bool {
$path = rtrim($path, '/\\');
if (!is_dir($path)) {
throw new InvalidArgumentException("不是有效目錄: {$path}");
}
$isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
$handle = @opendir($isWindows ? iconv('UTF-8', 'GBK//IGNORE', $path) : $path);
if (!$handle) throw new RuntimeException("無法打開目錄進行刪除: {$path}");
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$entryName = $isWindows ? iconv('GBK', 'UTF-8//IGNORE', $entry) : $entry;
$fullPath = $path . DIRECTORY_SEPARATOR . $entryName;
if (is_dir($fullPath)) {
// 遞歸刪除子目錄
if (!deleteDirectory($fullPath)) {
closedir($handle);
return false;
}
} elseif (is_file($fullPath) || is_link($fullPath)) {
// 刪除文件/符號鏈接
if (!@unlink($fullPath)) {
closedir($handle);
throw new RuntimeException("文件刪除失敗: {$fullPath}");
}
}
}
closedir($handle);
// 最後刪除空目錄本身
if (!@rmdir($path)) {
throw new RuntimeException("目錄刪除失敗(可能非空或權限不足): {$path}");
}
return true;
}
// 使用示例
try {
deleteDirectory('/tmp/cache/expired');
echo "緩存目錄清理完成";
} catch (Exception $e) {
error_log("清理失敗: " . $e->getMessage());
}
?>
| 場景 | 問題原因 | 解決方案 |
|---|---|---|
| Windows + PHP 讀取中文目錄 | 文件系統 API 期望 GBK/CP936 編碼,PHP 內部用 UTF-8 | iconv('UTF-8', 'GBK//IGNORE', $path) 轉換後再操作 |
| Windows + 讀取到的文件名顯示亂碼 | readdir() 返回 GBK 編碼字符串 | iconv('GBK', 'UTF-8//IGNORE', $entry) 轉回 UTF-8 |
| Linux/Mac | 文件系統原生支持 UTF-8 | 無需轉換,直接操作 |
| PHP 7.2+ / 8.0+ | 內部已改進 Unicode 支持 | 仍建議顯式處理,保證跨平台兼容 |
PHP 5.3+ 提供了更面向對象的 RecursiveDirectoryIterator,自動處理編碼(需配合 FilesystemIterator::CURRENT_AS_PATHNAME),代碼更簡潔:
<?php
// 遞歸獲取所有文件(PHP 5.3+)
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/path/to/dir', FilesystemIterator::SKIP_DOTS)
);
$files = [];
foreach ($iterator as $fileInfo) {
if ($fileInfo->isFile()) {
$files[] = $fileInfo->getPathname();
}
}
// 刪除目錄(需自寫遞歸,SPL 無直接刪除目錄方法)
?>
/ 或 \。is_writable() 預檢,避免半途失敗留下髒數據。opendir/readdir 佔用內存大,考慮分批處理或使用 glob() 分頁。is_link() 判斷,避免誤刪鏈接目標的真實文件。以上三個函數經過多個項目驗證,可直接納入項目公共庫。對於現代框架(Laravel、Symfony),建議優先使用框架封裝的 Storage/Filesystem 組件,底層已處理好跨平台兼容性。