编程 PHP 8.6:close 阶段失败时,fclose() 与 file_put_contents() 不再返回成功

2026-11-08 21:03:21

PHP 8.6:close 阶段失败时,fclose() 与 file_put_contents() 不再返回成功

PHP 8.6 的 BC 变更清单里有一条容易被跳过:fclose()、file_put_contents() 和 copy() 现在在 flush 或关闭 stream 失败时返回 false。此前这类失败被静默忽略,调用方拿到的是 true 或者写入字节数。RC5 定在 2026-11-05,GA 2026-11-19。

参考链接:

  • 迁移文档:https://www.php.net/manual/en/migration86.new-features.php
  • 文档追踪 issue:https://github.com/php/doc-en/issues/5901
  • file_put_contents() 手册:https://www.php.net/manual/en/function.file-put-contents.php

失败点位于 write() 之后

file_put_contents() 的返回值一直是写入字节数,失败返回 false。问题在于“失败”的判定位置:数据先写进页缓存,真正的落盘和元数据更新发生在 flush / close 阶段。在 NFS 或其他网络文件系统上,EDQUOT、ENOSPC、EIO 常常在 write() 已经返回成功之后才由 close() 抛出。本地磁盘配额结算、延迟分配的文件系统也是同样的时序。

于是代码看到的是:字节数对得上,函数没报错,文件实际是截断的或者干脆没落地。

只读挂载多数在 fopen() 阶段就失败了,不在这条改动的覆盖范围内。它管的是“打开成功、写也成功、关闭失败”这一类。

以前为什么是静默的

PHP 的 stream 层在关闭时执行的 flush 结果没有向上传播。fclose() 只看资源是不是有效、关闭流程有没有走完,flush 阶段底层返回的错误码被丢掉了。file_put_contents() 则用 php_stream_write() 的返回值累加字节数,close 的返回值不参与最终结果。所以错误不是没发生,是没传到 PHP 用户态。

升级后需要改的写法

file_put_contents():保留原来的字节数比对,再加 false 判断。

$data  = json_encode($payload, JSON_UNESCAPED_UNICODE);
$bytes = file_put_contents($path, $data);

if ($bytes === false) {
    throw new RuntimeException("write failed: {$path}");
}
if ($bytes !== strlen($data)) {
    throw new RuntimeException("short write: {$bytes}/" . strlen($data));
}

注意 === false,不能用 !$bytes——写入 0 字节是合法结果。

copy():以前 close 阶段失败返回 true,现在返回 false,调用点如果是 copy($a, $b) or die() 这类写法,行为会变。

if (!copy($src, $dst)) {
    throw new RuntimeException("copy failed: {$src} -> {$dst}");
}

fclose():手动管理句柄的路径要把返回值纳入判断,尤其是先 fwrite() 再 fclose() 的写法。

$fh = fopen($tmp, 'wb');
if ($fh === false) {
    throw new RuntimeException("open failed: {$tmp}");
}

if (fwrite($fh, $chunk) === false) {
    fclose($fh);
    throw new RuntimeException("write failed: {$tmp}");
}

if (!fclose($fh)) {
    throw new RuntimeException("close failed: {$tmp}");
}

rename($tmp, $final); // 只在 close 成功之后

临时文件 + rename() 的原子写模式尤其要检查这一条:rename() 之前必须确认 fclose() 没有返回 false,否则移动的是一个内容不完整的文件。

fsync 与校验点的关系

PHP 没有把 fsync() 封装进 file_put_contents()。要主动把数据推到存储,得自己走 fopen + fwrite + fflush + fsync + fclose:

$fh = fopen($path, 'wb');
fwrite($fh, $data);
fflush($fh);
if (!fsync($fh)) {
    fclose($fh);
    throw new RuntimeException("fsync failed: {$path}");
}
if (!fclose($fh)) {
    throw new RuntimeException("close failed: {$path}");
}

两者的分工不同:fsync() 是主动校验点,把 ENOSPC / EIO 提前暴露在一个有返回值的调用上;fclose() 的 false 是最后一道兜底,覆盖那些直到关闭才结算的错误。只有 fsync() 不够——配额和网络文件系统的部分错误仍然在 close 时才出现;只有 fclose() 也不够——你拿不到明确的同步点。

对可靠性要求高的写入,再补一层读回校验(长度或 hash),尤其是在 NFS 上。

同批 BC break 还有 by-reference foreach、array_intersect() 的字符串转换时机、trim() 默认字符集加入 \f 等,与本文主题无关,升级时单独过一遍清单即可。

复制全文 生成海报 PHP 文件写入 升级踩坑

推荐文章

程序员茄子在线接单