Исключения в Laravel: методы обработки ошибок от простых до продвинутых
Основы обработки исключений в Laravel
Исключения в Laravel обрабатываются через класс App\Exceptions\Handler. Этот класс отвечает за перехват всех исключений, их логирование и формирование ответа, возвращаемого пользователю. Рассмотрим наиболее эффективный подход к настройке обработки ошибок, а затем альтернативные сценарии.
Базовое решение заключается в переопределении методов report и render в классе Handler. Метод report отвечает за логирование исключения, а render - за формирование HTTP-ответа. Пример настройки для возврата JSON во всех окружениях (кроме debug):
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
protected $levels = [
// уровень логирования для конкретных исключений
];
protected $dontReport = [
// исключения, которые не нужно логировать
];
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
public function register(): void
{
$this->reportable(function (Throwable $e) {
// дополнительная логика отчетности
});
$this->renderable(function (Throwable $e, $request) {
if ($request->expectsJson()) {
return response()->json([
'message' => $e->getMessage()
], 500);
}
});
}
}Exceptions php laravel (исключения в laravel php)
В методе register регистрируются замыкания для отчетности и рендера. Таким образом, можно гибко настраивать поведение для разных типов запросов.
Типичная ошибка: забыть обновить автозагрузку Composer после добавления нового кастомного исключения. Это приводит к ошибке класса не найден. Решение - выполнить composer dump-autoload.
Также стоит помнить, что при использовании игнорирования исключений через $dontReport они всё равно могут быть обработаны в render.
Как переопределить метод report для отправки уведомлений в Slack?
В классе Handler можно использовать метод reportable с замыканием, которое отправляет уведомление в канал Slack при возникновении определённого исключения. Пример:
$this->reportable(function (Throwable $e) {
if ($e instanceof \App\Exceptions\PaymentException) {
// логика отправки в Slack
Notification::route('slack', config('services.slack.webhook_url'))
->notify(new ExceptionSlackNotification($e));
}
});Замыкания из reportable выполняются только после того, как исключение было залогировано стандартным образом. Чтобы отключить стандартное логирование и оставить только кастомную отправку, нужно вернуть false из замыкания.
Проблема: если в reportable вернуть false, исключение не будет залогировано вообще. Для случаев, когда нужно и логировать, и отправлять уведомление, лучше не возвращать false, а вызывать логирование внутри замыкания.
Как создать и зарегистрировать собственное исключение с кастомным HTTP-кодом?
Создайте класс, наследующий Exception, определите необходимые свойства. Затем в Handler::register добавьте проверку через замыкание renderable. Пример:
namespace App\Exceptions;
use Exception;
class ValidationException extends Exception
{
public $statusCode = 422;
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message ?: 'Ошибка валидации', $this->statusCode, $previous);
}
}
// В Handler::register:
$this->renderable(function (ValidationException $e, $request) {
if ($request->expectsJson()) {
return response()->json(['message' => $e->getMessage()], $e->statusCode);
}
return response()->view('errors.validation', ['exception' => $e], $e->statusCode);
});Теперь при выбросе ValidationException ответ будет содержать кастомное сообщение и код 422.
Частая ошибка: не указать правильный namespace в кастомном исключении, из-за чего автозагрузка не найдёт класс. Также следует проверить, что исключение не перехватывается более общим обработчиком раньше.
Как изменить отображение ошибок для окружения production?
В файле config/app.php параметр 'debug' управляет показом подробных ошибок. Для production установите 'debug' => env('APP_DEBUG', false). В классе Handler можно дополнительно проверять окружение:
public function render($request, Throwable $e)
{
if (app()->environment('production')) {
return response()->view('errors.500', [], 500);
}
return parent::render($request, $e);
}Для разных HTTP-статусов можно подготовить отдельные Blade-шаблоны в resources/views/errors/ (например, 404.blade.php, 500.blade.php). Laravel автоматически использует их, если они существуют.
Важно: если шаблон ошибки отсутствует, Laravel вернёт стандартное сообщение. Необходимо создать все нужные шаблоны заранее. Также убедитесь, что в production отключён debug, иначе пользователь увидит трассировку стека.
Как обработать исключения моделей (ModelNotFoundException) в API?
Используйте замыкание renderable для перехвата ModelNotFoundException и возврата JSON c сообщением:
$this->renderable(function (\Illuminate\Database\Eloquent\ModelNotFoundException $e, $request) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Ресурс не найден'], 404);
}
return response()->view('errors.404', [], 404);
});Такой подход позволяет централизованно обрабатывать все случаи, когда модель не найдена, не добавляя try-catch в каждый контроллер.
Проблема: если в маршруте используется implicit binding и модель не найдена, Laravel автоматически выбрасывает ModelNotFoundException. Если не обработать это исключение, пользователь получит стандартный HTML с трассировкой (в debug) или пустую 500 ошибку (в production).
Расширенные примеры работы с исключениями в Laravel демонстрируют более специфические сценарии и комбинации описанных выше подходов.
Пример 1: Кастомное исключение для API с поддержкой локализации
Создайте исключение, которое принимает код ошибки и динамически формирует сообщение на основе локали пользователя.
// app/Exceptions/ApiException.php
namespace App\Exceptions;
use Exception;
class ApiException extends Exception
{
public $statusCode;
public $errorCode;
public function __construct(string $errorCode, int $statusCode = 400, $message = null)
{
$this->errorCode = $errorCode;
$this->statusCode = $statusCode;
$message = $message ?: __('errors.'.$errorCode);
parent::__construct($message, $statusCode);
}
}
// resources/lang/en/errors.php
return [
'user_not_found' => 'User not found.',
'invalid_token' => 'Invalid or expired token.',
];
// В Handler::register
$this->renderable(function (ApiException $e, $request) {
return response()->json([
'error' => $e->errorCode,
'message' => $e->getMessage()
], $e->statusCode);
});
// Использование в контроллере
throw new ApiException('user_not_found', 404);// Ответ клиенту:
{
"error": "user_not_found",
"message": "User not found."
}Этот подход удобен для API с международной поддержкой и единой структурой ошибок.
Пример 2: Обработка исключений очередей с уведомлениями
В классе Handler можно перехватывать исключения из очередей, используя метод reportable с проверкой окружения.
use Illuminate\Queue\MaxAttemptsExceededException;
use Illuminate\Support\Facades\Log;
// В register()
$this->reportable(function (MaxAttemptsExceededException $e) {
Log::channel('slack')->warning('Job failed after max attempts', [
'job' => get_class($e->job),
'exception' => $e->getPrevious()?->getMessage()
]);
});
// В самом классе Job можно также использовать метод failed()Такой подход позволяет централизованно отслеживать сбои в фоновых задачах.
Пример 3: Использование макросов для Handler
Laravel позволяет добавлять макросы к классу Handler через сервис-провайдер, что удобно для модульных расширений.
// AppServiceProvider.php
use App\Exceptions\Handler;
use Illuminate\Support\Facades\Log;
public function boot()
{
Handler::macro('reportToExternalService', function (Throwable $e) {
// Отправка в внешний сервис мониторинга
Log::channel('external')->error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
});
}
// В Handler::register
$this->reportable(function (Throwable $e) {
$this->reportToExternalService($e);
});Макросы позволяют избежать дублирования кода и подключать дополнительные обработчики по необходимости.
Пример 4: Тестирование исключений с помощью assertThrows
В тестах Laravel можно проверить, что определённая операция выбрасывает нужное исключение, используя PHPUnit или встроенные методы.
// tests/Feature/ApiExceptionTest.php
public function test_user_not_found_throws_api_exception()
{
$this->expectException(\App\Exceptions\ApiException::class);
$this->expectExceptionMessage('User not found.');
$this->get('/api/user/99999');
}
// Альтернативный метод:
$response = $this->get('/api/user/99999');
$response->assertStatus(404);
$response->assertJson(['error' => 'user_not_found']);// Результат теста (зелёная галочка): все ассерты проходят.
Такой подход гарантирует, что обработка исключений работает корректно.
Пример 5: Глобальный обработчик через middleware
Иногда требуется добавить дополнительную обработку до того, как исключение попадёт в Handler. Для этого можно написать middleware, которое перехватывает исключения в конвейере.
// app/Http/Middleware/ExceptionLogger.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
use Throwable;
class ExceptionLogger
{
public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (Throwable $e) {
Log::channel('sentry')->error($e->getMessage(), [
'url' => $request->fullUrl(),
'method' => $request->method()
]);
throw $e; // Повторно выбрасываем для обработки Handler
}
}
}
// Зарегистрировать в kernel.php
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\ExceptionLogger::class,
// ...
],
];Этот middleware логирует все исключения, проходящие через маршруты группы web, и затем передаёт их дальше.