21CTO导读:PHP 8.6的流功能增强了很多函数和方法。
PHP 8.6 为开发者们添加了 StreamErrorMode、StreamError 对象和 stream_last_errors(),因此 fopen() 失败时会报告一个类型化的代码,而不再是原来的警告字符串。
任何超过一定年限的 PHP 代码库中都存在这个函数。它可能被叫做 `__init__` safeRead(),也可能是一个FileHelper没人愿意维护的静态方法。类似如下代码:
function safeRead(string $path): ?string
{
$contents = @file_get_contents($path);
if ($contents === false) {
$error = error_get_last();
if (str_contains($error['message'] ?? '', 'No such file')) {
return null;
}
throw new RuntimeException($error['message'] ?? 'Unknown stream failure');
}
return $contents;
}str_contains()是真正应该让你担心的地方。除了“文件存在但无法读取”之外,判断“文件不存在”的唯一方法是使用 grep 命令搜索一个 PHP 从未被强制要求保持稳定的英文句子。更改语言环境、更改封装器、升级 PHP后,你的代码分支就会悄无声息地停止了匹配。
现在,PHP 8.6 修复了这个问题。
Jakub Zelenka 撰写的“流错误处理改进”RFC在 5 月份以 25 票赞成、1 票反对、5 票弃权获得通过,并于 8 月底被标记为已实现,这意味着它已顺利地纳入 8.6 分支,赶在 9 月 24 日的版本冻结之前完成。
三种上下文选项
所有内容都依附于流上下文,位于一个新的根stream字段下。有以下三种选择。
error_mode决定错误如何显示。它接受一个StreamErrorMode枚举值:(Error当前行为、警告和通知,仍然是默认值),Exception(终止错误抛出异常StreamException),或Silent(根本不发出任何内容)。
error_store决定哪些错误会被保留以供稍后检索,使用枚举StreamErrorStore类型,包括、 、Auto和None。是默认值,它执行合理的操作:在模式下不存储任何错误,在模式下存储未抛出的非终止错误,在模式下存储所有错误。NonTerminatingTerminatingAllAutoErrorExceptionSilent
error_handler这是一个可选的回调函数,它接收一个对象数组StreamError。无论你选择哪种模式,它都会触发,因此是记录日志的理想位置。
区分终止错误和非终止错误至关重要。终止错误会阻止操作完成,例如文件缺失或权限被拒绝。非终止错误是 PHP 希望您了解但不会停止工作的错误,例如缓冲区截断。异常模式仅对终止错误抛出异常。
异常模式
这是大多数应用程序代码需要的版本:
$context = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Exception,
],
]);
try {
$handle = fopen('/srv/phparch/issues/2026-09.pdf', 'r', false, $context);
} catch (StreamException $e) {
$first = array_first($e->getErrors());
if ($first?->code === StreamErrorCode::NotFound) {
return $this->regenerateIssue();
}
throw $e;
}StreamException::getErrors()返回一个数组StreamError,StreamError是一个final readonly具有六个属性的类:(枚举code情况StreamErrorCode),,,message(通常及其朋友),,和,通常是失败的文件名或 URL。wrapperNameseverityE_WARNINGterminatingparam
该code属性是关键所在。它提供了StreamErrorCode超过七十种情况,涵盖了实际可能出错的情况:,,,,,,,,,,,,,以及大量NotFound特定于包装器的条件。您需要比较枚举类型,而不是子字符串PermissionDenied。AlreadyExistsReadFailedWriteFailedSeekNotSupportedConnectFailedRedirectLimitAuthFailedInvalidUrlLockFailedWrapperNotFound
静默模式,当你不希望出现例外情况时
并非每次读取失败都需要进行堆栈展开。例如,缓存查找本来就预期会出现未命中:
$context = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Silent,
'error_store' => StreamErrorStore::All,
],
]);
$cached = @fopen($cachePath, 'r', false, $context);
if ($cached === false) {
$error = array_first(stream_last_errors());
if ($error?->code === StreamErrorCode::NotFound) {
$cached = $this->warm($cachePath);
} else {
$this->logger->warning('Cache read failed', [
'code' => $error?->code->name,
'wrapper' => $error?->wrapperName,
'path' => $error?->param,
]);
}
}stream_last_errors()返回最近一次存储过数据的操作的错误,并按主要错误排序。它会替换每次可存储操作的先前结果,因此无需在调用之间清除数据。虽然也stream_clear_errors()可以显式地丢弃这些错误,但这只是一个维护工具,而非正确性要求。
array_first()这是 PHP 8.5 新增的功能,与此搭配使用效果很好。如果你仍然沿用 8.4 的语义,$errors[0] ?? null它也能实现同样的功能。
错误往往重复出现
这是我始料未及的,现在看来却显而易见。一次流调用可能因多种原因失败,而 PHP 8.6 会保留所有这些失败原因。
RFC 中的示例使用的是stream_select()用户空间流。由于 `select` 函数stream_cast()未实现,因此查询失败;随后,由于该流无法表示为文件描述符,查询再次失败。这两个查询都是真实的,都已终止,并且最终都进入了数组。
$errors = stream_last_errors();
foreach ($errors as $error) {
echo $error->code->name . ': ' . $error->message . PHP_EOL;
}
if (array_any($errors, fn ($e) => $e->code === StreamErrorCode::CastNotSupported)) {
echo 'This stream cannot be used with select()' . PHP_EOL;
}因为它是一个普通的数组,array_find()所以array_any()所有array_filter()操作都可以直接作用于它。RFC 的早期草案使用了带有属性的链表next,而 2.2 版本将其替换为数组。如果您阅读过今年早些时候提到的返回链式对象的文档stream_get_last_error(),那么该 API 已被移除。
四个函数与上下文参数
对于从未接受上下文的调用,无法配置错误处理,因此 RFC 添加了一到四个缺少此功能的函数:
stream_select()又提出了第六个参数stream_copy_to_stream()拿起第五个stream_socket_pair()拿起第四个stream_is_local()拿起第二个所有选项均为可选,默认值均为null。正因如此,stream_select()上面的示例才成为可能。
$context = stream_context_create([
'stream' => ['error_mode' => StreamErrorMode::Exception],
]);
try {
$src = fopen('phptek://schedule.json', 'r', false, $context);
$dst = fopen('/tmp/schedule.json', 'w', false, $context);
stream_copy_to_stream($src, $dst, null, 0, $context);
} catch (StreamException $e) {
$this->logger->error('Copy failed', ['errors' => $e->getErrors()]);
}重复了解的错误
你不能在默认上下文中设置error_mode、error_store或。将它们中的任何一个传递给 都会抛出异常。error_handlerstream_context_set_default()ValueError
这种限制是刻意为之,而且说实话,也是正确的。如果将进程中的每个流都切换到异常模式,就会破坏任何依赖于静默@fopen()返回false警告的库,而且你也无法知道你的 vendor 目录中的哪个库会受到影响。错误处理必须通过你创建并传入的显式上下文进行配置,这意味着它只会应用于你拥有的调用。
这对现有代码意味着什么
一切正常。StreamErrorMode::Error这是默认设置,会完全保留当前的警告和通知行为。
如果你有不常见的流代码,那么 RFC 列出了三项值得关注的细微改动。一些之前报告错误的错误已被修复。上下文现在可以正确地传递到子流。此外,错误报告的触发时间更靠近函数返回点,这可能会导致同一调用中流错误和非流错误的顺序发生变化。如果您有一个测试套件,需要对警告的顺序进行断言,那么最后这一点很可能是造成意外结果的原因。
我们什么时候可以使用它
PHP 8.6 Beta 1 已经于 2026 年 8 月 13 日发布。RC1 计划于 9 月 24 日发布(届时将正式冻结),正式版目标发布日期为 11 月 19 日。这些日期只是计划而非承诺,但该功能本身已经在分支中了。
如果要测试 beta 版,请注意一点。RFC 文档明确指出,该实现仍需对代码库进行进一步的转换和分组,因此并非所有流错误点都已集成到新系统中。如果遇到未转换的包装器,你将收到旧式错误,或者收到条目数少于预期的分组。API 本身已经确定,但其背后的功能仍在完善中。
对于大多数应用程序来说,比较现实的时间表是,你将在 2027 年的某个时候升级到 8.6 版本,到那时,你选择的框架中的某个Filesystem封装HttpClient库可能已经在内部采用了这项功能。这没问题。库恰恰是最需要这项功能的,因为它们目前都在自行发布str_contains($message, 'No such file')并寄希望于此。
如果你正维护着这些库之一,请立即在测试套件中安装 beta 版本。提交错误报告后仍可更改,实现的窗口期将于 9 月 24 日关闭。
作者:场长
本篇文章为 @ 场长 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 微信公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
请扫描二维码,使用微信支付哦。