PHP 文件读写

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

PHP 8.4

读写文件是最常见的持久化手段:日志、配置、导出数据、缓存。PHP 提供了从「一行读完」到「流式处理」的不同层次 API。

一次性读写

// 读整个文件
$content = file_get_contents("data.txt");

// 写入(覆盖)
file_put_contents("out.txt", "hello\n");

// 追加
file_put_contents("log.txt", "新的一行\n", FILE_APPEND);

file_put_contents 等价于 fopen + fwrite + fclose,简单场景用它就够了。它会返回写入的字节数,失败返回 false。

一次性读取会把整个文件读进内存,大文件(几百 MB)会撑爆内存限制,那种情况要用流式读取。

读取失败的处理

$content = @file_get_contents("missing.txt");   // @ 抑制警告,不推荐
if ($content === false) {
    // 处理失败
}

更清晰的写法是先检查:

$path = "data.txt";

if (!is_file($path) || !is_readable($path)) {
    throw new RuntimeException("文件不可读:{$path}");
}

$content = file_get_contents($path);

不要用 @ 抑制错误:它会把问题藏起来,让调试变困难。

按行读取

把文件读成数组,每行一个元素(包含行尾换行符):

$lines = file("data.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

foreach ($lines as $line) {
    echo $line . PHP_EOL;
}

遍历时清理空白:

$names = array_values(array_filter(array_map("trim", $lines)));

流式读取:大文件用这个

$handle = fopen("big.log", "r");

if ($handle === false) {
    throw new RuntimeException("无法打开文件");
}

while (($line = fgets($handle)) !== false) {
    // 每次只处理一行,内存占用恒定
    if (str_contains($line, "ERROR")) {
        echo $line;
    }
}

fclose($handle);

fgets 每次读一行;false 表示到达文件末尾。注意 fgets 会保留行尾的 \n,比较前先 trim。

写入与文件锁

多个进程同时写同一文件会互相覆盖,加锁解决:

file_put_contents("counter.txt", "1", LOCK_EX);

LOCK_EX 表示独占锁:写入期间其他进程会等待( flock 层面,跨进程有效)。

需要「读—改—写」完整原子操作时,用 fopen + flock:

$handle = fopen("counter.txt", "c+");   // 读写,不存在则创建,不截断
flock($handle, LOCK_EX);

$current = (int) stream_get_contents($handle);
$current++;

rewind($handle);
ftruncate($handle, 0);
fwrite($handle, (string) $current);

flock($handle, LOCK_UN);
fclose($handle);

目录操作

// 创建(递归创建多级目录)
if (!is_dir("storage/logs")) {
    mkdir("storage/logs", 0755, true);
}

// 遍历目录
foreach (scandir("storage") as $entry) {
    if ($entry === "." || $entry === "..") {
        continue;
    }
    echo $entry . PHP_EOL;
}

递归遍历用 SPL 迭代器,比手写递归清晰:

$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator("src", FilesystemIterator::SKIP_DOTS)
);

foreach ($iterator as $file) {
    if ($file->isFile() && $file->getExtension() === "php") {
        echo $file->getPathname() . PHP_EOL;
    }
}

FilesystemIterator::SKIP_DOTS 用于跳过 . 和 ..。

路径处理

__DIR__;                       // 当前文件所在目录的绝对路径
dirname(__FILE__);             // 同上(旧写法)
basename("/a/b/c.txt");        // "c.txt"
pathinfo("/a/b/c.txt");        // ["dirname" => "/a/b", "basename" => "c.txt", "extension" => "txt", "filename" => "c"]

拼接路径一律用 __DIR__ 作为起点,不要依赖相对路径或当前工作目录:

$logFile = __DIR__ . "/storage/app.log";

Web 环境(如 PHP-FPM)的当前工作目录与 CLI 不同,写相对路径会导致「命令行能跑、Web 里找不到文件」。

文件信息

$path = __DIR__ . "/data.txt";

file_exists($path);     // 文件或目录是否存在
is_file($path);         // 是普通文件
is_dir($path);          // 是目录
is_readable($path);     // 可读
is_writable($path);     // 可写
filesize($path);        // 字节数,失败返回 false
filemtime($path);       // 最后修改时间(Unix 时间戳)

删除与重命名:

unlink("old.txt");                    // 删除文件
rename("a.txt", "b.txt");             // 重命名或移动
copy("a.txt", "backup/a.txt");        // 复制

一个完整例子:简易日志

<?php
declare(strict_types=1);

function appLog(string $level, string $message, string $dir): void
{
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $file = $dir . "/" . date("Y-m-d") . ".log";
    $line = sprintf(
        "[%s] %s: %s%s",
        date("H:i:s"),
        strtoupper($level),
        $message,
        PHP_EOL
    );

    file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
}

appLog("error", "数据库连接失败", __DIR__ . "/storage/logs");

生成的 storage/logs/2026-11-02.log 内容:

[14:32:10] ERROR: 数据库连接失败

常见错误与注意事项

  1. 用相对路径:Web 与 CLI 的工作目录不同,一律用 __DIR__ 拼接。
  2. 大文件用 file_get_contents 一次读入:内存溢出,改用 fgets 流式读取。
  3. 多进程写同一文件不加锁:数据互相覆盖,用 LOCK_EX 或 flock。
  4. 用 @ 抑制警告代替检查:错误被隐藏,先 is_file / is_readable 再操作。
  5. 忘记 fclose:句柄泄漏,长时间运行的脚本会耗尽文件描述符。
  6. 目录权限过宽:0777 有安全风险,目录用 0755、文件用 0644 起步,按最小权限原则调整。

小结

  • 小文件用 file_get_contents / file_put_contents;大文件用 fopen + fgets 流式处理。
  • 追加写加 FILE_APPEND,并发写加 LOCK_EX。
  • 路径用 __DIR__ 拼接,不依赖当前工作目录。
  • 操作前检查 is_file / is_readable / is_writable,不用 @ 抑制错误。
  • 打开的句柄要 fclose。

下一步