#php #ооп
Хочу разобраться как делать правильно. Сейчас у меня в коде обработчик ошибок (ErrorHandler) обрабатывает как внутренние ошибки скрипта, так и исключения, которые я сам создаю. Выглядит этот так: class ErrorHandler { public static function register() { ini_set('display_errors', 'on'); error_reporting(E_ALL); set_error_handler(get_class() . "::showError"); register_shutdown_function(get_class() . "::catch_fatal_error"); ob_start(); } /** * @param int $errno * @param string $errstr * @param string $file * @param int $line * @param string $header */ public static function showError($errno, $errstr, $file, $line, $header = "HTTP/1.0 404 Not Found") {/*здесь отправляю ответ об ошибке*/} public static function catch_fatal_error() { /*здесь отправляю ответ о критической ошибке при помощи все того же метода self::showError*/ } } Весь код приложения заключен в блок try{} и исключение обрабатывается при помощи все того же метода: try{ //Здесь весь код моего приложения //в нем встречаются такие выбросы ошибок: throw new \Exception("Сообщение об ошибке."); } catch(\Exception $e){ //обрабатывается исключение при помощи все того же метода класса ErrorHandler ErrorHandler::showError($e->getCode(), $e->getMessage(), $e->getFile(), $e->getLine()); } Вопросы: Можно ли так делать? Какой наиболее приемлемый вариант работы с ошибками и исключениями, если мой неверный? Вопрос возник потому, что мой знакомый, которого я считаю более продвинутым чем я, говорит, что так как у меня делать нельзя. Но я не могу понять почему. Что не так? Некоторые важные моменты, которые я понял: Когда возникает фатальная ошибка, то она автоматически попадает в буфер вывода. Это означает, что мы можем сразу его удалять и НЕ создавать предварительно вручную функцией ob_start();, если, конечно, хотим обрабатывать фатальные ошибки тоже. Что не рекомендуется делать. Директива ini_set('display_errors', 'on'); в моем коде лишняя, т.к. я сам решаю, что делать с ошибками и мне не нужно сообщать php, что ему делать с ними. Я уже сам решу. А остальным пусть занимаются базовые настройки сервера. Отправлять ответ пользователю внутри обработчика ошибок допустимо. Т.к. другие механизмы ответа, предусмотренные приложением, могут быть недоступны как раз из-за этих ошибок. Встроенный обработчик именно это делает - он прерывает в месте ошибки ход программы и отправляет ответ пользователю.
Ответы
Ответ 1
В целом ваша механика отлова ошибок верная, потому мне не очень понятна критика вашего товарища. Было бы здорово взглянуть на его решение. Теперь по существу. Собственно, класс обработчик ошибок: class ErrorHandler { protected $format = '{{message}} {{class}}::{{method}} {{file}} on line {{line}}'; /** * @var HandlerInterface */ protected $displayHandler; /** * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that * when an out-of-memory issue occurs, the error handler is able to handle the error with * the help of this reserved memory. If you set this value to be 0, no memory will be reserved. * Defaults to 256KB. */ protected $memoryReserveSize = 262144; /** * @var string Used to reserve memory for fatal error handler. */ private $_memoryReserve; /** * Register this error handler. */ public function register() { // Catch errors set_error_handler([$this, 'handleError']); if ($this->memoryReserveSize > 0) { $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize); } // Start buffer ob_start(); // Catch fatal errors register_shutdown_function([$this, 'handleShutdown']); } /** * Unregisters this error handler by restoring the PHP error handlers. */ public function unregister() { restore_error_handler(); } /** * Error handler. * * @param int $code * @param string $msg * @param string $file * @param int $line * @return bool * @throws \ErrorException */ public function handleError($code, $msg, $file, $line) { if (~error_reporting() & $code) { return false; } switch ($code) { case E_USER_WARNING: case E_WARNING: $exception = new \ErrorException("[E_WARNING] {$msg}", Log::WARNING, $code, $file, $line); break; case E_USER_NOTICE: case E_NOTICE: case E_STRICT: $exception = new \ErrorException("[E_NOTICE] {$msg}", Log::NOTICE, $code, $file, $line); break; case E_RECOVERABLE_ERROR: $exception = new \ErrorException("[E_CATCHABLE] {$msg}", Log::ERROR, $code, $file, $line); break; default: $exception = new \ErrorException("[E_UNKNOWN] {$msg}", Log::CRITICAL, $code, $file, $line); } throw $exception; } /** * Fatal handler. * * @return void */ public function handleShutdown() { unset($this->_memoryReserve); $error = error_get_last(); if ( isset($error['type']) && ($error['type'] == E_ERROR || $error['type'] == E_PARSE || $error['type'] == E_COMPILE_ERROR || $error['type'] == E_CORE_ERROR) ) { $type = ""; switch ($error['type']) { case E_ERROR: $type = '[E_ERROR]'; break; case E_PARSE: $type = '[E_PARSE]'; break; case E_COMPILE_ERROR: $type = '[E_COMPILE_ERROR]'; break; case E_CORE_ERROR: $type = '[E_CORE_ERROR]'; break; } $exception = new \ErrorException("$type {$error['message']}", Log::CRITICAL, $error['type'], $error['file'], $error['line']); if (APP_LOG) { Log::log(Log::CRITICAL, $this->convertExceptionToString($exception)); } $this->display($exception); } else { if (ob_get_length() !== false) { // Display buffer, complete work buffer ob_end_flush(); } } } /** * Sets a display handler. * @param HandlerInterface $handler */ public function setDisplayHandler(HandlerInterface $handler) { $this->displayHandler = $handler; } /** * Sets a format message log. * @param string $format */ public function setFormat($format) { $this->format = $format; } /** * Sets a size memory. * @param int $size */ public function setMemoryReserve($size) { $this->memoryReserveSize = $size; } /** * @param \Exception $exception */ public function display(\Exception $exception) { // display Whoops if (APP_DEBUG === true) { if (!isset($this->displayHandler)) { $this->displayHandler = new PrettyPageHandler(); } $run = new Run(); $run->pushHandler($this->displayHandler); $run->handleException($exception); return; } die('This site is temporarily unavailable. Please, visit the page later.'); } /** * Converts an exception into a simple string. * * @param \Exception $exception the exception being converted * @return string the string representation of the exception. */ public function convertExceptionToString(\Exception $exception) { $trace = $exception->getTrace(); $placeholders = [ '{{class}}' => isset($trace[0]['class']) ? $trace[0]['class'] : '', '{{method}}' => isset($trace[0]['function']) ? $trace[0]['function'] : '', '{{message}}' => $exception->getMessage(), '{{file}}' => $exception->getFile(), '{{line}}' => $exception->getLine(), ]; return strtr($this->format, $placeholders); } } Класс может быть и статическим (т.е. содержать статические методы и свойства). Не имеет значения. За отображение исключений и ошибок в красивом интерфейсе отвечает вполне известная библиотека Whoops, а логирование производится с помощью Monolog (здесь представлен лёгенький wrapper над ней) Существуют различные нюансы, к примеру, для отлова фатала связанного с переполнением памяти (Allowed memory size of...), необходимо зарезервировать некий объём. Опытным путём был рассчитан объём в 256KB. Об этом я узнал на одной из конференции от Александра samdark Макарова (евангелист и мэйнтейнер фреймворка yii). Собственно, в обработчике фреймворка подобный хак имеется. Если проанализировать код обработчика yii2, то можно заментить другие нюансы связанные, к примеру, с обработкой ошибок/исключений HHVM. В единой точке входна в ваше приложение (index.php) указываем следующее: defined('APP_DEBUG') or define('APP_DEBUG', true); defined('APP_LOG') or define('APP_LOG', true); $errorHandler = new ErrorHandler; $errorHandler->register(); try { // ... bootstrap вашего приложения } catch (\Exception $e) { if (APP_LOG) { $msg = $errorHandler->convertExceptionToString($e); Log::log($e->getCode() ? : Log::CRITICAL, $msg); } $errorHandler->display($e); } В самом приложение можно ловить локальные исключения и делать с ними всё что угодно. К примеру, ловить и тут же логировать: try { // ... некая локальная логика приложения } catch (\Exception $e) { Log::log(Log::ERROR, (new ErrorHandler())->convertExceptionToString($e)); } Так или иначе, любое выбрашенное исключение будет отловлено в index.php и в зависимости от заданных констант (APP_DEBUG и APP_LOG) представленно в красивом интерфейсе и добавлено в лог. Естественно на продакшене стоит обязательно отключить режим дебага. По поводу обработчиков Whoops: Зная в каком формате (content type) вам необходимо отдать данные пользователю, можно выбрать подходящий обработчик. К примеру, проверка является ли запрос ajax-запросом if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { $errorHandler->setDisplayHandler(new JsonResponseHandler()); } Дебаг-информация будет представлена в виде json-а, что удобно для просмотра через консоль браузера. В идеале необходимо иметь HTTP прослойку (классы Response/Request) и всё это детектить на уровне роутинга или фильтров/поведений контроллера - ContentNegotiator или что-то подобное. P.S. Посмотрите видео холивара про оператор подавления ошибок @ с devconf. Очень занимательно. UPDATE Я лично в проектах стараюсь возвращать верные http-статусы. Вам никто не мешает выдать пользовотелю специально оформленную 404-страницу с аналогичным статусом. if (headers_sent()) { // проверка: не отправлены ли уже заголовоки return; } $version = isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0' ? '1.0' : '1.1' $statusCode = 404; $statusText = 'Not Found'; header("HTTP/$version $statusCode $statusText"); Как я уже отмечал ранее, лучше всего использовать класс-прослойку Response над нативными функциями. С роутингом этого добиться досточно просто: $route = new Route(); $route->get('/items/{id:\d+}/', ['\namespace\ItemsController', 'actionOne']); $route->post('/items/', ['\namespace\ItemsController', 'actionCreate']); //... другие правила $route->any('*', ['\namespace\ItemsController', 'actionNotFound']); // т.е. если вышеприведенные правила не выполнились, то выполнится последнее правило. Для ajax-запрос, достаточно в ответе указать заголовок без тела: $route->any('*', function(Route $route) { $route->response->status404(); return $route->response; }); 403 - если доступ к ресурсу (по какому-то URL-у) запрещен для неавторизованного пользователя или пользователя с иной ролью/правами, к примеру, не имеющего права администратора. Посмотрите RBAC. В yii подобный механизм реализован через поведения/фильтры к экшенам контроллера. 201 - ресурс успешно создан. К примеру, можно выдавать, когда зарегистрирован пользователь, создан комментарий или пост. 204 - запрос к БД завершился успешно, но данные по какой-то причине отсутствуют. К примеру, открылся новый раздел на сайте, но статьи для него ещё не написаны. 422 я использую, если не прошла валидация данных, к примеру формы. В данном случае, через $statusText (см. выше), можно дать пояснение: Validation failure. 429 - классический rate limiter. Чаще всего применятеся для REST API. При достижении определённого лимита на количтво запросов выдавать данный статус. 302 - при редиректе. К примеру, если данные пришедшие от формы верны и запись в БД успешно произведена, то проивзодится редирект на стороннюю страницу, либо рефреш текущий страницы с данным статус. 500 - если вы могли заметить, то в ErrorHandler имеется die('This site is temporarily unavailable. Please, visit the page later.');. Вы можете вместо этой "пресной" записи выдать пользователю статичную страницу-заглушку предварительно указав http-статус 500. die(file_get_contents('/path/to/stub.html')); Для реализации своего RESTful API указание верныx http-методов, статусов, заголовков (почитайте про HATEOAS) является обязательных. Полный список статусов Таким образом, в режиме дебага у вас будет выводится Whoops со стек-трейсом, а на продакшене (боевом сервере) пользователю будет показана заглушка. Логировать или нет ошибки/исключения на ваше усмотрение. К примеру, во многих фреймворках по умолчанию производлится "информационное" логирование (level Log::INFO), т.е. каждое соединие с БД, успешность транзакций, авторизация пользователя и т.д., что хорошенько может так забить ваш накопитель на сервере. Необходимо повышать уровень логирования ошибок до Log::WARNING или Log::ERROR, а также использоватать штатные утилиты по ротации логов. К примеру, в Linux это logrotate. Коль я затронул тут множество тем, то позвольте прорекламировать мои open source библиотеки: Rock Route - роутинг с гибкими правилами, группировкой (разделение правил на пространства/модули для, к примеру, ajax, backend/админка и т.д., как в laravel) и поддержкой REST. Алгоритм реализации облегчённых regexp-паттернов был взят из библиотеки FastRoute Никиты Попова (входит в core team PHP). Имеется подробная документация. Rock Response - форк yii2 response, который полностью отделён от фреймворка. Документации, к сожалению нет, но существует официальная документации yii. Правда, тоже пока короткая. Как мы знаем yii2 является монолитным фреймворком. Rock Request - аналогичный форк, но с моей библиотекой фильтрации данный. Все указанные библиотеки идут без лишних зависимостей - всё только самое необходимое для их работы.Ответ 2
В целом нормально, только два замечания. Заключать весь код в try catch бессмысленно и вредно. Вредно потому, что try в принципе вообще никогда не должен использоваться для показа ошибки на экране - РНР это прекрасно умеет и сам. А бессмысленно потому, что и без этого оператора ошибка прекрасно поймается. Никаких безусловных showerror вообще в принципе быть не должно. Пользователь не должен видеть системные ошибки вообще никогда. То есть показ ошибки на экране может произойти только в двух случаях: Единственным пользователем сайта является сам программист - то есть на сервере разработки. В этом случае ошибки можно и нужно выводить на экран. За это должна отвечать единственная директива настройки - та, которая определяет, на боевом мы сервере, или нет. Если исключение было брошено программистом, и текст ошибки специально предназначен для показа пользователю. Для этого можно создать отдельный класс исключений, и показывать сообщение об ошибке только если исключение относится к этому классу. Во всех остальных случаях никакого вывода на экран, кроме стандартных извинений и просьбы зайти позже, быль не должно. Вопрос про НТТР заголовки не имеет прямого отношения к обработке ошибок и исключений, но ответ на него очевиден: НТТР статус всегда должен отражать текущий статус сервера. Если это ошибка сервера, то статус должен быть 5хх. Если страница не найдена, то 404. Если доступ запрещен - то 403. И так далее.
Комментариев нет:
Отправить комментарий