Страницы

Поиск по вопросам

среда, 31 октября 2018 г.

Исключения и ошибки в 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, что ему делать с ними. Я уже сам решу. А остальным пусть занимаются базовые настройки сервера. Отправлять ответ пользователю внутри обработчика ошибок допустимо. Т.к. другие механизмы ответа, предусмотренные приложением, могут быть недоступны как раз из-за этих ошибок. Встроенный обработчик именно это делает - он прерывает в месте ошибки ход программы и отправляет ответ пользователю.


Ответ

В целом ваша механика отлова ошибок верная, потому мне не очень понятна критика вашего товарища. Было бы здорово взглянуть на его решение.
Теперь по существу. Собственно, класс обработчик ошибок:
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+}/', ['
amespace\ItemsController', 'actionOne']); $route->post('/items/', ['
amespace\ItemsController', 'actionCreate']); //... другие правила $route->any('*', ['
amespace\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 - аналогичный форк, но с моей библиотекой фильтрации данный
Все указанные библиотеки идут без лишних зависимостей - всё только самое необходимое для их работы.

Комментариев нет:

Отправить комментарий