Lifecycle в Laravel: как проходит запрос от входа до ответа

Lifecycle — это путь HTTP-запроса через приложение: от момента, когда браузер отправил URL, до отдачи готового ответа. По-русски говорят «жизненный цикл запроса». Понимать его нужно не для экзамена, а чтобы отвечать на вопрос «а что происходит, когда я открываю страницу?» — и чтобы не теряться, когда что-то ломается «внутри», до контроллера.


Зачем это нужно

  • Дебаг. Когда запрос падает с ошибкой до контроллера — не поймёшь, в каком слое. Знаешь lifecycle — сразу видишь, что проблема, например, в глобальном middleware, а не в твоём коде.
  • Понимание, куда что втыкать. Свой сервис-провайдер, middleware, listener — каждое из них срабатывает на конкретном этапе. Не зная порядок, ставишь их наугад.
  • Осознанная архитектура. Когда видишь путь запроса целиком, проще решать, где разрезать логику: что вынести в сервис, что в middleware, что в событие.

Как это работает

Путь запроса можно разбить на этапы. Смотрим на примере GET /users/5.

1. Точка входа: public/index.php. Веб-сервер (nginx + php-fpm, или artisan serve в разработке) все запросы направляет в этот файл. Он создаёт экземпляр приложения:

php
// public/index.php (упрощённо)
$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Kernel::class);          // HTTP-ядро

$response = $kernel->handle(
    $request = Request::capture()             // запрос из суперглобальных ($_GET, $_POST, ...)
)->send();

$kernel->terminate($request, $response);      // завершение

Здесь три ключевых слова: Request::capture() собирает всё, что пришло от клиента, в объект Request; handle() прогоняет запрос через приложение и возвращает Response; terminate() делает финальную работу уже после отправки ответа пользователю (об этом — в подводных камнях).

2. Bootstrap: подготовка окружения. bootstrap/app.php собирает приложение и регистрирует базовые провайдеры. Дальше ядро загрузит:

  • .env и конфиги из config/,
  • фасады,
  • провайдеры из config/app.php (метод register и потом boot).

Важно: у провайдеров два шага — сначала регистрируются все, потом загружаются все. Нельзя в boot() одного провайдера полагаться на порядок загрузки других — последовательность может быть не той, что вы ожидаете.

3. Service Container. К этому моменту контейнер — сердце приложения — уже знает все привязки (bind, singleton). Именно он через рефлексию подставляет зависимости в конструкторы контроллеров и middleware. Запрос-объект тоже живёт в контейнере.

4. Middleware. Ядро прогоняет запрос через глобальный middleware (в app/Http/Kernel.php — группа $middleware): CORS, тримминг строк, подстановка session и так далее.

5. Роутинг. Ядро передаёт запрос роутеру, тот ищет совпадение с маршрутом:

php
Route::get('/users/{id}', [UserController::class, 'show']);

Если совпадения нет — 404. Если есть — к запросу применяются роутовые middleware (группа web или api: сессии, csrf, throttling и свои). Важный момент: часть middleware выполняется уже после того, как роут найден, но до контроллера.

6. Контроллер. Контейнер резолвит контроллер, подставляет зависимости и параметры запроса:

php
class UserController extends Controller
{
    public function show(Request $request, UserService $service, int $id) // $id — из роута
    {
        $user = $service->find($id);
        return new UserResource($user);       // или view, или json
    }
}

Возвращаемое значение (view, строка, массив, Response, JsonResponse, ресурс) приводится к объекту Response.

7. Обратный проход middleware. Ответ идёт обратно через те же middleware — от контроллера наружу. Поэтому middleware с «после»-логикой (например, добавить заголовок) пишут так:

php
public function handle($request, Closure $next)
{
    $response = $next($request);   // сюда запрос «внутрь»
    $response->headers->set('X-Debug-Time', now()->toString());
    return $response;              // а это уже ответ «наружу»
}

