Любите загадки? Событие еще доступно на сайте.
Ищете работу? Мы поможем!
Ищете работу? Мы поможем!

Обработка ошибок (Exception)

Введение

Когда вы запускаете новый проект Laravel, обработка ошибок и исключений уже настроена для вас. Класс App\Exceptions\Handler – это то место, где все исключения, созданные вашим приложением, регистрируются и затем отображаются пользователю. В этой документации мы углубимся в этот класс.

Конфигурирование

Параметр debug в конфигурационном файле config/app.php определяет, сколько информации об ошибке фактически отобразится пользователю. По умолчанию этот параметр установлен, чтобы учесть значение переменной окружения APP_DEBUG, которая содержится в вашем файле .env.

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

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

Отчет об исключениях

Все исключения обрабатываются классом App\Exceptions\Handler. Этот класс содержит метод register, в котором вы можете зарегистрировать свои отчеты об исключениях и замыкания рендеринга. Мы подробно рассмотрим каждую из этих концепций. Отчеты об исключениях используются для регистрации исключений или отправки их во внешнюю службу, например Flare, Bugsnag или Sentry. По умолчанию исключения будут регистрироваться в соответствии с вашей конфигурацией логирования. Однако вы можете регистрировать исключения как хотите.

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

use App\Exceptions\InvalidOrderException;

/**
 * Зарегистрировать замыкания, обрабатывающие исключения приложения.
 */
public function register(): void
{
    $this->reportable(function (InvalidOrderException $e) {
        // ...
    });
}

Когда вы регистрируете собственные замыкания для создания отчетов об исключениях, используя метод reportable, Laravel по-прежнему регистрирует исключение, используя конфигурацию логирования по умолчанию для приложения. Если вы хотите остановить распространение исключения в стек журналов по умолчанию, вы можете использовать метод stop при определении замыкания отчета или вернуть false из замыкания:

$this->reportable(function (InvalidOrderException $e) {
    //
})->stop();

$this->reportable(function (InvalidOrderException $e) {
    return false;
});

Чтобы настроить отчет об исключениях для переданного исключения, вы можете рассмотреть возможность использования отчетных исключений.

Глобальное содержимое журнала

Если доступно, Laravel автоматически добавляет идентификатор текущего пользователя в каждое сообщение журнала исключения в качестве контекстных данных. Вы можете определить свои собственные глобальные контекстные данные, определив метод context класса App\Exceptions\Handler вашего приложения. Эта информация будет включена в каждое сообщение журнала исключения, написанное вашим приложением:

/**
 * Получить переменные контекста по умолчанию для ведения журнала.
 *
 * @return array<string, mixed>
 */
protected function context(): array
{
    return array_merge(parent::context(), [
        'foo' => 'bar',
    ]);
}

Контекст журнала исключений

Добавление контекста к каждому сообщению в журнале может быть полезным, но иногда у конкретного исключения может быть уникальный контекст, который вы хотели бы включить в журнал. Определив метод context в одном из исключений вашего приложения, вы можете указать любые данные, относящиеся к этому исключению, которые должны быть добавлены в журнал записи об исключении:

<?php

namespace App\Exceptions;

use Exception;

class InvalidOrderException extends Exception
{
    // ...

    /**
     * Получить контекстную информацию исключения.
     *
     * @return array<string, mixed>
     */
    public function context(): array
    {
        return ['order_id' => $this->orderId];
    }
}

Помощник report

По желанию может потребоваться сообщить об исключении, но продолжить обработку текущего запроса. Помощник report позволяет вам быстро сообщить об исключении через обработчик исключений, не отображая страницу с ошибкой для пользователя:

public function isValid(string $value): bool
{
    try {
        // Проверка `$value` ...
    } catch (Throwable $e) {
        report($e);

        return false;
    }
}

Исключения дубликатов

Если вы используете функцию report в вашем приложении, вы иногда можете сообщать об одном и том же исключении несколько раз, создавая дублирующие записи в журналах.

Если вы хотите гарантировать, что один и тот же экземпляр исключения будет сообщен только один раз, вы можете установить свойство $withoutDuplicates в true в классе App\Exceptions\Handler вашего приложения:

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    /**
     * Указывает, что экземпляр исключения должен быть сообщен только один раз.
     *
     * @var bool
     */
    protected $withoutDuplicates = true;

    // ...
}

Теперь, когда функция report вызывается с тем же экземпляром исключения, будет сообщено только первое вызов:

$original = new RuntimeException('Whoops!');

report($original); // сообщено

try {
    throw $original;
} catch (Throwable $caught) {
    report($caught); // проигнорировано
}

