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. Роутинг. Ядро передаёт запрос роутеру, тот ищет совпадение с маршрутом:
phpRoute::get('/users/{id}', [UserController::class, 'show']);
Если совпадения нет — 404. Если есть — к запросу применяются роутовые middleware (группа web или api: сессии, csrf, throttling и свои). Важный момент: часть middleware выполняется уже после того, как роут найден, но до контроллера.
6. Контроллер. Контейнер резолвит контроллер, подставляет зависимости и параметры запроса:
phpclass 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 с «после»-логикой (например, добавить заголовок) пишут так:
phppublic 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.
send() отдаёт ответ пользователю, но terminate() и «after»-колбэки работают после. Проверяйте, когда реально бежит ваш код: «после return» в контроллере — не значит «после всего».api сессии не запускаются (это веб-группа). Код, который в контроллере API обращается к session(), падает или работает не так, как вы думаете. Если нужна сессия в API — либо добавляйте 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
// Путь запроса (по шагам):
// 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
Три вывода
- Запрос проходит
index.php → bootstrap → контейнер → middleware → роут → middleware роута → контроллер → ответ → обратный middleware → terminate(). Уметь перечислить по памяти — база для работы с фреймворком. - Middleware работает в обе стороны: запрос идёт через них «вниз», ответ — «вверх». Код до
$next() и после него — это два разных момента жизни запроса. - Ответ пользователю и конец работы приложения — не одно и то же: после
send() ещё выполняется terminate(), поэтому «после ответа» не значит «после всего».
index.php → bootstrap → контейнер → middleware → роут → middleware роута → контроллер → ответ → обратный middleware → terminate(). Уметь перечислить по памяти — база для работы с фреймворком.$next() и после него — это два разных момента жизни запроса.send() ещё выполняется terminate(), поэтому «после ответа» не значит «после всего».Коварный вопрос
Вопрос: «Что выполняется раньше — middleware web или роутер?»
Подвох: кажется, что middleware — это «до всего». Но web — это роутовая группа: Laravel сначала находит совпавший маршрут, и только потом применяет к нему middleware группы. Раньше выполняются только глобальные middleware. На этом месте спотыкаются даже опытные разработчики.