strata-mvc/strata

View on GitHub
src/Error/BaseErrorHandler.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace Strata\Error;

use Strata\Strata;
use Strata\Router\Router;
use Strata\Core\StrataConfigurableTrait;
use Strata\Logger\Debugger;

use Exception;

/**
 * The base error handler of Strata code. This is very much so based on
 * CakePHP's method.
 * @link https://github.com/cakephp/cakephp/blob/master/src/Error/BaseErrorHandler.php
 */
class BaseErrorHandler
{
    use StrataConfigurableTrait;

    private $hasError = false;

    /**
     * Checks the passed exception type. If it is an instance of `Error`
     * then, it wraps the passed object inside another Exception object
     * for backwards compatibility purposes.
     *
     * @param \Exception|\Error $exception The exception to handle
     * @return void
     */
    public function wrapAndHandleException($exception)
    {
        if ($exception instanceof Error) {
            $exception = new Exception($exception);
        }

        $this->handleException($exception);
    }

    /**
     * Register the error and exception handlers.
     * @return void
     */
    public function register()
    {
        $debugLevel = $this->getDebugLevel();

        if ($this->shouldBeDebugging()) {
            ob_start();
            register_shutdown_function(function() {
                if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) {
                    return;
                }

                $error = error_get_last();
                if (!is_array($error)) {
                    return;
                }

                if (!in_array($error['type'], $this->getFatalErrorsTypes(), true)) {
                    return;
                }

                if (!$this->canCatchTheError($error['file'])) {
                    return;
                }

                $this->handleFatalError(
                    $error['type'],
                    $error['message'],
                    $error['file'],
                    $error['line']
                );
            });
            set_error_handler(array($this, 'handleError'), $debugLevel);
            set_exception_handler(array($this, 'wrapAndHandleException'));

            error_reporting($debugLevel);
            ini_set($debugLevel);
        }
    }

    /**
     * Set as the default error handler by Strata
     * @param int $code Code of error
     * @param string $description Error description
     * @param string|null $file File on which error occurred
     * @param int|null $line Line that triggered the error
     * @param array|null $context Context
     * @return bool True if error was handled
     */
    public function handleError($code, $description, $file = null, $line = null, $context = null)
    {
        if (!$this->canCatchTheError($file)) {
            return;
        }

        $data = array(
            'type' => "Error",
            'code' => $code,
            'description' => $description,
            'file' => $file,
            'line' => $line,
            'context' => $context,
            'trace' => Debugger::trace(debug_backtrace()),
        );

        $this->logErrorData($data);
        $this->displayErrorData($data);
        $this->endProcesses();
    }

    /**
     * Logs a fatal error.
     * @param int $code Code of error
     * @param string $description Error description
     * @param string $file File on which error occurred
     * @param int $line Line that triggered the error
     */
    public function handleFatalError($code, $description, $file, $line)
    {
        if (!$this->canCatchTheError($file)) {
            return;
        }

        $data = array(
            'type' => "Fatal Error",
            'code' => $code,
            'description' => $description,
            'file' => $file,
            'line' => $line,
            'context' => null,
            'trace' => Debugger::trace(debug_backtrace()),
        );

        $this->logErrorData($data);
        $this->displayErrorData($data);
        $this->endProcesses();
    }

    /**
     * Handle uncaught exceptions.
     *
     * Uses a template method provided by subclasses to display errors in an
     * environment appropriate way.
     *
     * @param \Exception $exception Exception instance.
     * @return void
     * @throws \Exception When renderer class not found
     * @see http://php.net/manual/en/function.set-exception-handler.php
     */
    public function handleException($exception)
    {
        if (!$this->canCatchTheError($exception->getFile())) {
            return;
        }

        $data = array(
            'type' => "Exception",
            'code' => $exception->getCode(),
            'description' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'context' => null,
            'trace' => Debugger::trace($exception->getTrace()),
        );

        $this->displayExceptionData($data);
        $this->logExceptionData($data);
        $this->endProcesses();
    }

    /**
     * Displays an error.
     * @param array $data Array of error data.
     */
    protected function displayErrorData($data)
    {
        $this->raiseDebuggerTakeover();

        $debug = new ErrorDebugger();
        $debug->setErrorData($data);
        echo $debug->compile();
    }

    /**
     * Displays an exception.
     * @param array $data Array of error data.
     */
    protected function displayExceptionData($data)
    {
        $this->raiseDebuggerTakeover();

        $debug = new ErrorDebugger();
        $debug->setErrorData($data);
        echo $debug->compile();
    }

    /**
     * Log an error.
     * @param  array  $errorDetails Array of error data.
     */
    protected function logErrorData(array $errorDetails)
    {
        $logger = new ErrorLogger();
        $logger->logError($errorDetails);
    }

    /**
     * Log an exception.
     * @param array $exception
     */
    protected function logExceptionData(array $errorDetails)
    {
        $logger = new ErrorLogger();
        $logger->logError($errorDetails);
    }

    private function getDebugLevel()
    {
        if ($this->hasConfig("error.debug_level")) {
            return $this->getConfig("error.debug_level");
        }

        return E_ALL;
    }

    private function getFatalErrorsTypes()
    {
        return array(
            E_ERROR,
            E_PARSE,
            E_CORE_ERROR,
            E_COMPILE_ERROR,
            E_USER_ERROR,
        );
    }

    private function clearBuffer()
    {
        while (ob_get_level()) {
            ob_get_clean();
        }
    }

    private function useStrataDebugger()
    {
        if ($this->hasConfig("error.use_debugger")) {
            return (bool)$this->getConfig("error.use_debugger");
        }

        return true;
    }

    private function shouldBeDebugging()
    {
        return $this->useStrataDebugger() &&
                Strata::isDev() &&
               !Strata::isCommandLineInterface();
    }

    private function endProcesses()
    {
        if (function_exists('do_action')) {
            do_action('shutdown');
        }

        die();
    }

    private function canCatchTheError($triggerFilePath = '')
    {
        if (function_exists("is_admin") && is_admin()) {
            if (!class_exists("\Strata\Router\Router")) {
                return false;
            }

            if (!Router::isAjax()) {
                return false;
            }
        }

        if (strstr($triggerFilePath, Strata::getPluginsPath()) || strstr($triggerFilePath, Strata::getWordpressPath())) {
            return false;
        }

        if (!function_exists('get_template_directory')) {
            return false;
        }

        return true;
    }

    private function raiseDebuggerTakeover()
    {
        $this->hasError = true;
        $this->clearBuffer();
        $router = Strata::router();
        if ($router) {
            $controller = Strata::router()->getCurrentController();
            if (!is_null($controller)) {
                $controller->serverError();
            }
        }
    }

}