PHP 完全指南 / 第 18 章 — 文件系统
第 18 章 — 文件系统:文件操作、目录遍历与流
18.1 读写文件
<?php
// 读取整个文件
$content = file_get_contents('/tmp/example.txt');
// 按行读取
$lines = file('/tmp/example.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// 写入文件
file_put_contents('/tmp/output.txt', "Hello, World!\n", FILE_APPEND);
// 带锁写入(防止并发写冲突)
file_put_contents('/tmp/output.txt', "Safe write\n", FILE_APPEND | LOCK_EX);
18.2 文件指针操作
<?php
// 打开文件
$handle = fopen('/tmp/data.txt', 'r');
// 逐行读取
while (($line = fgets($handle)) !== false) {
echo trim($line) . "\n";
}
// 写入
$handle = fopen('/tmp/output.txt', 'w');
fwrite($handle, "Line 1\n");
fwrite($handle, "Line 2\n");
fclose($handle);
// 文件指针操作
$handle = fopen('/tmp/data.txt', 'r');
fseek($handle, 10); // 移动到第 10 字节
$pos = ftell($handle); // 当前位置
$end = fseek($handle, 0, SEEK_END); // 移到末尾
rewind($handle); // 回到开头
fclose($handle);
18.3 文件信息
<?php
$path = '/tmp/example.txt';
file_exists($path); // 文件是否存在
is_file($path); // 是否为文件
is_dir('/tmp'); // 是否为目录
is_readable($path); // 是否可读
is_writable($path); // 是否可写
filesize($path); // 文件大小(字节)
filemtime($path); // 最后修改时间(时间戳)
filectime($path); // 创建时间
fileatime($path); // 最后访问时间
pathinfo($path); // 返回路径信息数组
pathinfo($path, PATHINFO_EXTENSION); // 文件扩展名
pathinfo($path, PATHINFO_FILENAME); // 文件名(不含扩展名)
pathinfo($path, PATHINFO_DIRNAME); // 目录名
// MIME 类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($path); // 'text/plain'
// 磁盘空间
disk_free_space('/'); // 可用空间
disk_total_space('/'); // 总空间
18.4 目录操作
<?php
// 创建目录
mkdir('/tmp/new-dir', 0755, true); // 递归创建
// 列出目录内容
$files = scandir('/tmp');
// ['.', '..', 'file1.txt', ...]
// 排除 . 和 ..
$files = array_diff(scandir('/tmp'), ['.', '..']);
// glob 模式匹配
$phpFiles = glob('/src/*.php');
$allImages = glob('/static/image/*.{jpg,png,gif}', GLOB_BRACE);
// 删除
rmdir('/tmp/empty-dir');
unlink('/tmp/file.txt');
// 重命名/移动
rename('/tmp/old.txt', '/tmp/new.txt');
rename('/tmp/file.txt', '/archive/file.txt'); // 移动
// 复制
copy('/tmp/source.txt', '/tmp/dest.txt');
// 递归删除目录
function deleteDir(string $dir): void
{
if (!is_dir($dir)) return;
foreach (scandir($dir) as $item) {
if ($item === '.' || $item === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $item;
is_dir($path) ? deleteDir($path) : unlink($path);
}
rmdir($dir);
}
18.5 SPL 文件迭代器
<?php
// DirectoryIterator — 遍历目录
$dir = new DirectoryIterator('/tmp');
foreach ($dir as $file) {
if ($file->isDot()) continue;
printf(
"%-30s %8s %s\n",
$file->getFilename(),
$file->isDir() ? '<DIR>' : formatSize($file->getSize()),
date('Y-m-d H:i', $file->getMTime())
);
}
// RecursiveDirectoryIterator — 递归遍历
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('/src'),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
echo $file->getPathname() . "\n";
}
}
// FilesystemIterator
$fs = new FilesystemIterator('/tmp', FilesystemIterator::SKIP_DOTS);
foreach ($fs as $file) {
echo $file->getFilename() . "\n";
}
SplFileObject
<?php
// 面向对象的文件操作
$file = new SplFileObject('/tmp/data.txt', 'r');
// 逐行读取
while (!$file->eof()) {
echo $file->fgets();
}
// 按行号读取
$file->seek(5); // 跳到第 6 行
echo $file->current();
// CSV 读取
$csv = new SplFileObject('/tmp/data.csv');
$csv->setFlags(SplFileObject::READ_CSV);
$csv->setCsvControl(',', '"', '\\');
foreach ($csv as $row) {
print_r($row); // 每行一个数组
}
// 写入
$writer = new SplFileObject('/tmp/output.txt', 'w');
$writer->fwrite("Line 1\n");
18.6 临时文件
<?php
// 创建临时文件
$tmpFile = tempnam(sys_get_temp_dir(), 'php_');
file_put_contents($tmpFile, 'temporary data');
// 读取后删除
$content = file_get_contents($tmpFile);
unlink($tmpFile);
// 临时目录
$tmpDir = sys_get_temp_dir() . '/myapp_' . bin2hex(random_bytes(8));
mkdir($tmpDir, 0700, true);
// 使用后清理
deleteDir($tmpDir);
18.7 文件锁
<?php
$handle = fopen('/tmp/shared.txt', 'r+');
// 排他锁(写锁)
if (flock($handle, LOCK_EX)) {
fwrite($handle, "Safe concurrent write\n");
flock($handle, LOCK_UN); // 释放锁
}
fclose($handle);
// 共享锁(读锁)
$handle = fopen('/tmp/shared.txt', 'r');
if (flock($handle, LOCK_SH)) {
$content = fread($handle, filesize('/tmp/shared.txt'));
flock($handle, LOCK_UN);
}
fclose($handle);
// 非阻塞锁
if (flock($handle, LOCK_EX | LOCK_NB)) {
// 立即获得锁
} else {
echo "文件已被锁定,请稍后重试";
}
18.8 文件上传
<?php
declare(strict_types=1);
class UploadHandler
{
private const MAX_SIZE = 10 * 1024 * 1024; // 10MB
private const ALLOWED_TYPES = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
];
public function handle(string $fieldName, string $uploadDir): array
{
if (!isset($_FILES[$fieldName])) {
throw new RuntimeException('No file uploaded');
}
$file = $_FILES[$fieldName];
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException($this->getErrorMessage($file['error']));
}
if ($file['size'] > self::MAX_SIZE) {
throw new RuntimeException('File too large');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, self::ALLOWED_TYPES, true)) {
throw new RuntimeException("Invalid file type: {$mimeType}");
}
$extension = match ($mimeType) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
default => 'bin',
};
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = rtrim($uploadDir, '/') . '/' . $filename;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Failed to move uploaded file');
}
return [
'filename' => $filename,
'path' => $destination,
'mime_type' => $mimeType,
'size' => $file['size'],
];
}
private function getErrorMessage(int $code): string
{
return match ($code) {
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file',
default => 'Unknown upload error',
};
}
}
18.9 业务场景:文件缓存
<?php
declare(strict_types=1);
class FileCache
{
public function __construct(
private readonly string $cacheDir,
private readonly int $defaultTtl = 3600,
) {
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
}
public function get(string $key): mixed
{
$path = $this->getPath($key);
if (!file_exists($path)) return null;
$data = unserialize(file_get_contents($path));
if ($data['expires_at'] < time()) {
unlink($path);
return null;
}
return $data['value'];
}
public function set(string $key, mixed $value, ?int $ttl = null): void
{
$data = [
'value' => $value,
'expires_at' => time() + ($ttl ?? $this->defaultTtl),
];
file_put_contents($this->getPath($key), serialize($data), LOCK_EX);
}
public function delete(string $key): void
{
$path = $this->getPath($key);
if (file_exists($path)) unlink($path);
}
public function clear(): void
{
foreach (glob($this->cacheDir . '/*.cache') as $file) {
unlink($file);
}
}
private function getPath(string $key): string
{
return $this->cacheDir . '/' . md5($key) . '.cache';
}
}
18.10 扩展阅读
上一章:第 17 章 — PDO 下一章:第 19 章 — HTTP 编程