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:我知道基准测试的原因是......充其量是可疑的,只要性能可行,我应该采用最清晰的代码,但它仍然让我发现了这一点有趣的代码点。
编辑:我忘了放我测试过的版本:
- 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
为什么要经历这些麻烦?只处理 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%。
我正在研究资源流,更具体地说是 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:我知道基准测试的原因是......充其量是可疑的,只要性能可行,我应该采用最清晰的代码,但它仍然让我发现了这一点有趣的代码点。
编辑:我忘了放我测试过的版本:
- 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
为什么要经历这些麻烦?只处理 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%。