report($original); // проигнорировано
report($caught); // проигнорировано

Уровни журнала исключений

Когда сообщения записываются в журнал вашего приложения, сообщения записываются с указанным уровнем журнала, который указывает на серьезность или важность сообщения, которое записывается.

Как отмечено выше, даже когда вы регистрируете пользовательский обратный вызов сообщения об исключении с использованием метода reportable, Laravel все равно будет записывать исключение с использованием конфигурации регистрации журнала по умолчанию для приложения. Однако поскольку уровень журнала иногда может влиять на каналы, на которых записывается сообщение, вы можете настроить уровень журнала, на котором определенные исключения записываются.

Для этого вы можете определить свойство $levels в обработчике исключений вашего приложения. Это свойство должно содержать массив типов исключений и их соответствующих уровней журнала:

use PDOException;
use Psr\Log\LogLevel;

/**
 * Список типов исключений с соответствующими уровнями журнала.
 *
 * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
 */
protected $levels = [
    PDOException::class => LogLevel::CRITICAL,
];

Игнорирование исключений по типу

При построении вашего приложения могут возникать типы исключений, которые вы никогда не хотите сообщать. Чтобы игнорировать эти исключения, определите свойство $dontReport в обработчике исключений вашего приложения. Любые классы, которые вы добавите в это свойство, никогда не будут сообщаться, но они всё равно могут иметь собственную логику для отображения:

use App\Exceptions\InvalidOrderException;

/**
 * Список исключений, о которых не следует сообщать.
 *
 * @var array<class-string<\Throwable>>
 */
protected $dontReport = [
    SomeException::class,
    AnotherException::class,
];

Внутри Laravel уже есть механизм игнорирования некоторых типов ошибок, таких как исключения, вызванные ошибками HTTP 404 или ответами HTTP 419, генерируемыми из-за недействительных токенов CSRF. Если вы хотите указать Laravel прекратить игнорирование определенного типа исключения, вы можете вызвать метод stopIgnoring в методе register обработчика исключений вашего приложения:

use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * Зарегистрировать замыкания, обрабатывающие исключения приложения.
 */
public function register(): void
{
    $this->stopIgnoring(HttpException::class);

    // ...
}

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

По умолчанию обработчик исключений Laravel будет преобразовывать исключения в HTTP-ответ за вас. Однако вы можете зарегистрировать свое замыкание для отображения исключений конкретного типа. Вы можете сделать это с помощью метода renderable обработчика исключений.

Замыкание, переданное методу renderable, должно вернуть экземпляр Illuminate\Http\Response, который может быть сгенерирован с помощью функции response. Laravel определит, какой тип исключения отображает замыкание с помощью типизации аргументов:

use App\Exceptions\InvalidOrderException;
use Illuminate\Http\Request;

/**
 * Зарегистрировать замыкания, обрабатывающие исключения приложения.
 *
 * @return void
 */
public function register(): void
{
    $this->renderable(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', [], 500);
    });
}

Вы также можете использовать метод renderable чтобы переопределить отображение для встроенных исключений Laravel или Symfony, таких, как NotFoundHttpException. Если замыкание, переданное методу renderable не возвращает значения, будет использоваться отрисовка исключений Laravel по умолчанию:

use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * Зарегистрировать замыкания, обрабатывающие исключения приложения.
 */
public function register(): void
{
    $this->renderable(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });
}

Отчетные и отображаемые исключения

Вместо того чтобы определять настраиваемое поведение для создания отчетов и отображения ошибок в методе register обработчика исключений, вы можете напрямую определить методы report и render в самих классах исключений. Когда эти методы существуют, фреймворк автоматически будет вызывать их для обработки ошибок.

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class InvalidOrderException extends Exception
{
    /**
     * Отчитаться об исключении.
     */
    public function report() : void
    {
        // ...
    }

    /**
     * Преобразовать исключение в HTTP-ответ.
     */
    public function render(Request $request): Response
    {
        return response(/* ... */);
    }
}

Если ваше исключение расширяет исключение, которое уже доступно для визуализации, например встроенное исключение Laravel или Symfony, вы можете вернуть false из метода render исключения, чтобы отобразить HTTP-ответ исключения по умолчанию:

/**
 * Преобразовать исключение в HTTP-ответ.
 */
public function render(Request $request): Response|bool
{
    if (/** Определить, требуется ли для исключения пользовательское отображение */) {

        return response(/* ... */);
    }

    return false;
}

Если ваше исключение содержит пользовательскую логику отчетности, которая необходима только при выполнении определенных условий, то вам может потребоваться указать Laravel когда сообщать об исключении, используя конфигурацию обработки исключений по умолчанию. Для этого вы можете вернуть false из метода report исключения:

