Pull to refresh

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

Website development *PHP *Programming *Yii *
В 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('Эта проблема критичная');
Tags:
Hubs:
Total votes 14: ↑11 and ↓3 +8
Views 33K
Comments 12
Comments Comments 12

Posts