Skip to content

便捷函数

概述

PHP 提供了一系列便捷的文件操作函数,可以用一行代码完成常见的文件读写任务。file_get_contents()file_put_contents() 是其中最常用的两个函数。对于简单的文件操作,便捷函数比 fopen 系列更简洁高效。

适用场景

  • 快速读取配置文件
  • 一次性写入小文件
  • 读取远程 URL 内容
  • INI 配置解析

基础概念

核心便捷函数

函数功能返回值
file_get_contents()读取整个文件为字符串string|false
file_put_contents()将字符串写入文件int|false
file()读取文件为数组(每行一个元素)array|false
readfile()读取文件并直接输出int|false
parse_ini_file()解析 INI 配置文件array|false
parse_ini_string()解析 INI 格式字符串array|false

性能说明

file_get_contents() 使用内存映射(mmap)技术,在读取大文件时比 fread() 更高效。但处理超大文件(>内存)时应使用流式读取。

file_put_contents 标志

标志说明
FILE_USE_INCLUDE_PATH在 include_path 中搜索文件
FILE_APPEND追加写入(等同于 a 模式)
LOCK_EX获取独占锁再写入

PHP 8.0+ 变更

file_get_contents()file_put_contents() 在 PHP 8.0 中已正确声明返回类型。file()$flags 参数类型更严格。

语法与代码示例

file_get_contents 读取文件

php
<?php

// 基本读取
$content = file_get_contents('/tmp/config.json');

if ($content === false) {
    throw new RuntimeException('文件读取失败');
}

echo $content;

// 指定偏移和长度读取
$partialContent = file_get_contents('/tmp/data.txt', false, null, 0, 1024);
echo $partialContent; // 只读取前 1024 字节

// 从偏移位置开始读取
$fromOffset = file_get_contents('/tmp/data.txt', false, null, 100, 500);
echo $fromOffset; // 从第 100 字节开始读取 500 字节

file_put_contents 写入文件

php
<?php

// 基本写入(覆盖模式)
$bytes = file_put_contents('/tmp/output.txt', 'Hello, World!');
echo "写入 {$bytes} 字节\n";

// 追加写入
file_put_contents('/tmp/log.txt', "[INFO] 新日志\n", FILE_APPEND);

// 带锁写入(防止并发冲突)
file_put_contents('/tmp/counter.txt', '100', LOCK_EX);

// 写入数组(自动连接为字符串)
$lines = ['Line 1', 'Line 2', 'Line 3'];
file_put_contents('/tmp/lines.txt', implode(PHP_EOL, $lines));

file() 读取文件为数组

php
<?php

// 读取为行数组(包含换行符)
$lines = file('/tmp/data.txt');

if ($lines === false) {
    throw new RuntimeException('读取失败');
}

foreach ($lines as $lineNumber => $line) {
    echo sprintf("第 %d 行: %s", $lineNumber + 1, $line);
}

// 去除换行符
$lines = file('/tmp/data.txt', FILE_IGNORE_NEW_LINES);

