【问题标题】:Throwing exception from custom error handler in PHP从 PHP 中的自定义错误处理程序抛出异常
【发布时间】:2019-07-26 18:16:39
【问题描述】:

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

我想到了两种可能性:使用错误抑制运算符@ 或使用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:我知道基准测试的原因是......充其量是可疑的,只要性能可行,我应该采用最清晰的代码,但它仍然让我发现了这个有趣的代码点。

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

  • Windows 7、Apache 2.4.38、PHP 7.3.2(通过 XAMPP)
  • Windows 7、Apache 2.4.29、PHP 7.2.2(通过 XAMPP)
  • Ubuntu 服务器 18.04、Apache/2.4.29 (Ubuntu)、PHP 7.2.15-0ubuntu0.18.04.1

【问题讨论】:

    标签: php apache exception crash php-7


    【解决方案1】:

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

    【讨论】:

    • 这个函数(fopen)如果失败仍然会抛出警告。在使用@ 抑制错误的情况下,如果要获取有关错误的信息,则需要调用error_get_last,并从那里获取信息。这添加了相当多的代码,因为 fopen (和其他同类函数)被多次调用,尽管我可以创建一个函数来处理这些错误并抛出,你在这一点上是对的。这仍然留下了关于这是否是一个错误的问题。
    • @Baldorien,您可以忽略该警告,在生产环境中,您的警告永远不应显示给用户,因此不会造成问题。此外,您可以检查标准内容,例如文件是否存在可读/可写,以捕捉 fopen 失败的常见情况
    • 虽然我同意不应向用户显示 PHP 错误这一事实,但我不同意它们可以被忽略的观点,因为警告的存在是有原因的,并且可能会出现大量警告是获取繁重且可读性较差的日志文件的好方法。文件存在只是 IO 可能出错的众多事情之一。它仍然留下一个主要问题:这是 PHP 的错误吗?
    • 是的,我同意你的观点,是的,我认为这是 PHP 中的一个错误。因为它同时生成警告并返回 false。 fopen 的一个好的实现应该是返回内容并在任何失败时抛出异常。这意味着开发人员可以捕获此异常并相应地处理它。遗憾的是,除了使用 @ (如您所提到的)会降低性能之外,使用 E_WARNINing 是不可能的。很遗憾,没有一个完美的解决方案,你可以认为这是 PHP 的问题
    • 我不是在这里谈论 fopen 的实现,所以我的问题可能写得不好。我在谈论 apache 服务器与我的问题中的脚本的崩溃,因为这也可能是一种未记录或错误记录但“正常”的行为,考虑到 PHP 的文档并不总是详尽无遗。我将关闭此问题,将其视为错误并将其添加到跟踪器中。
    【解决方案2】:

    由于这已被确认为 PHP 错误并找到了确切原因,我将发布答案以及如何解决此问题,直到错误解决为止。

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

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

    由于明确了原因,解决方案很简单:在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%,当所有错误参数都用于两者时。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-08-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-12
      相关资源
      最近更新 更多