8. send() и terminate(). Ответ уходит клиенту. Затем ядро вызывает terminate() — отрабатывают terminable-middleware и события kernel.terminating. Здесь же обычно запланированные после ответа задачи: отправка писем, запись логов.

Картинка целиком:

text
Браузер
  ↓ nginx → public/index.php
  Request::capture()
  ↓ загрузка .env, конфигов, провайдеров (register → boot)
  ↓ Service Container готов
  ↓ глобальный middleware → роутер → роутовый middleware
  ↓ контроллер (+ резолв зависимостей из контейнера)
  ↓ Response
  ↓ обратный проход middleware
  ↓ send()
  terminate() — финальная работа после отправки

Плохо / Хорошо

ПлохоХорошо
Держать всю логику в контроллере, «он же получает запрос»Контроллер принимает запрос и вызывает сервис/action, возвращает ответ
Строить запрос руками из $_GET/$_POST вместо использования RequestПолагаться на объект Request: валидация, параметры, файлы — всё там
Вешать проверку в boot() провайдера, предполагая порядок загрузки другихВ boot() полагаться только на зарегистрированное, а порядок — явно в config/app.php
Тяжёлая работа в middleware «после» ответа (отправка письма синхронно)Отправлять в очередь; в terminate — только лёгкую финальную работу
Пытаться вернуть из контроллера «что-то своё» без приведенияВозвращать Response/JsonResponse/view — то, что Laravel умеет преобразовать

Подводный камень

  • Ответ уже ушёл — а код ещё выполняется. send() отдаёт ответ пользователю, но terminate() и «after»-колбэки работают после. Проверяйте, когда реально бежит ваш код: «после return» в контроллере — не значит «после всего».
  • Middleware до роута ≠ middleware после. Проверка в глобальном middleware выполняется до поиска роута; 404 вы получите уже после того, как часть middleware отработала. Если ставите «before»-проверку в глобальный middleware — она сработает и для несуществующих страниц.
  • Session не всегда есть. В группе api сессии не запускаются (это веб-группа). Код, который в контроллере API обращается к session(), падает или работает не так, как вы думаете. Если нужна сессия в API — либо добавляйте middleware сессии, либо откажитесь от идеи.
  • Транзакции не закатываются сами. Lifecycle заканчивается отправкой ответа — если вы открыли транзакцию в middleware и забыли закрыть, она «висит» до таймаута соединения. Каждый открывший DB::beginTransaction() отвечает за commit/rollback.

Мини-шпаргалка

php
// Путь запроса (по шагам):
// index.php → Request::capture() → kernel->handle()
//   → bootstrap (config, providers register/boot)
//   → глобальный middleware
//   → роутер (поиск роута) → роутовый middleware (web/api)
//   → контроллер (резолв из контейнера)
//   → Response → обратный проход middleware
//   → send() → terminate()

// Где что смотреть:
// app/Http/Kernel.php — какие middleware и в каких группах
// bootstrap/app.php — сборка приложения
// app/Providers/* — register() → boot() все провайдеров
// routes/web.php, routes/api.php — что дальше после роутового middleware

Три вывода

  1. Запрос проходит index.php → bootstrap → контейнер → middleware → роут → middleware роута → контроллер → ответ → обратный middleware → terminate(). Уметь перечислить по памяти — база для работы с фреймворком.
  2. Middleware работает в обе стороны: запрос идёт через них «вниз», ответ — «вверх». Код до $next() и после него — это два разных момента жизни запроса.
  3. Ответ пользователю и конец работы приложения — не одно и то же: после send() ещё выполняется terminate(), поэтому «после ответа» не значит «после всего».

Коварный вопрос

Вопрос: «Что выполняется раньше — middleware web или роутер?»

Подвох: кажется, что middleware — это «до всего». Но web — это роутовая группа: Laravel сначала находит совпавший маршрут, и только потом применяет к нему middleware группы. Раньше выполняются только глобальные middleware. На этом месте спотыкаются даже опытные разработчики.