// 跳过空行
$lines = file('/tmp/data.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// 实用技巧:快速读取文件为行数组并去空白
$lines = array_filter(
    array_map('trim', file('/tmp/data.txt', FILE_IGNORE_NEW_LINES))
);

readfile 直接输出到缓冲区

php
<?php

// readfile 直接输出到浏览器,返回字节数
$bytes = readfile('/tmp/image.jpg');

// 常用于文件下载
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="document.pdf"');
header('Content-Length: ' . filesize('/tmp/document.pdf'));
readfile('/tmp/document.pdf');
exit;

parse_ini_file 解析配置

php
<?php

// config.ini 内容示例:
// [database]
// host = localhost
// port = 3306
// name = myapp
//
// [cache]
// driver = redis
// ttl = 3600

// 基本解析(扁平数组)
$config = parse_ini_file('/tmp/config.ini');
print_r($config);

// 分段解析(按节分组为二维数组)
$config = parse_ini_file('/tmp/config.ini', true);
print_r($config);
/*
输出:
[
    'database' => ['host' => 'localhost', 'port' => '3306', 'name' => 'myapp'],
    'cache'    => ['driver' => 'redis', 'ttl' => '3600'],
]
*/

// PHP 8.0+ 指定 scanner_mode 自动推断类型
$config = parse_ini_file(
    '/tmp/config.ini',
    true,
    INI_SCANNER_TYPED // 数字、布尔值、null 会被自动转换
);
// port 会从字符串 "3306" 变为整数 3306

实战示例

配置管理类

php
<?php

declare(strict_types=1);

class ConfigManager
{
    private array $config = [];
    private string $configFile;

    public function __construct(string $configFile)
    {
        $this->configFile = $configFile;
        $this->load();
    }

    private function load(): void
    {
        if (!file_exists($this->configFile)) {
            throw new RuntimeException("配置文件不存在: {$this->configFile}");
        }

        $config = parse_ini_file($this->configFile, true, INI_SCANNER_TYPED);

        if ($config === false) {
            throw new RuntimeException("配置文件解析失败: {$this->configFile}");
        }

        $this->config = $config;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        $keys = explode('.', $key);
        $value = $this->config;

        foreach ($keys as $segment) {
            if (!is_array($value) || !array_key_exists($segment, $value)) {
                return $default;
            }
            $value = $value[$segment];
        }

        return $value;
    }

    public function set(string $key, mixed $value): void
    {
        $keys = explode('.', $key);
        $config = &$this->config;

        foreach ($keys as $i => $segment) {
            if ($i === count($keys) - 1) {
                $config[$segment] = $value;
            } else {
                if (!isset($config[$segment]) || !is_array($config[$segment])) {
                    $config[$segment] = [];
                }
                $config = &$config[$segment];
            }
        }
    }

    public function save(): void
    {
        $content = $this->toIniString($this->config);
        file_put_contents($this->configFile, $content);
    }

    private function toIniString(array $config, string $prefix = ''): string
    {
        $result = '';
        foreach ($config as $key => $value) {
            if (is_array($value) && !is_int(key($value))) {
                $section = $prefix ? "{$prefix}.{$key}" : $key;
                $result .= PHP_EOL . "[{$section}]" . PHP_EOL;
                $result .= $this->toIniString($value, $section);
            } else {
                if (is_bool($value)) {
                    $value = $value ? 'true' : 'false';
                } elseif ($value === null) {
                    $value = 'null';
                } elseif (!is_scalar($value)) {
                    $value = json_encode($value, JSON_UNESCAPED_UNICODE);
                }
                $result .= "{$key} = {$value}" . PHP_EOL;
            }
        }
        return $result;
    }
}

// 使用示例
$config = new ConfigManager('/tmp/app.ini');
$host = $config->get('database.host', 'localhost');
$config->set('cache.ttl', 7200);
$config->save();

JSON 文件工具类

php
<?php

declare(strict_types=1);

class FileHelper
{
    public static function read(string $path): string
    {
        $content = file_get_contents($path);
        if ($content === false) {
            throw new RuntimeException("读取失败: {$path}");
        }
        return $content;
    }

    public static function write(string $path, string $content, int $flags = 0): int
    {
        $bytes = file_put_contents($path, $content, $flags);
        if ($bytes === false) {
            throw new RuntimeException("写入失败: {$path}");
        }
        return $bytes;
    }

    public static function append(string $path, string $content): int
    {
        return self::write($path, $content, FILE_APPEND | LOCK_EX);
    }

    public static function readJson(string $path, bool $assoc = true): mixed
    {
        $content = self::read($path);
        $data = json_decode($content, $assoc);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new RuntimeException('JSON 解析失败: ' . json_last_error_msg());
        }

        return $data;
    }

    public static function writeJson(string $path, mixed $data, bool $pretty = true): int
    {
        $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
        if ($pretty) {
            $flags |= JSON_PRETTY_PRINT;
        }
        return self::write($path, json_encode($data, $flags));
    }

    public static function readLines(string $path, bool $skipEmpty = true): array
    {
        $flags = FILE_IGNORE_NEW_LINES;
        if ($skipEmpty) {
            $flags |= FILE_SKIP_EMPTY_LINES;
        }

        $lines = file($path, $flags);
        return $lines ?: [];
    }
}

