强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 编程