前言:為什麼要精通檔案系統操作?
變數生命週期隨請求結束而銷毀,持久化資料必須落盤。無論是日誌記錄、快取檔案、上傳處理、匯出報表、部署腳本,檔案與目錄操作都是 PHP 後端開發的核心基本功。本文系統整理屬性讀取、目錄遍歷、遞歸計算/刪除/複製、檔案讀寫指標控制等實戰技巧。
一、檔案屬性與元資訊獲取
1. 類型判斷
function getFileType(string $path): string
{
return match (filetype($path)) {
'file' => '普通檔案',
'dir' => '目錄',
'link' => '符號連結',
'block', 'char', 'fifo' => '特殊裝置檔',
default => '未知類型',
};
}
Windows 僅支援 file、dir、unknown 三種;Linux 可見完整 7 類型。
2. 常用屬性函數速查
| 函數 | 用途 | 回傳值 |
file_exists() | 檔案/目錄是否存在 | bool |
is_file() / is_dir() / is_link() | 類型判斷 | bool |
filesize() | 檔案大小(位元組) | int|false |
is_readable() / is_writable() / is_executable() | 權限檢查 | bool |
filectime() / filemtime() / fileatime() | 建立/修改/存取時間 | Unix timestamp |
fileowner() / filegroup() | 擁有者 UID / 群組 GID | int |
fileperms() | 權限位元遮罩 | int(需配合 sprintf('%o', ...) 看八進制) |
stat() | 一次性取得所有屬性 | 混合索引/關聯陣列 |
3. 人類可讀的檔案大小格式化
function formatSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$bytes = max($bytes, 0);
$pow = $bytes ? floor(log($bytes, 1024)) : 0;
$pow = min($pow, count($units) - 1);
$value = $bytes / (1 << (10 * $pow));
return round($value, 2) . ' ' . $units[$pow];
}
4. 綜合資訊輸出範例
function printFileInfo(string $path): void
{
if (!file_exists($path)) {
echo "路徑不存在: $path\n";
return;
}
$info = [
'類型' => getFileType($path),
'大小' => formatSize(filesize($path)),
'權限' => substr(sprintf('%o', fileperms($path)), -4),
'擁有者' => fileowner($path),
'群組' => filegroup($path),
'建立時間' => date('Y-m-d H:i:s', filectime($path)),
'修改時間' => date('Y-m-d H:i:s', filemtime($path)),
'最後存取' => date('Y-m-d H:i:s', fileatime($path)),
'可讀' => is_readable($path) ? '是' : '否',
'可寫' => is_writable($path) ? '是' : '否',
'可執行' => is_executable($path) ? '是' : '否',
];
foreach ($info as $k => $v) echo "$k: $v\n";
}
二、目錄操作:遍歷、計算大小、建立、刪除
1. 讀取目錄內容(非遞歸)
function listDirectory(string $dir): array
{
if (!is_dir($dir)) return [];
$files = [];
$handle = opendir($dir);
while (false !== ($name = readdir($handle))) {
if ($name === '.' || $name === '..') continue; // 關鍵:過濾當前/上層目錄
$full = $dir . DIRECTORY_SEPARATOR . $name;
$files[] = [
'name' => $name,
'path' => $full,
'type' => filetype($full),
'size' => is_file($full) ? filesize($full) : null,
'mtime' => filemtime($full),
];
}
closedir($handle);
return $files;
}
2. 遞歸計算目錄總大小
function getDirSize(string $dir): int
{
$size = 0;
$handle = opendir($dir);
while (false !== ($entry = readdir($handle))) {
if ($entry === '.' || $entry === '..') continue;
$full = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_file($full)) {
$size += filesize($full);
} elseif (is_dir($full)) {
$size += getDirSize($full); // 遞歸
}
}
closedir($handle);
return $size;
}
// 使用
echo "目錄大小: " . formatSize(getDirSize('/var/www/html'));
3. 建立多層目錄
// recursive=true 允許建立巢狀目錄,mode=0775 受 umask 影響
mkdir('/var/www/storage/logs/2024/05', 0775, true);
4. 遞歸刪除非空目錄(含檔案與子目錄)
function removeDirRecursive(string $dir): bool
{
if (!is_dir($dir)) return false;
$handle = opendir($dir);
while (false !== ($entry = readdir($handle))) {
if ($entry === '.' || $entry === '..') continue;
$full = $dir . DIRECTORY_SEPARATOR . $entry;
is_dir($full) ? removeDirRecursive($full) : unlink($full);
}
closedir($handle);
return rmdir($dir);
}
核心陷阱: 必須過濾 . 和 ..,否則遞歸會陷入無限循環(. 指向自身)。
5. 遞歸複製目錄
function copyDirRecursive(string $src, string $dst): bool
{
if (!is_dir($src)) return false;
if (!is_dir($dst)) mkdir($dst, 0775, true);
$handle = opendir($src);
while (false !== ($entry = readdir($handle))) {
if ($entry === '.' || $entry === '..') continue;
$srcPath = $src . DIRECTORY_SEPARATOR . $entry;
$dstPath = $dst . DIRECTORY_SEPARATOR . $entry;
if (is_dir($srcPath)) {
copyDirRecursive($srcPath, $dstPath);
} else {
copy($srcPath, $dstPath);
}
}
closedir($handle);
return true;
}
6. 遞歸移動/重命名(利用 rename 跨目錄即為移動)
// 移動整個目錄
rename('/tmp/old_project', '/var/www/new_project'); // 原子操作,極快
三、檔案讀寫:模式、指標控制、高效讀取
1. 開啟模式速查
| 模式 | 說明 | 指標位置 | 建檔 | 截斷 |
r | 唯讀 | 開頭 | 否 | 否 |
r+ | 讀寫 | 開頭 | 否 | 否 |
w | 唯寫 | 開頭 | 是 | 是 |
w+ | 讀寫 | 開頭 | 是 | 是 |
a | 追加寫入 | 結尾 | 是 | 否 |
a+ | 讀寫追加 | 結尾 | 是 | 否 |
x | 獨佔建立寫入 | 開頭 | 僅不存在時 | 否 |
c | 不截斷開啟寫入 | 開頭 | 是 | 否 |
2. 寫入檔案
// 單次寫入(自動開啟/關閉,支援 FLAG)
file_put_contents('log.txt', "新日誌\n", FILE_APPEND | LOCK_EX);
// 手動控制(大量寫入、需精確指標)
$fp = fopen('data.csv', 'w');
foreach ($rows as $row) {
fputcsv($fp, $row); // 自動處理 CSV 轉義
}
fclose($fp);
3. 讀取策略對比
| 函數 | 適用場景 | 記憶體 | 指標行為 |
fread($fp, $len) | 二進位、固定長度 | 可控 | 前進 $len 位元組 |
fgets($fp) | 逐行讀取文字 | 單行 | 前進至換行符 |
fgetc($fp) | 逐字元 | 單字元 | 前進 1 字元 |
file($path) | 小檔案全讀成陣列 | O(n) | 不適用 |
file_get_contents() | 小檔案全讀成字串 | O(n) | 不適用 |
readfile() | 直接輸出到瀏覽器 | 流式 | 不適用 |
4. 指標精細控制(fseek / rewind / ftell)
$fp = fopen('alphabet.txt', 'r'); // 內容: abcdefghijklmnopqrstuvwxyz
echo "起始位置: " . ftell($fp) . "\n"; // 0
fread($fp, 3); // 讀 'abc'
echo "讀 3 字元後: " . ftell($fp) . "\n"; // 3
fseek($fp, 5, SEEK_CUR); // 從目前位置向後 5 字元
echo "SEEK_CUR +5: " . ftell($fp) . "\n"; // 8
rewind($fp); // 回到開頭
echo "rewind: " . ftell($fp) . "\n"; // 0
fseek($fp, 0, SEEK_END); // 跳到結尾
echo "SEEK_END: " . ftell($fp) . "\n"; // 26 (檔案大小)
fclose($fp);
常數: SEEK_SET(預設,從開頭)、SEEK_CUR(從目前)、SEEK_END(從結尾)。
5. 判斷檔案結尾
while (!feof($fp)) {
$line = fgets($fp);
if ($line === false) break; // 實際讀取失敗
// 處理 $line
}
四、實戰最佳實踐清單
- 路徑統一使用
DIRECTORY_SEPARATOR 或 /(PHP 自動轉換),避免跨平台問題。
- 檔案操作前驗證:
is_readable()/is_writable(),失敗拋出具體異常。
- 嚴格關閉資源:
fclose()、closedir(),或使用 try/finally 確保釋放。
- 大檔案採用流式處理: 逐行/分塊讀寫,避免
file_get_contents() 撐爆記憶體。
- 併發寫入加鎖:
flock($fp, LOCK_EX) 獨佔鎖,LOCK_SH 共享鎖。
- 權限最小化原則: 目錄 0755、檔案 0644,敏感設定檔 0600。
- 臨時檔案用
sys_get_temp_dir() + tempnam(),用畢 unlink() 清理。
結語
檔案系統操作看似基礎,實則藏著跨平台相容性、效能陷阱、併發安全等深層課題。掌握 SplFileInfo、RecursiveDirectoryIterator 等 SPL 類別,能進一步簡化代碼、提升健壯性。建議將常用工具函數封裝為 Filesystem 服務類,統一錯誤處理與日誌記錄,讓業務代碼專注於核心邏輯。