dadajuice/zephyrus

View on GitHub
src/Zephyrus/Application/ErrorHandler.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php namespace Zephyrus\Application;

class ErrorHandler
{
    /**
     * @var mixed[] Contains exception class name as key and corresponding
     * callback as value.
     */
    private $registeredThrowableCallbacks = [];

    /**
     * @var mixed[] Contains error type as key and corresponding callback as
     * value.
     */
    private $registeredErrorCallbacks = [];

    /**
     * @var ErrorHandler
     */
    private static $instance;

    /**
     * @return ErrorHandler
     */
    public static function getInstance(): self
    {
        if (is_null(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function restoreDefaultHandlers()
    {
        restore_error_handler();
        restore_exception_handler();
    }

    public function restoreDefaultErrorHandler()
    {
        restore_error_handler();
    }

    public function restoreDefaultExceptionHandler()
    {
        restore_exception_handler();
    }

    /**
     * Defines a callback to use when a notice error occurs (E_USER_NOTICE,
     * E_NOTICE, E_DEPRECATED, E_USER_DEPRECATED).
     *
     * @param callable $callback
     */
    public function notice(callable $callback)
    {
        $this->registerError(E_DEPRECATED, $callback);
        $this->registerError(E_USER_DEPRECATED, $callback);
        $this->registerError(E_NOTICE, $callback);
        $this->registerError(E_USER_NOTICE, $callback);
        set_error_handler([$this, 'errorHandler']);
    }

    /**
     * Defines a callback to use when a warning occurs which includes system
     * warning and user defined (E_WARNING, E_USER_WARNING, E_CORE_WARNING,
     * E_COMPILE_WARNING).
     *
     * @param callable $callback
     */
    public function warning(callable $callback)
    {
        $this->registerError(E_WARNING, $callback);
        $this->registerError(E_USER_WARNING, $callback);
        set_error_handler([$this, 'errorHandler']);
    }

    /**
     * Defines a callback to use when a user defined error (E_USER_ERROR)
     * occurs.
     *
     * @param callable $callback
     */
    public function error(callable $callback)
    {
        $this->registerError(E_USER_ERROR, $callback);
        $this->registerError(E_RECOVERABLE_ERROR, $callback);
        set_error_handler([$this, 'errorHandler']);
    }

    /**
     * Give a specific callback function to be used when a specific Exception
     * is thrown. This gives a great control over the program flow, specially
     * for generic application exceptions. The given callback must have only
     * one parameter hinted as an Exception subclass (or directly Exception if
     * the default Exception behavior must be overridden).
     *
     * @param callable $callback
     */
    public function exception(callable $callback)
    {
        // @codeCoverageIgnoreStart
        try {
            $reflection = new \ReflectionFunction($callback);
        } catch (\ReflectionException $e) {
            throw new \InvalidArgumentException("Specified callback is invalid : " . $e->getMessage());
        }
        // @codeCoverageIgnoreEnd
        $parameters = $reflection->getParameters();
        if (count($parameters) != 1) {
            throw new \InvalidArgumentException("Specified callback must only have one argument hinted as a 
                Throwable class");
        }
        $argumentType = $parameters[0]->getType();
        $argumentClass = new \ReflectionClass($argumentType->getName());
        if (!$argumentClass->isSubclassOf('Throwable')) {
            throw new \InvalidArgumentException("Specified callback argument must be hinted child of a 
                Throwable class");
        }
        $this->registeredThrowableCallbacks[$argumentClass->getShortName()] = $callback;
        set_exception_handler([$this, 'exceptionHandler']);
    }

    /**
     * Register an error level with a specific user defined callback. The
     * specified callback can have up to 4 arguments, but they are not
     * required. First provided argument is the message, second is the file
     * path, third is the line number and the fourth is an error context.
     *
     * @param int $level
     * @param callable $callback
     */
    public function registerError($level, callable $callback)
    {
        try {
            $reflection = new \ReflectionFunction($callback);
            $parameters = $reflection->getParameters();
            if (count($parameters) > 4) {
                throw new \InvalidArgumentException("Specified callback cannot have more than 4 arguments (message, file, line, context)");
            }
        } catch (\ReflectionException $e) {
            throw new \InvalidArgumentException("Specified callback is invalid : " . $e->getMessage()); // @codeCoverageIgnore
        }
        $this->registeredErrorCallbacks[$level] = $callback;
    }

    /**
     * When an exception is thrown, this method catches it and tries to find
     * the best user defined callback as a response. If there is no direct
     * callback associated, it will tries to find a definition within the
     * Exception class hierarchy. If nothing is found, the default behavior is
     * to display the error. Should not be called manually. Used as a registered
     * PHP handler.
     *
     * @param \Throwable $error
     * @throws \Throwable
     */
    public function exceptionHandler(\Throwable $error)
    {
        $reflection = new \ReflectionClass($error);
        $registeredException = $this->findRegisteredExceptions($reflection);
        if (!is_null($registeredException)) {
            $registeredException($error);
        } else {
            $this->printUnhandledException($error);
        }
    }

    /**
     * When an error, a notice or a warning is thrown, this method catches it
     * and tries to find the a user defined callback matching the PHP internal
     * error type. Will validate if the raised error type is included in the
     * error_reporting config. Should not be called manually. Used as a
     * registered PHP handler.
     *
     * @param int $type
     * @throws \Exception
     * @return bool
     */
    public function errorHandler($type, ...$args)
    {
        if (!(error_reporting() & $type)) {
            // This error code is not included in error_reporting
            // @codeCoverageIgnoreStart
            return true;
            // @codeCoverageIgnoreEnd
        }
        if (0 === error_reporting()) {
            // error was suppressed with the @-operator
            // @codeCoverageIgnoreStart
            return false;
            // @codeCoverageIgnoreEnd
        }
        if (array_key_exists($type, $this->registeredErrorCallbacks)) {
            $callback = $this->registeredErrorCallbacks[$type];
            $reflection = new \ReflectionFunction($callback);
            $reflection->invokeArgs($args);
            return true;
        }
        // @codeCoverageIgnoreStart
        return false;
        // @codeCoverageIgnoreEnd
    }

    /**
     * @param \ReflectionClass $reflection
     * @return callable|null
     */
    private function findRegisteredExceptions(\ReflectionClass $reflection)
    {
        $exceptionClass = $reflection->getShortName();
        if (isset($this->registeredThrowableCallbacks[$exceptionClass])) {
            return $this->registeredThrowableCallbacks[$exceptionClass];
        }
        while ($parent = $reflection->getParentClass()) {
            if (isset($this->registeredThrowableCallbacks[$parent->getShortName()])) {
                return $this->registeredThrowableCallbacks[$parent->getShortName()];
            }
            $reflection = $parent;
        }
        return null;
    }

    /**
     * Mimics xdebug style of exception displaying table.
     *
     * @param \Throwable $error
     */
    private function printUnhandledException(\Throwable $error)
    {
        $traces = array_reverse($error->getTrace());
        $previousFile = "";
        $previousLine = 0; ?>
        <table>
            <tr><th align='left' bgcolor='#f57900' colspan="3"><span style='background-color: #cc0000; color: #fce94f; font-size: x-large;'>( ! )</span> <?= $error->getMessage() ?> in <?= $error->getFile() ?> on line <i><?= $error->getLine() ?></i></th></tr>
            <tr><th align='left' bgcolor='#e9b96e' colspan='3'>Call Stack</th></tr>
            <tr><th align='center' bgcolor='#eeeeec'>#</th><th align='left' bgcolor='#eeeeec'>Function</th><th align='left' bgcolor='#eeeeec'>Location</th></tr>
            <?php foreach ($traces as $i => $trace) { ?>
                <?php
                if (!empty($trace['file'])) {
                    $previousFile = $trace['file'];
                }
                if (!empty($trace['line'])) {
                    $previousLine = $trace['line'];
                }
                $filename = pathinfo($previousFile, PATHINFO_FILENAME);
                ?>
                <tr><td bgcolor='#eeeeec' align='center'><?= $i ?></td><td bgcolor='#eeeeec'><?= ((isset($trace['class'])) ? $trace['class'] : "") . ((isset($trace['type'])) ? $trace['type'] : "") . $trace['function'] ?>()</td><td title='<?= $previousFile ?>' bgcolor='#eeeeec'>.../<?= $filename ?><b>:</b><?= $previousLine ?></td></tr>
            <?php } ?>
        </table>
        <?php
    }

    /**
     * Made private to make sure to use Singleton pattern getInstance method.
     */
    private function __construct()
    {
    }
}