檔案系統基礎
| 函數 | 用途 | 回傳值 |
| fopen() | 開啟檔案指標 | resource / false |
| fclose() | 關閉指標 | bool |
| feof() | 檢查結尾 | bool |
| fread() | 讀取指定位元組 | string / false |
| fwrite() | 寫入字串 | int / false |
| fgets() | 逐行讀取 | string / false |
| file_get_contents() | 一次讀全檔 | string / false |
| file_put_contents() | 一次寫全檔 | int / false |
| file() | 讀成陣列(每行一元素) | array / false |
目錄操作
// 掃描目錄 (推薦:SPL 迭代器)
$files = new FilesystemIterator('/var/www/uploads');
foreach ($files as $file) {
if ($file->isFile()) {
echo $file->getFilename() . ' (' . $file->getSize() . " bytes)\n";
}
}
// 遞迴掃描
$rdi = new RecursiveDirectoryIterator('/var/www');
$rii = new RecursiveIteratorIterator($rdi);
foreach ($rii as $file) {
if ($file->isFile()) echo $file->getPathname() . "\n";
}
// 建立/刪除目錄
mkdir('/path/to/dir', 0755, true); // 遞迴建立
rmdir('/path/to/empty_dir'); // 僅空目錄
// 遞迴刪除非空目錄
function rrmdir($dir) {
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
) as $file) {
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
}
rmdir($dir);
}
檔案讀取完整範例
function readFileSafe(string $path): ?string {
// 1. 存在性與可讀性檢查
if (!is_readable($path)) {
error_log("File not readable: $path");
return null;
}
// 2. 大小檢查 (防止記憶體耗盡)
$size = filesize($path);
if ($size > 10 * 1024 * 1024) { // 10MB 限制
error_log("File too large: $path ($size bytes)");
return null;
}
// 3. 讀取 (PHP 5.4+ file_get_contents 支援 stream context)
$content = @file_get_contents($path);
if ($content === false) {
error_log("Read failed: $path");
return null;
}
return $content;
}
// 串流讀取大檔案 (不載入記憶體)
function processLargeFile(string $path, callable $lineHandler): void {
$fp = fopen($path, 'r');
if (!$fp) throw new RuntimeException("Cannot open $path");
while (($line = fgets($fp)) !== false) {
$lineHandler(rtrim($line));
}
fclose($fp);
}
檔案寫入與鎖定 (併發安全)
function writeFileAtomic(string $path, string $data): bool {
$dir = dirname($path);
$tmp = $dir . '/.' . basename($path) . '.tmp.' . uniqid();
$fp = fopen($tmp, 'w');
if (!$fp) return false;
// 獨佔鎖
if (!flock($fp, LOCK_EX)) {
fclose($fp);
@unlink($tmp);
return false;
}
$written = fwrite($fp, $data);
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
if ($written === false) {
@unlink($tmp);
return false;
}
// 原子性重命名 (POSIX 保證)
return @rename($tmp, $path);
}
// 追加寫入 (日誌適用)
function appendFileLocked(string $path, string $data): bool {
$fp = fopen($path, 'a');
if (!$fp) return false;
if (!flock($fp, LOCK_EX)) { fclose($fp); return false; }
$ok = fwrite($fp, $data) !== false;
flock($fp, LOCK_UN);
fclose($fp);
return $ok;
}
檔案屬性與資訊
$stat = stat('/path/to/file');
/* 回傳陣列鍵值:
dev, ino, mode, nlink, uid, gid, rdev, size,
atime, mtime, ctime, blksize, blocks
*/
// 常用封裝函數
function fileInfo(string $path): array {
if (!file_exists($path)) return ['exists' => false];
$s = stat($path);
return [
'exists' => true,
'size' => $s['size'],
'size_human' => formatBytes($s['size']),
'mime' => mime_content_type($path),
'modified' => date('Y-m-d H:i:s', $s['mtime']),
'accessed' => date('Y-m-d H:i:s', $s['atime']),
'changed' => date('Y-m-d H:i:s', $s['ctime']),
'permissions' => substr(sprintf('%o', $s['mode']), -4),
'is_readable' => is_readable($path),
'is_writable' => is_writable($path),
'owner' => posix_getpwuid($s['uid'])['name'] ?? $s['uid'],
'group' => posix_getgrgid($s['gid'])['name'] ?? $s['gid'],
];
}
function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
while ($bytes >= 1024 && $i < 4) { $bytes /= 1024; $i++; }
return round($bytes, 2) . ' ' . $units[$i];
}
複製、移動、刪除
// 複製
copy('/src/file.txt', '/dst/file.txt') or die('Copy failed');
// 移動/重命名 (跨檔案系統 rename 會失敗,需 copy+unlink)
function moveFile(string $src, string $dst): bool {
if (@rename($src, $dst)) return true;
// 跨裝置 fallback
if (copy($src, $dst)) {
unlink($src);
return true;
}
return false;
}
// 刪除
unlink('/path/to/file') or die('Delete failed');
最佳實踐總結
- 永遠檢查回傳值:所有檔案函數失敗回傳
false
- 使用絕對路徑:避免工作目錄變更導致錯誤
- 大檔案用串流:
fopen + fgets 避免記憶體爆炸
- 併發寫入必鎖:
flock(LOCK_EX) 獨佔、LOCK_SH 共享
- 原子性寫入:暫存檔 +
rename() 保證一致性
- 權限最小化:目錄 0755、檔案 0644、敏感設定檔 0600
- 路徑注入防護:
basename()、realpath() 驗證在允許目錄內
- 錯誤記錄:
error_log() 記錄失敗詳情,勿直接 die()
PHP 8.1+ 現代化寫法
// 使用 FilesystemIterator + SplFileObject
$file = new SplFileObject('/path/to/file', 'r');
$file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
foreach ($file as $lineNum => $line) {
// $lineNum 從 0 開始
processLine($line);
}
// 類型安全
function readJson(string $path): array|object {
$content = file_get_contents($path);
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
return $data;
}