PHP 中的自定义错误处理程序引发异常

Throwing exception from custom error handler in PHP

我正在研究资源流,更具体地说是 fopen()。 除了返回 false 而不是资源之外,此函数在失败时会引发警告。不需要的警告是我决定抑制它们的问题。

我想到了两种可能性:使用错误抑制运算符 @ or using set_error_handler()。并且被告知 @ 没有那么好的表现并且经常带来比它解决的问题更多的问题,我 运行 一个快速基准来看看 set_error_handler() 如何对抗它。

问题代码来了:

<?php 

error_reporting(E_ALL);

function errorHandler(int $errorNumber, string $errorMessage)
{
    throw new \Exception();
}

$previousHandler = set_error_handler("errorHandler");

$operations = 10000;

for($i = 0; $i < $operations; $i++) {
    try{
        $inexistant[0];
    } catch (\Exception $e) {}
}

set_error_handler($previousHandler);

echo 'ok';

运行 这个简单的代码将使 apache 服务器崩溃并显示以下消息:

[mpm_winnt:notice] [pid 6000:tid 244] AH00428: Parent: child process 3904 exited with status 3221225725 -- Restarting.

经过搜索,这条信息的意思是服务器发生了访问冲突错误,主要是在达到堆栈大小限制的情况下。然而,情况不应如此,因为此代码不应增加堆栈大小(事实上,它不会增加 PHP 堆栈帧)。

我还测试了时间是否重要,但即使每次迭代之间有 3 毫秒的休眠,崩溃也会在大致相同的迭代次数后发生。这个数字在 700 左右,但波动很小,有时 运行 704 很好,有时则不好。

此外,在 php 错误跟踪器上搜索没有显示任何相关内容,除了这个 bug entry,它讨论了对处理函数的调用进行处理的事实。这可能意味着异常有可能绕过函数退出时的某些处理,但由于我对 PHP 源代码一无所知,这纯粹是猜测。

因为我想正确传播错误消息,使用 set_error_handler() 的方式将是最清晰的方式,但我知道我可以使用 error_get_last() 和 @ 运算符用更多的代码实现相同的目标(因为在真实的项目中有多个函数像 fopen() 一个接一个地调用)。

所以问题来了:这是 PHP 的错误吗?有没有办法在保持代码清晰的同时规避这个问题?

谢谢。

PS:我知道基准测试的原因是......充其量是可疑的,只要性能可行,我应该采用最清晰的代码,但它仍然让我发现了这一点有趣的代码点。

编辑:我忘了放我测试过的版本:

为什么要经历这些麻烦?只处理 false 的 return 值会好很多,因为文档声明:Returns a file pointer resource on success, or FALSE on error. 因此,只检查 return 的值是否就足够了fopen 为 false,然后基于此继续操作(或在必要时抛出您自己的错误)。

由于这已被确认为 PHP 错误并且找到了确切原因,我将 post 提供答案和解决方法,直到错误被解决。

首先,这是错误报告:https://bugs.php.net/bug.php?id=77693

这是一个堆栈溢出导致异常上下文的捕获,在这种情况下包括调用错误处理程序的函数,因为这是在他的行为中捕获父上下文。此父上下文包括先前捕获的异常,该异常被添加到要捕获的新异常的上下文中,并重复 ad vitam aeternam 直到崩溃。

原因很明确,解决方法很简单:在catch块的末尾加一个unset()即可,如下:

$operations = 10000;

for($i = 0; $i < $operations; $i++) {
    try{
        $inexistant[0];
    } catch (\Exception $e) {
        unset($e);
    }
}

那就没有问题了

要添加到第二个问题中,要求在 fopen 和其他类似方法的情况下使用干净的替代方法,这是一个解决方案:

function throwLastError() {
    $context = error_get_last();
    error_clear_last();
    throw new ErrorException($context["message"],
                             0,
                             $context["type"],
                             $context["file"],
                             $context["line"]);
}

// Wrong call to fopen
if (!@fopen("", "a"))
    throwLastError();

两个答案的性能大致相同,当所有错误参数都在两者中使用时,@ 方法要慢 10%。