Подписывайтесь на наш Telegram канал и будьте в курсе всех событий.
Примите наш вызов и улучшите свои навыки!
Примите наш вызов и улучшите свои навыки!

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

Введение

Когда вы запускаете новый проект Laravel, обработка ошибок и исключений уже настроена для вас; однако в любой момент вы можете использовать метод withExceptions в файле bootstrap/app.php вашего приложения, чтобы управлять тем, как ваше приложение сообщает об исключениях и обрабатывает их.

Объект $exceptions, предоставляемый замыканию withExceptions, является экземпляром Illuminate\Foundation\Configuration\Exceptions и отвечает за управление обработкой исключений в вашем приложении. В этой документации мы углубимся в этот объект.

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

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

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

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

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

В Laravel отчеты об исключениях используются для регистрации исключений или их отправки во внешнюю службу Sentry или Flare. По умолчанию исключения будут регистрироваться на основе вашей конфигурации logging. Однако вы можете регистрировать исключения по своему усмотрению.

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    });
})

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    })->stop();

    $exceptions->report(function (InvalidOrderException $e) {
        return false;
    });
})

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

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

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->context(fn () => [
        '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 в вашем приложении, вы иногда можете сообщать об одном и том же исключении несколько раз, создавая дублирующие записи в журналах.

Если вы хотите, чтобы об одном экземпляре исключения сообщалось только один раз, вы можете вызвать метод исключения dontReportDuplicates в файле bootstrap/app.php вашего приложения:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->dontReportDuplicates();
})

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

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

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

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

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

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

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

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

Для этого вы можете использовать метод исключения level в файле bootstrap/app.php вашего приложения. Этот метод получает тип исключения в качестве первого аргумента и уровень журнала в качестве второго аргумента:

use PDOException;
use Psr\Log\LogLevel;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->level(PDOException::class, LogLevel::CRITICAL);
})

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

При создании приложения могут возникнуть некоторые типы исключений, о которых вы никогда не захотите сообщать. Чтобы игнорировать эти исключения, вы можете использовать метод исключений dontReport в файле bootstrap/app.php вашего приложения. Ни о каком классе, предоставленном этому методу, никогда не будет сообщено; однако они все равно могут иметь собственную логику рендеринга:

use App\Exceptions\InvalidOrderException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->dontReport([
        InvalidOrderException::class,
    ]);
})

В качестве альтернативы вы можете просто «пометить» класс исключений с помощью интерфейса Illuminate\Contracts\Debug\ShouldntReport. Когда исключение помечено этим интерфейсом, обработчик исключений Laravel никогда не сообщит об этом:

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Contracts\Debug\ShouldntReport;

class PodcastProcessingException extends Exception implements ShouldntReport
{
    //
}

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

use Symfony\Component\HttpKernel\Exception\HttpException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->stopIgnoring(HttpException::class);
})

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

По умолчанию обработчик исключений Laravel преобразует исключения в HTTP-ответ. Однако вы можете зарегистрировать свое замыкание для исключений заданного типа. Вы можете добиться этого, используя метод исключения render в файле bootstrap/app.php вашего приложения.

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

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})

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

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Record not found.'
            ], 404);
        }
    });
})

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

When rendering an exception, Laravel will automatically determine if the exception should be rendered as an HTML or JSON response based on the Accept header of the request. If you would like to customize how Laravel determines whether to render HTML or JSON exception responses, you may utilize the shouldRenderJsonWhen method: При обработке исключения Laravel автоматически определяет, должно ли исключение быть отображено в виде ответа HTML или JSON, на основе заголовка Accept запроса. Если вы хотите настроить, как Laravel определяет, следует ли отображать ответы об исключениях HTML или JSON, вы можете использовать метод shouldRenderJsonWhen:

use Illuminate\Http\Request;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
        if ($request->is('admin/*')) {
            return true;
        }

        return $request->expectsJson();
    });
})

Настройка ответа на исключение

В редких случаях вам может потребоваться настроить весь HTTP-ответ, отображаемый обработчиком исключений Laravel. Для этого вы можете зарегистрировать закрытие настройки ответа, используя метод respond:

use Symfony\Component\HttpFoundation\Response;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->respond(function (Response $response) {
        if ($response->getStatusCode() === 419) {
            return back()->with([
                'message' => 'The page expired, please try again.',
            ]);
        }

        return $response;
    });
})

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

Вместо того чтобы настраивать пользовательское поведение отчетов и отображение ошибок в файле bootstrap/app.php вашего приложения, вы можете определить методы 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.

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

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

Чтобы получить случайную частоту выборки исключений, вы можете использовать метод исключений throttle в файле bootstrap/app.php вашего приложения. Метод throttle получает замыкание, которое должно возвращать экземпляр Lottery:

use Illuminate\Support\Lottery;
use Throwable;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        return Lottery::odds(1, 1000);
    });
})

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

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof ApiMonitoringException) {
            return Lottery::odds(1, 1000);
        }
    });
})

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

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        if ($e instanceof BroadcastException) {
            return Limit::perMinute(300);
        }
    });
})

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

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

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        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;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->throttle(function (Throwable $e) {
        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 вашего приложения.

При определении “запасных” страниц ошибок, “запасные” страницы не влияют на ответы об ошибках 404, 500 и 503, поскольку в Laravel есть внутренние, выделенные страницы для этих кодов состояния. Чтобы настроить страницы, отображаемые для этих кодов состояния, необходимо определить собственную страницу ошибок для каждого из них индивидуально.