Исключения в Laravel: методы обработки ошибок от простых до продвинутых

Раздел: Laravel -> Обработка ошибок в 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, и затем передаёт их дальше.

Исключения в Laravel PHP - comments

En
Exceptions php laravel (php)