Отлов и обработка исключений в Yii2

    В Yii2 по-умолчанию все Exception обрабатываются, за это отвечает специальный обработчик. Если при обработке запроса возникает нехорошая ситуация (например, пришли некорректные данные от клиента), то можно выбросить исключение. Обработчик сформирует человекообразный ответ.

    Интересно, что в таком случае ошибка “Warning: Uncaught exception” в лог ошибок не выводится. Может создаться впечатление, что все исключения перехватываются средствами фреймворка. Но это не так. На наш проект некоторое время назад натравили средство мониторинга (в нашем случае New Relic), которое информацию обо всех выброшенных исключениях отображает в ошибках (именно как “Warning: Uncaught exception”), считает эти исключения необработанными. С этим надо было что-то делать.

    Ниже расскажу о схеме обработки исключений, которую в итоге выбрал. Вполне возможно, что кому-то это еще пригодится.


    Почему обработанные исключения считаются не пойманными


    В Yii2 обработчик ошибок задается функцией set_exception_handler(). Эта функция определяет обработчик для не пойманных исключений. При этом исключения хоть и обрабатываются, но остаются-таки не пойманными. Чтобы исключения считались пойманными, их все равно надо явно ловить, оборачивая вызовы в try-catch. В каждом экшне каждого контроллера делать этого очень не хотелось. Я считаю удобным иметь единую точку перехвата.

    В Yii2, как оказалось, для этого есть готовый вариант — если выбросить исключение yii\base\ExitException (или потомка от него), то такое исключение обрабатывается средствами фреймворка. Для наглядности, вот как это сделано в Application::run():

     public function run()
        {
            try {
    
                $this->state = self::STATE_BEFORE_REQUEST;
                $this->trigger(self::EVENT_BEFORE_REQUEST);
    
                $this->state = self::STATE_HANDLING_REQUEST;
                $response = $this->handleRequest($this->getRequest());
    
                $this->state = self::STATE_AFTER_REQUEST;
                $this->trigger(self::EVENT_AFTER_REQUEST);
    
                $this->state = self::STATE_SENDING_RESPONSE;
                $response->send();
    
                $this->state = self::STATE_END;
    
                return $response->exitStatus;
    
            } catch (ExitException $e) {
    
                $this->end($e->statusCode, isset($response) ? $response : null);
                return $e->statusCode;
    
            }
        }
    


    “Хорошие” и “плохие” исключения


    Мне удобно выбрасывать исключения с целью завершения обработки запроса в двух случаях.
    1. Если ничего не сломалось, просто имеет место мелкое недоразумение — пришел кривой веб-запрос на клиент или не нашлось каких-то не особо критичных запрашиваемых данных.
    2. Если что-то сломалось.

    В первом случае не надо логировать событие как ошибку и разбираться с этим не надо.
    Во втором случае надо логировать проблему, чтобы знать о случившемся и разбираться с проблемой.

    Для первого случая я создал такой класс, унаследованный от yii\base\ExitException. Чтобы результатом работы скрипта была не пустая страница, прямо в исключении генерируется ответ.

    <?php
    
    namespace app\components;
    
    use yii;
    use yii\base\ExitException;
    
    /**
     * Исключение, которое будет автоматически обрабатываться на уровне yii\base\Application
     */
    class GoodException extends ExitException
    {
        /**
         * Конструктор
         * @param string $name Название (выведем в качестве названия страницы)
         * @param string $message Подробное сообщение об ошибке
         * @param int $code Код ошибки
         * @param int $status Статус ответа
         * @param \Exception $previous Предыдущее исключение
         */
        public function __construct($name, $message = null, $code = 0, $status = 500, \Exception $previous = null)
        {
            # Генерируем ответ
            $view = yii::$app->getView();
            $response = yii::$app->getResponse();
            $response->data = $view->renderFile('@app/views/exception.php', [
                'name' => $name,
                'message' => $message,
            ]);
    
            # Возвратим нужный статус (по-умолчанию отдадим 500-й)
            $response->setStatusCode($status);
    
            parent::__construct($status, $message, $code, $previous);
        }
    }
    

    А также создано еще представление.
    <?php
    
    /* @var $this yii\web\View */
    /* @var $name string */
    /* @var $message string */
    /* @var $exception Exception */
    
    use yii\helpers\Html;
    
    $this->title = $name;
    ?>
    
    <?php $this->beginContent('@app/views/layouts/main.php'); ?>
    <div class="site-error">
    
        <h1><?= Html::encode($this->title) ?></h1>
    
        <div class="alert alert-danger">
            <?= nl2br(Html::encode($message)) ?>
        </div>
    
        <p>
            The above error occurred while the Web server was processing your request.
        </p>
        <p>
            Please contact us if you think this is a server error. Thank you.
        </p>
    
    </div>
    <?php $this->endContent(); ?>
    


    Итого


    Таким образом, чтобы выбросить “культурное” исключение, пишем:
    # Выбрасываем исключение, которое будет поймано
    throw new GoodException('Проблемка', 'Эта проблема аккуратно обрабатывается');
    

    Такие исключения будут перехвачены и на клиент вернется аккуратный ответ. В лог ошибок такие события попадать не будут.

    Все остальные исключения, если вы их явно не поймаете, ловиться не будут. И будут попадать в ошибки. Т.е. для второго случая можно писать
    throw new yii\base\ErrorException('Эта проблема критичная');
    
    Support the author
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      +3
      Т.е. для того, чтобы в NewRelic не было Unhandled Exception вы решили сделать GoodException с вьюхами вместо того, чтобы написать ErrorHandler, в котором указывать, какие ошибки кидать в ньюрелик, а какие — нет?
        +2
        А для того, чтобы юзеру выводился красивый ответ нужно 2 вещи:
        1. Кидать ошибки-наследники UserException
        2. Почитать доку по отлову ошибок — www.yiiframework.com/doc-2.0/guide-runtime-handling-errors.html — там как раз есть раздел «Customizing Error Display»
          0
          Эта дока про вывод ошибок. Если ошибку не ловить, но аккуратно выводить через ErrorHandler, она так и остается непойманной. Соответственно, можно выбросить «GoodException», если ошибка не критичная. А если с ошибкой надо разбираться, то можно выбросить и не ловить другое исключение — HttpServerException и т.п. ErrorHandler его обработает, но оно будет непойманным.
            +2
            Я даже специально 2 разных комментария оставил — про «непойпанность», и отдельно — про отображение. Почему ответили-то только на один? :)
            Чем вас смущает то, что ошибка «не поймана»? Не нравится, когда в ньюрелике отображается куча 400/404-х? Вот — как раз в ErrorHandler'е есть волшебный метод handleException() внутри которого очень удобно решать — посылать ошибку в ньюрелик, или не нужно.
              0
              Мы не решаем, что посылать в ньюрелик, а что — нет.
              Все, что мы делаем для ньюрелика — вызываем в index.php
              if (extension_loaded('newrelic')) {
              	newrelic_set_appname('projectname');
              }
              

              А дальше неперехваченные исключения сами попадают в статистику, что нас и смущало
                0
                Вы не подумайте, что я это всё из вредности писал — просто у меня у самого проект с Yii2+newrelic под боком — и никаких не отловленных исключений там нет — мы сами всё в ньюрелик репортим, что нам нужно.

                Оказывается в агенте от 8 июля внесли соответствующие изменения, добавляющие не отловленные исключения. У нас более старая версия.
        0
        Пришёл ответ от техподдержки. Для того, чтобы отключить отлов ошибок — нужно добавить вот такую строку в конфиг php:
        newrelic.special.disable_instrumentation = restore_exception_handler,set_exception_handler
          0
          Отлов ошибок полезен. Если случился Warning или Notice, то тоже будет не пойманной исключение. Если возникло исключение внутри фреймворка, о котором разработчик не позаботился, то такие события тоже стоит фиксировать в мониторинге. На мой взгляд, наиболее удачный подход — самостоятельно контролировать, что будет ошибкой, а что — нет. При этом все нехорошие события, о которых разработчик не позаботился, автоматически считать ошибками. Это на мой взгляд.
            0
            Дык я не спорю, что он полезен. Все не отловленные ошибки попадают в стандартный Yii'шный ErrorHandler, а дальше уже с ними можно делать всё, что угодно: посылать в ньюрелик, не посылать в ньюрелик, нарисовать юзеру красивую страницу, и т.д.

            Ваш подход меня пугает как минимум тем, что вы внутри эксепшена меняете состояние системы. Ошибки же вообще ничего не должны менять — иначе может стать ещё хуже.
              0
              Меняю состояние в смысле возвращаю 500 ошибку? На мой взгляд это нормально. В стандартном ErrorHandler происходит примерно то-же самое.

              public function handleException($exception)
                  {
                      if ($exception instanceof ExitException) {
                          return;
                      }
              
                      $this->exception = $exception;
              
                      // disable error capturing to avoid recursive errors while handling exceptions
                      $this->unregister();
              
                      // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
                      // HTTP exceptions will override this value in renderException()
                      if (PHP_SAPI !== 'cli') {
                          http_response_code(500);
                      }
              ...
              
                0
                Дык разницу-то заметьте — не внутри конструктора Exception'а, а в обработчике ошибок.
                  0
                  На мой взгляд это вполне нормально. Если закралась ошибка в самодельном исключении, то это событие тоже окажется в обработчике ошибок.
                  Но ваш подход тоже интересен — взять и убрать из мониторинга то, что не хочется там видеть ;) Мы может быть тоже рады были бы так сделать, но у нас так не принято.

        Only users with full accounts can post comments. Log in, please.