// 使用示例
FileHelper::writeJson('/tmp/test.json', ['name' => 'test', 'value' => 42]);
$data = FileHelper::readJson('/tmp/test.json');
FileHelper::append('/tmp/log.txt', "[INFO] 测试完成\n");

注意事项

大文件处理

php
<?php

// file_get_contents 不适合读取超大文件(可能耗尽内存)
// 对于大文件应使用生成器逐行读取
function readLargeFile(string $path): Generator
{
    $handle = fopen($path, 'r');
    if ($handle === false) {
        throw new RuntimeException("无法打开文件: {$path}");
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle);
    }
}

// 使用
foreach (readLargeFile('/tmp/huge.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo $line;
    }
}

FILE_APPEND 与 LOCK_EX 同时使用

php
<?php

// PHP 7.2.14+ 可以同时使用 FILE_APPEND | LOCK_EX
file_put_contents('/tmp/log.txt', $message, FILE_APPEND | LOCK_EX);

// 旧版本需要手动 flock
$handle = fopen('/tmp/log.txt', 'a');
if (flock($handle, LOCK_EX)) {
    fwrite($handle, $message);
    flock($handle, LOCK_UN);
}
fclose($handle);

LOCK_EX 阻塞

LOCK_EX 是阻塞锁,在高并发写入场景可能成为性能瓶颈。考虑使用消息队列或 Redis 进行异步写入。

原子写入

php
<?php

// 原子写入:先写临时文件,再 rename
function atomicWrite(string $path, string $content): void
{
    $tempPath = $path . '.' . getmypid() . '.tmp';
    file_put_contents($tempPath, $content, LOCK_EX);

    if (!rename($tempPath, $path)) {
        @unlink($tempPath);
        throw new RuntimeException("原子写入失败: {$path}");
    }
}

rename 原子性

rename() 在同一个文件系统上是原子操作,可以保证文件内容的完整性。

最佳实践

1. 便捷函数与 fopen 的选择

php
<?php

// 适合 file_get_contents/file_put_contents 的场景:
// - 小文件(< 1MB)
// - 一次性读写
// - 配置文件、模板文件
$content = file_get_contents('config.json');

// 适合 fopen 系列的场景:
// - 大文件
// - 需要逐行处理
// - 需要文件锁定
// - 需要读写指针操作
$handle = fopen('huge.csv', 'r');

2. 安全读取远程内容

php
<?php

// 读取远程 URL 时设置超时
$context = stream_context_create([
    'http' => [
        'timeout' => 10,
        'method'  => 'GET',
    ],
]);

$content = file_get_contents('https://example.com/api/data', false, $context);

if ($content === false) {
    throw new RuntimeException('远程文件读取失败');
}

3. JSON 配置文件管理

php
<?php

// 推荐 JSON 格式的配置文件(比 INI 支持嵌套和更多数据类型)
$configPath = __DIR__ . '/config.json';

// 读取
$config = json_decode(file_get_contents($configPath), true);

// 修改
$config['debug'] = false;
$config['database']['port'] = 3307;

// 保存(原子写入)
$tempPath = $configPath . '.tmp';
file_put_contents($tempPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
rename($tempPath, $configPath);

4. 错误处理模板

php
<?php

declare(strict_types=1);

function loadConfig(string $path): array
{
    if (!file_exists($path)) {
        throw new InvalidArgumentException("配置文件不存在: {$path}");
    }

    $content = file_get_contents($path);

    if ($content === false) {
        throw new RuntimeException("配置文件读取失败: {$path}");
    }

    $data = json_decode($content, true);

    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new RuntimeException(
            "JSON 解析错误: " . json_last_error_msg() . " (文件: {$path})"
        );
    }

    return $data;
}

参考链接