PHP 文件

Reading, writing, existence checks, directory listing, and concurrent write hazards.

Beginner
Key PointsUse `file_get_contents` for small files; `fopen` for large ones or handle control.

Reading and writing small files

PHP
<?php
$path = __DIR__ . "/demo.txt";

file_put_contents($path, "第一行\n第二行\n");
echo file_exists($path) ? "文件存在" : "文件不存在", "\n";
echo filesize($path), "\n";
echo file_get_contents($path);

unlink($path);
echo file_exists($path) ? "文件存在" : "文件不存在", "\n";
Output
文件存在
20
第一行
第二行
文件不存在

filesize returns bytes: 9 + 1 + 9 + 1 = 20. Each Chinese character is 3 bytes in UTF-8.

Reading line by line

Logs and CSVs must not be slurped whole. fgets reads one line at a time with constant memory.

PHP
<?php
$path = __DIR__ . "/log.txt";
file_put_contents($path, "2023-11-15 INFO 启动\n2023-11-15 WARN 磁盘\n2023-11-15 ERROR 失败\n");

$fp = fopen($path, "r");
if ($fp === false) {
    exit("打不开文件");
}

$count = 0;
while (($line = fgets($fp)) !== false) {
    $line = trim($line);
    if ($line === "") {
        continue;
    }
    if (str_contains($line, "ERROR")) {
        echo "命中: $line\n";
    }
    $count++;
}
fclose($fp);

echo "共 $count 行\n";
unlink($path);
Output
命中: 2023-11-15 ERROR 失败
共 3 行
The !== false in while (($line = fgets($fp)) !== false) must be strict — != can end the loop early on an empty line.

Open modes

ModeReadWriteOn existing file
r是否从开头读
r+是是从开头读写,不截断
w否是清空再写
w+是是清空再读写
a否是追加到末尾
a+是是追加写,可读
x否是文件已存在则失败
b——二进制模式,附加在末尾如 rb

x creates exclusively and fails if the file exists — safer than check-then-open, which races.

Locked writes

Concurrent writes interleave and corrupt data. Use flock with an exclusive lock.

PHP
<?php
$path = __DIR__ . "/counter.txt";
file_put_contents($path, "0");

$fp = fopen($path, "r+");
if ($fp === false) {
    exit("打不开");
}

if (flock($fp, LOCK_EX)) {
    $n = (int)stream_get_contents($fp);
    $n++;
    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, (string)$n);
    fflush($fp);
    flock($fp, LOCK_UN);
}
fclose($fp);

echo file_get_contents($path), "\n";
unlink($path);
Output
1
flock is unreliable on NFS and differs on Windows. Shared state like counters belongs in a database or Redis; file locks suit only low-concurrency local use.

Directory operations

PHP
<?php
$dir = __DIR__ . "/tmpdir";
@mkdir($dir);
file_put_contents($dir . "/a.txt", "a");
file_put_contents($dir . "/b.txt", "b");

$items = scandir($dir);
print_r($items);

foreach (glob($dir . "/*.txt") as $f) {
    echo basename($f), "\n";
}

array_map("unlink", glob($dir . "/*.txt"));
rmdir($dir);
echo is_dir($dir) ? "目录还在" : "目录已删", "\n";
Output
Array
(
    [0] => .
    [1] => ..
    [2] => a.txt
    [3] => b.txt
)
a.txt
b.txt
目录已删

scandir includes . and .. — skip them. glob supports wildcards and suits extension filters.

Path safety

Anywhere users influence a path (download, preview, upload) is a directory-traversal hotspot. Block ?file=../../etc/passwd.

PHP
<?php
function safePath(string $base, string $name): ?string {
    if (str_contains($name, "\0")) {
        return null;
    }
    $real = realpath($base . "/" . $name);
    if ($real === false) {
        return null;
    }
    $baseReal = realpath($base);
    if ($baseReal === false || !str_starts_with($real, $baseReal . DIRECTORY_SEPARATOR)) {
        return null;
    }
    return $real;
}

var_dump(safePath(__DIR__, "demo.txt") !== null);
var_dump(safePath(__DIR__, "../etc/passwd"));
var_dump(safePath(__DIR__, "a/../../x"));
Output
bool(false)
NULL
NULL

The first is false because demo.txt does not exist. The key cases are two and three: traversal is blocked by the prefix check.

realpath() resolves symlinks, so a prefix check on the resolved path is sound. Stripping .. as a string is not — ....// slips through.

Friends & Partners