如何捕获 PHP 致命错误(`E_ERROR`)?

600

我可以使用set_error_handler()来捕获大多数PHP错误,但它无法处理致命错误(E_ERROR),例如调用不存在的函数。是否有另一种方法来捕获这些错误?

我正尝试在所有错误时调用mail(),并且正在运行PHP 5.2.3。


1
我写了一个类似维基百科的问答,提供了PHP中捕获所有错误的完整解决方案;可以在Stack Overflow上查看、借鉴、抄袭或评论。该解决方案包括五种方法,可以包装PHP可能生成的所有错误,并最终将这些错误传递给一个“ErrorHandler”类型的对象。 - DigitalJedi805
参见:https://dev59.com/OHNA5IYBdhLWcg3wHp-B - dreftymac
参见:https://bugs.php.net/bug.php?id=41418 - dreftymac
参见:https://dev59.com/f2w05IYBdhLWcg3wpDVu - dreftymac
我提供了一个适用于PHP 7的简单答案:https://dev59.com/f2w05IYBdhLWcg3wpDVu - David Spector
18个回答

10

这里有一个很好的技巧,可以获取当前的错误处理程序方法 =)

<?php
    register_shutdown_function('__fatalHandler');

    function __fatalHandler()
    {
        $error = error_get_last();

        // Check if it's a core/fatal error. Otherwise, it's a normal shutdown
        if($error !== NULL && $error['type'] === E_ERROR) {

            // It is a bit hackish, but the set_exception_handler
            // will return the old handler
            function fakeHandler() { }

            $handler = set_exception_handler('fakeHandler');
            restore_exception_handler();
            if($handler !== null) {
                call_user_func(
                    $handler,
                    new ErrorException(
                        $error['message'],
                        $error['type'],
                        0,
                        $error['file'],
                        $error['line']));
            }
            exit;
        }
    }
?>

此外,我希望指出的是,如果您调用了该函数

<?php
    ini_set('display_errors', false);
?>

PHP停止显示错误。否则,错误文本将在您的错误处理程序之前发送到客户端。


1
因为这行代码 ini_set('display_errors', false);,我点了赞。 - Sahib Khan
如果由于某种原因此位被打开,即使您以不同的方式处理它,它仍将显示PHP错误。 - Sahib Khan

10

Zend Framework 2 中找到了一个好的解决方案:

/**
 * ErrorHandler that can be used to catch internal PHP errors
 * and convert to an ErrorException instance.
 */
abstract class ErrorHandler
{
    /**
     * Active stack
     *
     * @var array
     */
    protected static $stack = array();

    /**
     * Check if this error handler is active
     *
     * @return bool
     */
    public static function started()
    {
        return (bool) static::getNestedLevel();
    }

    /**
     * Get the current nested level
     *
     * @return int
     */
    public static function getNestedLevel()
    {
        return count(static::$stack);
    }

    /**
     * Starting the error handler
     *
     * @param int $errorLevel
     */
    public static function start($errorLevel = \E_WARNING)
    {
        if (!static::$stack) {
            set_error_handler(array(get_called_class(), 'addError'), $errorLevel);
        }

        static::$stack[] = null;
    }

    /**
     * Stopping the error handler
     *
     * @param  bool $throw Throw the ErrorException if any
     * @return null|ErrorException
     * @throws ErrorException If an error has been catched and $throw is true
     */
    public static function stop($throw = false)
    {
        $errorException = null;

        if (static::$stack) {
            $errorException = array_pop(static::$stack);

            if (!static::$stack) {
                restore_error_handler();
            }

            if ($errorException && $throw) {
                throw $errorException;
            }
        }

        return $errorException;
    }

    /**
     * Stop all active handler
     *
     * @return void
     */
    public static function clean()
    {
        if (static::$stack) {
            restore_error_handler();
        }

        static::$stack = array();
    }

    /**
     * Add an error to the stack
     *
     * @param int    $errno
     * @param string $errstr
     * @param string $errfile
     * @param int    $errline
     * @return void
     */
    public static function addError($errno, $errstr = '', $errfile = '', $errline = 0)
    {
        $stack = & static::$stack[count(static::$stack) - 1];
        $stack = new ErrorException($errstr, 0, $errno, $errfile, $errline, $stack);
    }
}

如果需要,此类允许您启动特定的ErrorHandler,然后您还可以停止处理程序。

例如,可以像这样使用此类:

ErrorHandler::start(E_WARNING);
$return = call_function_raises_E_WARNING();

if ($innerException = ErrorHandler::stop()) {
    throw new Exception('Special Exception Text', 0, $innerException);
}

// or
ErrorHandler::stop(true); // directly throws an Exception;

完整类代码链接:
https://github.com/zendframework/zf2/blob/master/library/Zend/Stdlib/ErrorHandler.php


也许更好的解决方案是来自Monolog的解决方案:

完整类代码链接:
https://github.com/Seldaek/monolog/blob/master/src/Monolog/ErrorHandler.php

它还可以使用register_shutdown_function函数处理致命错误。根据此类,致命错误包括以下错误类型:array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR)

class ErrorHandler
{
    // [...]

    public function registerExceptionHandler($level = null, $callPrevious = true)
    {
        $prev = set_exception_handler(array($this, 'handleException'));
        $this->uncaughtExceptionLevel = $level;
        if ($callPrevious && $prev) {
            $this->previousExceptionHandler = $prev;
        }
    }