/**
 * Сообщить об исключении.
 */
public function report(): bool
{
    if (/** Определить, требуется ли для исключения пользовательское отображение */) {

        return true;
    }

    return false;
}

Вы можете указать любые требуемые зависимости метода report, и они будут автоматически внедрены в метод контейнером служб Laravel.

Ограничение на количество зарегистрированных исключений

Если ваше приложение регистрирует очень большое количество исключений, вам может потребоваться ограничить количество фактически регистрируемых или отправляемых во внешний сервис отслеживания ошибок.

Для выборочной выборки исключений вы можете вернуть экземпляр Lottery из метода throttle обработчика исключений. Если класс App\Exceptions\Handler не содержит этот метод, вы можете просто добавить его в класс:

use Illuminate\Support\Lottery;
use Throwable;

/**
 * Ограничивает поступающие исключения.
 */
protected function throttle(Throwable $e): mixed
{
    return Lottery::odds(1, 1000);
}

Также можно условно выбирать исключения на основе их типа. Если вы хотите выбирать только экземпляры конкретного класса исключений, вы можете вернуть экземпляр Lottery только для этого класса:

use App\Exceptions\ApiMonitoringException;
use Illuminate\Support\Lottery;
use Throwable;

/**
 * Ограничивает поступающие исключения.
 */
protected function throttle(Throwable $e): mixed
{
    if ($e instanceof ApiMonitoringException) {
        return Lottery::odds(1, 1000);
    }
}

Вы также можете ограничивать количество исключений, зарегистрированных или отправленных во внешний сервис отслеживания ошибок, вернув экземпляр Limit вместо Lottery. Это полезно, если вы хотите защититься от внезапных всплесков исключений, засоряющих ваши логи, например, когда сторонний сервис, используемый вашим приложением, недоступен:

use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

/**
 * Ограничивает поступающие исключения.
 */
protected function throttle(Throwable $e): mixed
{
    if ($e instanceof BroadcastException) {
        return Limit::perMinute(300);
    }
}

По умолчанию ограничения будут использовать класс исключения в качестве ключа ограничения по количеству. Вы можете настроить это, указав свой собственный ключ с помощью метода by на Limit:

use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Throwable;

/**
 * Ограничивает поступающие исключения.
 */
protected function throttle(Throwable $e): mixed
{
    if ($e instanceof BroadcastException) {
        return Limit::perMinute(300)->by($e->getMessage());
    }
}

Конечно же, вы можете возвращать смешанные экземпляры Lottery и Limit для разных исключений:

use App\Exceptions\ApiMonitoringException;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Lottery;
use Throwable;

/**
 * Ограничивает поступающие исключения.
 */
protected function throttle(Throwable $e): mixed
{
    return match (true) {
        $e instanceof BroadcastException => Limit::perMinute(300),
        $e instanceof ApiMonitoringException => Lottery::odds(1, 1000),
        default => Limit::none(),
    };
}

HTTP-исключения

Некоторые исключения описывают коды HTTP-ошибок с сервера. Например, это может быть ошибка «страница не найдена» (404), «неавторизованный доступ» (401) или даже ошибка 500, сгенерированная разработчиком. Чтобы создать такой ответ из любой точки вашего приложения, вы можете использовать глобальный помощник abort:

abort(404);

Пользовательские страницы для HTTP ошибок

Laravel позволяет легко отображать пользовательские страницы ошибок для различных кодов состояния HTTP. Например, если вы хотите настроить страницу ошибок для кодов HTTP-состояния 404, создайте файл resources/views/errors/404.blade.php. Это представление будет отображено для всех ошибок 404, сгенерированных вашим приложением. Шаблоны в этом каталоге должны быть названы в соответствии с кодом состояния HTTP, которому они соответствуют. Экземпляр Symfony\Component\HttpKernel\Exception\HttpException, вызванный функцией abort, будет передан в шаблон как переменная $exception:

<h2>{{ $exception->getMessage() }}</h2>

Вы можете опубликовать стандартные шаблоны страниц ошибок Laravel с помощью команды vendor:publish Artisan. После публикации шаблонов вы можете настроить их по своему вкусу:

php artisan vendor:publish --tag=laravel-errors

Запасные страницы для HTTP ошибок

Вы также можете определить “запасную” страницу ошибки для определенного набора кодов состояния HTTP. Эта страница будет отображаться, если нет соответствующей страницы для конкретного кода состояния HTTP, который произошел. Для этого определите шаблон 4xx.blade.php и шаблон 5xx.blade.php в директории resources/views/errors вашего приложения.