17611538698
webmaster@21cto.com

PHP 8.6 增强流功能,并提供了真正的错误信息

编程语言 0 16 1小时前
图片

21CTO导读:PHP 8.6的流功能增强了很多函数和方法。

PHP 8.6 为开发者们添加了 StreamErrorMode、StreamError 对象和 stream_last_errors(),因此 fopen() 失败时会报告一个类型化的代码,而不再是原来的警告字符串。

“PHP 8.6 为流提供真正的错误,您终于可以停止解析警告字符串”的特色图片

任何超过一定年限的 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类型,包括、 、AutoNone默认值,它执行合理的操作:在模式下不存储任何错误,在模式下存储未抛出的非终止错误,在模式下存储所有错误。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()返回一个数组StreamErrorStreamError是一个final readonly具有六个属性的类:(枚举code情况StreamErrorCode),,,message通常及其朋友),,,通常是失败的文件名或 URL。wrapperNameseverityE_WARNINGterminatingparam

code属性是关键所在。提供StreamErrorCode超过七十种情况,涵盖了实际可能出错的情况:,,,,,,,,,,,,,以及大量NotFound特定包装器的条件。您需要比较枚举类型,而不是字符串PermissionDeniedAlreadyExistsReadFailedWriteFailedSeekNotSupportedConnectFailedRedirectLimitAuthFailedInvalidUrlLockFailedWrapperNotFound

静默模式,当你不希望出现例外情况时

并非每次读取失败都需要进行堆栈展开。例如,缓存查找本来就预期会出现未命中:

$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, null0, $context);
catch (StreamException $e) {
    $this->logger->error('Copy failed', ['errors' => $e->getErrors()]);
}

重复了解的错误

你不能在默认上下文中设置error_modeerror_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 日关闭。

作者:场长

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信