    public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1)
    {
        $prev = set_error_handler(array($this, 'handleError'), $errorTypes);
        $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
        if ($callPrevious) {
            $this->previousErrorHandler = $prev ?: true;
        }
    }

    public function registerFatalHandler($level = null, $reservedMemorySize = 20)
    {
        register_shutdown_function(array($this, 'handleFatalError'));

        $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
        $this->fatalLevel = $level;
    }

    // [...]
}

7

PHP存在可捕获的致命错误,它们被定义为E_RECOVERABLE_ERROR。PHP手册描述E_RECOVERABLE_ERROR如下:

可捕获的致命错误。它表示可能发生了危险的错误,但未使引擎处于不稳定的状态。如果用户没有定义句柄来处理该错误(参见set_error_handler()),则应用程序将停止,就像一个E_ERROR一样。

您可以使用set_error_handler()并检查是否存在E_RECOVERABLE_ERROR来“捕获”这些“致命”错误。当捕获到此错误时,我认为将其抛出异常是很有用的,然后您可以使用try/catch。

以下问题和回答提供了一个有用的示例:如何在PHP类型提示中捕获“可捕获的致命错误”?

然而,E_ERROR错误可以被处理,但无法从中恢复,因为引擎处于不稳定状态。


6

由于这里的大多数答案都冗长无用,以下是我非丑陋的版本,是排名第一的答案:

function errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = array()) {
    //Do stuff: mail, log, etc
}

function fatalHandler() {
    $error = error_get_last();
    if($error) errorHandler($error["type"], $error["message"], $error["file"], $error["line"]);
}

set_error_handler("errorHandler")
register_shutdown_function("fatalHandler");

5
并不是这样的。致命错误被称为致命错误,因为它们是致命的。你无法从中恢复。

16
捕捉和恢复是两件非常不同的事情。 - Simon Forsberg

3

有些情况下,即使是致命错误也应该被捕获(您可能需要在优雅地退出之前进行一些清理,而不仅仅是停止运行)。

我在我的 CodeIgniter 应用程序中实现了一个 pre_system 钩子,以便我可以通过电子邮件获得我的致命错误,并且这帮助我找到了未报告的错误(或者在修复后才报告,因为我已经知道它们 :))。

Sendemail 检查错误是否已经报告,以便不会多次发送已知错误的垃圾邮件。

class PHPFatalError {

    public function setHandler() {
        register_shutdown_function('handleShutdown');
    }
}

function handleShutdown() {
    if (($error = error_get_last())) {
        ob_start();
        echo "<pre>";
        var_dump($error);
        echo "</pre>";
        $message = ob_get_clean();
        sendEmail($message);
        ob_start();
        echo '{"status":"error","message":"Internal application error!"}';
        ob_flush();
        exit();
    }
}

什么是“Sendemail”?你的意思是*Sendmail*吗?请通过编辑回答,而不是在评论中回复。 - Peter Mortensen

3

我开发了这个函数,使得可以“沙盒”可能导致致命错误的代码。由于从闭包register_shutdown_function抛出的异常不会从预先致命错误的调用堆栈中发出,所以我被迫在此函数之后退出,以提供一种统一的使用方式。

function superTryCatchFinallyAndExit( Closure $try, Closure $catch = NULL, Closure $finally )
{
    $finished = FALSE;
    register_shutdown_function( function() use ( &$finished, $catch, $finally ) {
        if( ! $finished ) {
            $finished = TRUE;
            print "EXPLODE!".PHP_EOL;
            if( $catch ) {
                superTryCatchFinallyAndExit( function() use ( $catch ) {
                    $catch( new Exception( "Fatal Error!!!" ) );
                }, NULL, $finally );                
            } else {
                $finally();                
            }
        }
    } );
    try {
        $try();
    } catch( Exception $e ) {
        if( $catch ) {
            try {
                $catch( $e );
            } catch( Exception $e ) {}
        }
    }
    $finished = TRUE;
    $finally();
    exit();
}

0

截至PHP 7.4.13,我的经验是一个程序中所有可能的错误和异常都可以通过仅使用两个回调函数来捕获:

set_error_handler("ErrorCB");
set_exception_handler("ExceptCB");

ErrorCB会以任何所需的方式报告其参数并调用Exit()。

ExceptCB在其异常参数上调用“get”方法,并进行一些逻辑来确定文件、行和函数的位置(如果您想了解详细信息,请问我),然后以任何所需的方式报告信息并返回。

唯一需要try/catch的情况是,当@或isset()不足以抑制某些代码的错误时。对于没有设置处理程序的“主函数”使用try/catch会失败,因为它无法捕获所有错误。

如果有人发现生成错误的代码无法被此方法捕获,请告诉我,我将编辑此答案。此方法无法拦截的一个错误是PHP程序末尾附近的单个{字符;这会生成一个Parse error,需要通过包含错误处理的Include文件运行主PHP程序。

我没有发现需要register_shutdown_function()。

请注意,我关心的只是报告错误然后退出程序;我不需要从错误中恢复——那将是一个更困难的问题。


1
需要 "NoExists.php";:抛出一个警告,然后是致命错误,警告由用户错误处理程序处理,但致命错误既不由用户错误处理程序处理,也不由用户异常处理程序处理,也不由 try/catch 处理。自 PHP 8 以来已经被“修复”。 - Esteban

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接