Как обрабатывать ошибки MVC PHP?

161
05 декабря 2019, 01:50

Всем привет. Пишу свою библиотеку для разработки приложений. Сейчас встал серьезный вопрос о том, как обрабатывать ошибки и исключения. Было решено использовать методы регистрации ошибок, фатальных ошибок и исключений. Заметил странность, что set_error_handler и register_shutdown_function при headers already sent by срабатывает дважды. Проблема сейчас заключается в том, что я не могу сделать редирект на страницу ошибки: Cannot modify header information - headers already sent by. Ошибку вызвал специально в view, чтобы проверить, то есть после рендеринга страницы.

А вообще задача стоит в том, чтобы отобразить либо страницу ошибки с подробностями, либо страницу ошибки без них.

Каким образом я могу решить проблему с дублированием ошибок и сделать редирект при необходимости?

<?php
namespace Core;
use Helpers\Config;
use Helpers\File;
use Helpers\Url;
/**
 * Class ErrorHandler
 * Обработчик ошибок и исключений
 * @package Сore
 */
class ErrorHandler
{
    /**
     * @var array Константа ассоциаций кода ошибок к тексту
     */
    const ERRORS = [
        E_ERROR => 'ERROR',
        E_WARNING => 'WARNING',
        E_PARSE => 'PARSE',
        E_NOTICE => 'NOTICE',
        E_CORE_ERROR => 'CORE_ERROR',
        E_CORE_WARNING => 'CORE_WARNING',
        E_COMPILE_ERROR => 'COMPILE_ERROR',
        E_COMPILE_WARNING => 'COMPILE_WARNING',
        E_USER_ERROR => 'USER_ERROR',
        E_USER_WARNING => 'USER_WARNING',
        E_USER_NOTICE => 'USER_NOTICE',
        E_STRICT => 'STRICT',
        E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
        E_DEPRECATED => 'DEPRECATED',
        E_USER_DEPRECATED => 'USER_DEPRECATED',
    ];
    public static $status;
    /**
     * Регистрация методов управления ошибками и исключениями.
     * Включение отображения ошибок
     */
    public static function register()
    {
        ini_set('display_errors', 1);
        error_reporting(E_ALL);
        # Управление ошибками
        set_error_handler([__CLASS__, 'errorHandler']);
        # Управление фатальными ошибками
        register_shutdown_function([__CLASS__, 'fatalErrorHandler']);
        # Управление исключениями
        set_exception_handler([__CLASS__, 'exceptionHandler']);
    }
    /**
     * Управление ошибками
     * @param $errno Код (номер) ошибки
     * @param $errstr Сообщение ошибки
     * @param $errfile Файл ошибки
     * @param $errline Строка в файле ошикбки
     * @return bool Предотвращение дальнейшего выполнения
     */
    public static function errorHandler($errno, $errstr, $errfile, $errline)
    {
        self::show($errno, $errstr, $errfile, $errline);
        # Не передается ошибка далее на обработку
        return true;
    }
    /**
     * Отображение ошибок на экран
     * @param $errno Код (номер) ошибки
     * @param $errstr Сообщение ошибки
     * @param $errfile Файл ошибки
     * @param $errline Строка в файле ошикбки
     */
    private static function show($errno, $errstr, $errfile, $errline)
    {
        http_response_code(self::$status);
        echo "[" . date('Y-m-d H:i:s') . "] <b>" . self::getErrorName($errno) .
            "</b> в файле {$errfile} на строке <b>{$errline}</b>:<br>{$errstr}<br>";
        $message = "[" . date('Y-m-d H:i:s') . "]" . self::getErrorName($errno) .
            " в файле {$errfile} на строке {$errline}:" . PHP_EOL . $errstr . PHP_EOL . PHP_EOL;
        self::writeLog($message);
    }
    /**
     * Получение наименования класса ошибки по ее коду
     * @param $error integer Номер ошибки
     * @return string Наименование класса ошибки
     */
    private static function getErrorName($error)
    {
        $error_name = $error;
        if (array_key_exists($error, self::ERRORS)) {
            $error_name = self::ERRORS[$error];
        }
        return $error_name;
    }

    private static function writeLog($message)
    {
        $errors_config_section = Config::getSettings('logs', 'errors');
        $log_file = File::createFile($errors_config_section['filename'], $errors_config_section['extension'], LOG);
        File::write($log_file, $message);
        header("Location: " . Url::to($errors_config_section['controller'], self::$status));
    }
    /**
     * Управление фатальными ошибками
     * @return bool Предотвращение дальнейшего выполнения
     */
    public static function fatalErrorHandler(int $status = 500)
    {
        self::$status = $status;
        if (!empty($error = error_get_last()) && $error['type'] && (E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR)) {
            ob_get_clean();
            self::show($error['type'], $error['message'], $error['file'], $error['line']);
        }
        return true;
    }
    /**
     * Управление исключениями
     * @param \Exception|\Error $ex Исключение
     * @return bool Предотвращение дальнейшего выполнения
     */
    public static function exceptionHandler($ex, int $status = 500)
    {
        self::$status = $status;
        self::show(get_class($ex), $ex->getMessage(), $ex->getFile(), $ex->getLine());
        return true;
    }
}
Answer 1

Непонятно, при чем тут редирект.

Редирект - это самое неправильное, что только можно сделать при ошибке.

Существует такая вещь, как коды возврата НТТР. Вещь чрезвычайно полезная и удобная.
Она сообщает клиенту, чем завершился его запрос.
Соответственно, обманывать клиента не следует. А следует отдавать ровно тот код возврата, который соответствует статусу страницы.
Если после завершения обработки запроса следует запросить другой урл - тогда отправляем код 3хх и заголовок Location:. Но если выполнение запроса завершилось с ошибкой, то единственно правильным кодом ответа будет 500, без каких-бы то ни было редиректов.

Код отображения страницы оибки отличается только проверкой переменной, которая отвечает за режим сайта - боевой или в разработке. И в зависимости от этот выводятся подобности, или нет. Код же возврата остается одним и тем же - 500, без редиректов.

Отлов ошибок при рендеринге - это один из немногих случаев, когда включение буфеиринга оправдано. Но не следует увлекаться буфферингом - как и любое сообщение об ошибке, Cannot modify header information служит для помощи программисту. И без нужды затыкать эту ошибку не следует. То есть, буфферинг включать только перед тем, как начинается непосредственно вывод во View

Answer 2

Вам нужно буферизовать рендеринг view с помощью ob_start() и выводить его в поток только если все ок. В этом случае никакого "headers already sent by" не произойдет.

READ ALSO
Как подружить nuxt(node) и php?

Как подружить nuxt(node) и php?

Собственно есть сервер на нем настроен apache+php и отдельно крутится на nodejs nuxt, стал вопрос, как увязать их вместеNUXT отвечает за фронтэнд и php за бекэнд...

153
Найти и удалить одинаковые слова в std::string

Найти и удалить одинаковые слова в std::string

Ввести 2 предложения, а затем исключить слова во втором предложении, которые имеются в первомПосле чего вывести полученное второе предложение

169