How do I catch a PHP fatal (`E_ERROR`) error?

前端 未结 17 2291
北荒
北荒 2020-11-21 06:21

I can use set_error_handler() to catch most PHP errors, but it doesn\'t work for fatal (E_ERROR) errors, such as calling a function that doesn\'t e

相关标签:
17条回答
  • 2020-11-21 06:46

    Nice solution found in 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);
        }
    }
    

    This class allows you to start the specific ErrorHandler sometimes if you need it. And then you can also stop the Handler.

    Use this class e.g. like this:

    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;
    

    Link to the full class code:
    https://github.com/zendframework/zf2/blob/master/library/Zend/Stdlib/ErrorHandler.php


    A maybe better solution is that one from Monolog:

    Link to the full class code:
    https://github.com/Seldaek/monolog/blob/master/src/Monolog/ErrorHandler.php

    It can also handle FATAL_ERRORS using the register_shutdown_function function. According to this class a FATAL_ERROR is one of the following 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;
        }
    
        // [...]
    }
    
    0 讨论(0)
  • 2020-11-21 06:47

    Log fatal errors using the register_shutdown_function, which requires PHP 5.2+:

    register_shutdown_function( "fatal_handler" );
    
    function fatal_handler() {
        $errfile = "unknown file";
        $errstr  = "shutdown";
        $errno   = E_CORE_ERROR;
        $errline = 0;
    
        $error = error_get_last();
    
        if($error !== NULL) {
            $errno   = $error["type"];
            $errfile = $error["file"];
            $errline = $error["line"];
            $errstr  = $error["message"];
    
            error_mail(format_error( $errno, $errstr, $errfile, $errline));
        }
    }
    

    You will have to define the error_mail and format_error functions. For example:

    function format_error( $errno, $errstr, $errfile, $errline ) {
        $trace = print_r( debug_backtrace( false ), true );
    
        $content = "
        <table>
            <thead><th>Item</th><th>Description</th></thead>
            <tbody>
                <tr>
                    <th>Error</th>
                    <td><pre>$errstr</pre></td>
                </tr>
                <tr>
                    <th>Errno</th>
                    <td><pre>$errno</pre></td>
                </tr>
                <tr>
                    <th>File</th>
                    <td>$errfile</td>
                </tr>
                <tr>
                    <th>Line</th>
                    <td>$errline</td>
                </tr>
                <tr>
                    <th>Trace</th>
                    <td><pre>$trace</pre></td>
                </tr>
            </tbody>
        </table>";
        return $content;
    }
    

    Use Swift Mailer to write the error_mail function.

    See also:

    • $php_errormsg
    • Predefined Constants
    0 讨论(0)
  • 2020-11-21 06:48

    You cannot throw an exception inside a registered shutdown function like that:

    <?php
        function shutdown() {
            if (($error = error_get_last())) {
               ob_clean();
               throw new Exception("fatal error");
            }
        }
    
        try {
            $x = null;
            $x->method()
        } catch(Exception $e) {
            # This won't work
        }
    ?>
    

    But you can capture and redirect request to another page.

    <?php
        function shutdown() {
            if (($error = error_get_last())) {
               ob_clean();
               # Report the event, send email, etc.
               header("Location: http://localhost/error-capture");
               # From /error-capture. You can use another
               # redirect, to e.g. the home page
            }
        }
        register_shutdown_function('shutdown');
    
        $x = null;
        $x->method()
    ?>
    
    0 讨论(0)
  • 2020-11-21 06:49

    Here is just a nice trick to get the current error_handler method =)

    <?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;
            }
        }
    ?>
    

    Also I want to note that if you call

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

    PHP stops displaying the error. Otherwise, the error text will be send to the client prior to your error handler.

    0 讨论(0)
  • 2020-11-21 06:49

    Since most answers here are unnecesarily verbose, here's my non-ugly version of the top voted answer:

    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");
    
    0 讨论(0)
  • 2020-11-21 06:52

    There are certain circumstances in which even fatal errors should be caught (you might need to do some clean up before exiting gracefully and don’t just die..).

    I have implemented a pre_system hook in my CodeIgniter applications so that I can get my fatal errors through emails, and this helped me finding bugs that were not reported (or were reported after they were fixed, as I already knew about them :)).

    Sendemail checks if the error has already been reported so that it does not spam you with known errors multiple times.

    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();
        }
    }
    
    0 讨论(0)
提交回复
热议问题