Skip to content

Глава 2.7: Обработка ошибок

Время изучения: 40 минут


1. Почему стандартные ошибки — это плохо?

Если в вашем Laravel-приложении происходит ошибка (например, не найдена запись в базе), а вы это никак не обработали, пользователь увидит огромную HTML-страницу с отладочной информацией или неинформативное сообщение "Server Error".

Для API это катастрофа. Ваше фронтенд-приложение ожидает получить JSON, а не HTML. Наша задача — перехватить любую ошибку и превратить ее в структурированный JSON-ответ.


2. Центральный диспетчер ошибок: bootstrap/app.php

В старых версиях Laravel был громоздкий файл App\Exceptions\Handler.php. В Laravel 11/12 все стало гораздо проще и элегантнее. Центр управления ошибками теперь находится прямо в файле конфигурации вашего приложения — bootstrap/app.php.

Откройте bootstrap/app.php. В самом низу вы увидите блок .withExceptions(...). Это и есть наш "центральный диспетчер".

<?php
// bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // ...
    })
    ->withExceptions(function (Exceptions $exceptions) {
        // <-- ВОТ ЗДЕСЬ МЫ БУДЕМ РАБОТАТЬ
    })->create();

3. Обрабатываем самую частую ошибку: "Не найдено" (404)

Самая распространенная ошибка в API — это когда пользователь запрашивает ресурс, которого не существует (например, GET /api/planets/999). Laravel в этом случае генерирует исключение ModelNotFoundException или NotFoundHttpException. Давайте их перехватим.

Добавьте следующий код внутрь .withExceptions(...):

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // Перехватываем исключение, когда модель не найдена в базе данных
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        // Проверяем, что запрос пришел именно на наш API
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Запрашиваемый ресурс не найден в нашей галактике.'
            ], 404);
        }
    });

    // Перехватываем исключение, когда сам маршрут не найден
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json([
                'message' => 'Такого космического маршрута не существует.'
            ], 404);
        }
    });

})->create();
Что мы сделали?

  1. $exceptions->render(...) — мы регистрируем "обработчик". Он говорит: "Если произойдет исключение типа ModelNotFoundException, выполни этот код".
  2. if ($request->is('api/*')) — это важная проверка. Она гарантирует, что наш красивый JSON-ответ будет отправляться только для API-запросов, не затрагивая обычные веб-страницы.
  3. return response()->json(...) — мы создаем и возвращаем стандартизированный JSON-ответ с кодом 404.

Теперь, если вы запросите несуществующую планету, вместо уродливой HTML-страницы вы получите аккуратный JSON.


4. Кастомные исключения: Создаем собственные "сигналы тревоги"

Иногда стандартных исключений недостаточно. Представим, что у нас есть бизнес-правило: "нельзя удалять планету 'Земля'". Если кто-то попытается это сделать, мы должны вернуть осмысленную ошибку.

Шаг 1: Создаем свой класс исключения Выполним в терминале:

php artisan make:exception CannotDeleteEarthException

Шаг 2: Используем его в контроллере Откроем PlanetController.php и изменим метод destroy:

<?php
// app/Http/Controllers/PlanetController.php
use App\Exceptions\CannotDeleteEarthException; // <-- Импортируем наше исключение
use App\Models\Planet;

public function destroy(Planet $planet)
{
    // Наше новое бизнес-правило
    if (strtolower($planet->name) === 'земля') {
        throw new CannotDeleteEarthException('Удаление планеты Земля запрещено Галактическим Кодексом.');
    }

    $planet->delete();
    return response()->json(null, 204);
}
Теперь, если кто-то попытается выполнить DELETE /api/planets/1 (где 1 — это ID Земли), наш код выбросит исключение CannotDeleteEarthException.

Шаг 3: Учим Laravel красиво обрабатывать нашу "тревогу" Вернемся в bootstrap/app.php и добавим новый обработчик для нашего исключения.

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // Наш новый обработчик
    $exceptions->render(function (CannotDeleteEarthException $e, Request $request) {
        return response()->json([
            'message' => 'Операция запрещена.',
            'details' => $e->getMessage() // Получаем сообщение, которое мы передали в throw
        ], 403); // 403 Forbidden - "Доступ запрещен"
    });

    // ... (остальные обработчики для 404)

})->create();
Готово! Мы создали собственное именованное исключение, которое делает код контроллера чище, и научили Laravel превращать его в красивый, осмысленный JSON-ответ с правильным HTTP-статусом.


5. Обработка всех остальных сбоев (500 Internal Server Error)

Что делать со всеми остальными, непредвиденными ошибками? Например, если отвалилась база данных или в коде синтаксическая ошибка. Для этого мы можем зарегистрировать "универсальный" обработчик для самого общего типа ошибок — Throwable.

Важно: Этот обработчик должен быть последним, чтобы не перехватывать более специфичные исключения, которые мы определили выше.

<?php
// bootstrap/app.php

->withExceptions(function (Exceptions $exceptions) {

    // ... (обработчики для CannotDeleteEarthException и 404)

    // УНИВЕРСАЛЬНЫЙ ОБРАБОТЧИК (в самом конце)
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->is('api/*')) {
            // В режиме отладки можно показать настоящее сообщение об ошибке
            $message = config('app.debug')
                ? 'Произошла ошибка: ' . $e->getMessage()
                : 'На борту произошла непредвиденная ошибка. Инженеры уже вызваны.';

            return response()->json(['message' => $message], 500);
        }
    });

})->create();

Теперь любое "неизвестное" исключение будет аккуратно перехвачено и превращено в JSON с кодом 500, не ломая ваше API и не показывая пользователю лишней информации.


6. Логирование ошибок: Черный ящик космического корабля

Настройки логирования в config/logging.php:

<?php
'channels' => [
    'space_api' => [
        'driver' => 'daily',
        'path' => storage_path('logs/space_api.log'),
        'level' => 'error',
        'days' => 14,
    ],
],

Добавление записи в лог:

<?php
try {
    // Код с риском ошибки
} catch (Exception $e) {
    Log::channel('space_api')->error('Ошибка доступа к планетам', [
        'exception' => $e,
        'request' => request()->all(),
        'user_id' => auth()->id()
    ]);
    throw $e;
}


Квиз для закрепления

1. HTTP-статус для "Планета не найдена:

2. Класс для глобальной обработки ошибок:

3. Метод для создания кастомного исключения:

4. Канал для раздельного логирования ошибок API:

5. Главное преимущество создания кастомных исключений:


🚀 Итог главы:

Вы оснастили свое API надежной системой спасения:

  • 🛟 Глобальный перехват стандартных ошибок
  • 🪐 Кастомные исключения с понятными кодами
  • 📝 Единый JSON-формат для всех ошибок
  • 🔍 Логирование с деталями инцидента
  • 📡 Интеграция с системами мониторинга

Космический корабль готов к аварийным ситуациям! В финальной главе раздела мы протестируем все системы.

📌 Проверка:

  1. Создайте исключение PlanetNotFoundException
  2. Добавьте обработку 404 ошибок в ->withExceptions
  3. Протестируйте запрос к несуществующей планете

⚠️ Если ошибки не перехватываются:

  • Убедитесь что is('api/*') соответствует вашим роутам
  • Проверьте порядок обработчиков в register()
  • Для кастомных исключений используйте throw new