Laravel middleware - это код, который встаёт между входящим HTTP-запросом и контроллером, а на обратном пути может подправить готовый ответ перед отправкой в браузер. На практике в бэкендах на Laravel через middleware проходит львиная доля инфраструктурной логики: проверка токена, роль пользователя, лимит запросов, подпись вебхука от платёжной системы. Путаница у новичков обычно не в том, как написать сам класс - там пять строк кода, а в том, в каком порядке Laravel вызывает десяток разных middleware на одном запросе и почему auth иногда отрабатывает раньше session, хотя в коде объявлен позже.
Разберу, из чего складывается порядок вызова, что такое приоритет между слоями и какие сценарии встречаются в моих проектах на Laravel чаще всего.
Что такое middleware в Laravel и как устроен вызов
Middleware - это класс с одним обязательным методом handle(), который принимает объект запроса и замыкание $next. Всё, что написано до вызова $next($request), выполняется на пути запроса к контроллеру. Всё, что после этой строки - на обратном пути, когда контроллер уже вернул ответ. Middleware удобно представлять слоями матрёшки: запрос идёт снаружи внутрь, ответ возвращается в обратном порядке.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user() || ! $request->user()->is_admin) {
abort(403);
}
return $next($request);
}
}
Метод handle обязан вернуть Response - либо результат $next($request), либо собственный ответ через abort() или response(). Если не вызвать $next(), запрос до контроллера не дойдёт вообще: это стандартный способ остановить обработку на любом слое и не тратить время на обращение к базе там, где доступа заведомо не будет.
Порядок выполнения: глобальные, групповые и маршрутные слои
Laravel собирает итоговую цепочку middleware для конкретного запроса из трёх уровней, и порядок между этими уровнями фиксированный.
- Глобальные middleware выполняются на каждый запрос независимо от маршрута - это TrustProxies, HandleCors, PreventRequestsDuringMaintenance и всё, что добавлено вручную через append().
- Middleware группы - web или api, которые Laravel подключает автоматически для маршрутов из соответствующих файлов роутинга. В web-группе это куки, сессия, ошибки из сессии, защита от подделки запроса и подстановка моделей, а в api-группе по умолчанию остаётся только подстановка моделей: throttle попадает туда лишь после вызова throttleApi() в bootstrap/app.php.
- Middleware маршрута или контроллера - подключаются через alias и вызов ->middleware(‘admin’) на конкретном роуте или группе роутов.
Внутри каждого уровня порядок в массиве тоже имеет значение: первый элемент выполняется первым на пути «туда» и последним на пути «обратно». Если в web-группе StartSession стоит раньше VerifyCsrfToken, сессия будет доступна к моменту проверки токена. Поменяйте их местами - и проверка CSRF сломается, потому что токен сверяется с данными из сессии, которой ещё нет на этом шаге.
В проектах на Laravel 11 и новее регистрация уехала из Kernel.php в bootstrap/app.php, но сама трёхуровневая логика не изменилась - поменялся только синтаксис.
| Что регистрируем | Laravel до 11 (Kernel.php) | Laravel 11+ (bootstrap/app.php) |
|---|---|---|
| Глобальный middleware | свойство $middleware | $middleware->append() |
| Группы web и api | свойство $middlewareGroups | $middleware->group() / appendToGroup() |
| Алиас для маршрута | свойство $routeMiddleware | $middleware->alias() |
| Приоритет вызова | свойство $middlewarePriority | $middleware->priority() |
Как создать свой middleware и подключить его к маршруту
Класс создаётся артизан-командой:
php artisan make:middleware EnsureUserIsAdmin
Далее классу нужен алиас, чтобы обращаться к нему по короткому имени в маршрутах:
<?php
use App\Http\Middleware\EnsureUserIsAdmin;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => EnsureUserIsAdmin::class,
]);
})
->create();
После этого middleware подключается к маршруту одной строкой:
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('admin');
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/reports', [ReportController::class, 'index']);
});
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Приоритет middleware, если порядок регистрации не совпадает с порядком вызова
На одном маршруте middleware чаще всего приходят из разных источников: часть из группы web, часть добавлена через alias руками. Laravel не всегда может понять по порядку регистрации, что должно выполниться раньше - например, проверка авторизации логически обязана идти перед проверкой роли, даже если в коде маршрута роль указана первой. Для таких случаев есть явный приоритет.
->withMiddleware(function (Middleware $middleware) {
$middleware->priority([
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\Auth\Middleware\Authenticate::class,
\App\Http\Middleware\EnsureUserIsAdmin::class,
]);
})
Список задаёт желаемый порядок вызова независимо от того, откуда конкретный middleware попал в цепочку - из глобальных, из группы или из alias на маршруте. На практике приоритет нужен редко, в основном когда своя проверка роли зависит от того, что аутентификация уже отработала.
Параметры в middleware и terminable middleware
В handle можно принимать дополнительные аргументы после $next - Laravel передаёт их из строки алиаса через двоеточие.
public function handle(Request $request, Closure $next, string $role): Response
{
if (! $request->user()?->hasRole($role)) {
abort(403);
}
return $next($request);
}
Применяется так: ->middleware(‘role:editor’). Значений после двоеточия может быть несколько, через запятую - они попадут в handle по порядку следующими аргументами.
Отдельный вид - terminable middleware, у которого кроме handle есть метод terminate(). Он вызывается уже после того, как ответ фактически отправлен клиенту, и удобен для логирования или подсчёта метрик, которые не должны задерживать пользователя.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class LogRequestDuration
{
public function handle(Request $request, Closure $next): Response
{
$request->attributes->set('started_at', microtime(true));
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
$duration = microtime(true) - $request->attributes->get('started_at');
logger()->info('Request finished', ['duration' => $duration]);
}
}
Важная оговорка: terminate() реально освобождает браузер от ожидания только на PHP-FPM, где сервер умеет закрыть соединение с клиентом и продолжить работу воркера (fastcgi_finish_request). На обычном shared-хостинге это работает из коробки, потому что там как раз PHP-FPM. А вот если приложение крутится через встроенный сервер artisan serve, ответ клиенту физически уйдёт только после terminate() - разницы по задержке не будет.
Типовые сценарии применения middleware на практике
Большинство middleware в моих проектах на Laravel закрывают несколько повторяющихся задач.
Проверка роли и прав доступа - самый частый случай, разобран выше на примере EnsureUserIsAdmin. Обычно таких middleware несколько: для админки, для API-клиентов с отдельным токеном, для гостевых форм.
Ограничение частоты запросов - готовый alias throttle из коробки, не нужно писать свой класс. Например, ->middleware(‘throttle:60,1’) разрешает 60 запросов в минуту с одного ключа (по умолчанию - по IP или пользователю).
Проверка подписи входящего вебхука - типичная задача для интеграции с эквайрингом. У Т‑Банка, как и у большинства платёжных провайдеров, уведомление о статусе платежа приходит на отдельный URL с подписью в теле запроса, и её нужно сверить до того, как обработчик поверит в данные:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class VerifyAcquiringSignature
{
public function handle(Request $request, Closure $next): Response
{
$token = $request->input('Token');
$params = $request->except(['Token', 'Receipt', 'DATA']);
ksort($params);
$params['Password'] = config('services.acquiring.password');
$expected = hash('sha256', implode('', $params));
if (! $token || ! hash_equals($expected, $token)) {
abort(400, 'Invalid signature');
}
return $next($request);
}
}
Алгоритм сборки строки для подписи стоит сверять с актуальной документацией конкретного эквайринга перед продакшеном - у разных провайдеров набор полей и порядок может отличаться. Смысл middleware в том, чтобы эта проверка стояла одним слоем перед всеми вебхук-обработчиками, а не дублировалась в каждом контроллере.
Логирование и подсчёт времени ответа - через terminable middleware, как в примере выше, без замедления самого запроса.
Локализация - middleware читает заголовок Accept-Language или префикс маршрута и выставляет App::setLocale() до того, как запрос попадёт в контроллер, чтобы переводы и форматирование дат сразу были на нужном языке.
Спроектировать такую цепочку middleware под конкретный проект, от авторизации до проверки вебхуков, - то, чем я занимаюсь на услуге разработка бэкенда на Laravel.
Laravel регулярно обновляется, поэтому перед внедрением я сверяюсь с актуальной документацией.
Частые вопросы
В каком порядке выполняются несколько middleware на одном маршруте?
Сначала глобальные, потом middleware группы web или api, затем те, что подключены через alias на самом маршруте. Внутри каждого уровня действует порядок объявления в массиве: код до $next выполняется сверху вниз, код после $next - в обратном порядке. Если middleware из разных уровней должны выполняться в определённой последовательности не по умолчанию, для этого есть priority().
Как передать параметр в middleware?
Третьим (и последующими) аргументом в handle() после $next. В маршруте параметр указывается через двоеточие после имени алиаса, например ->middleware(‘role:editor’), а несколько значений перечисляются через запятую.
Чем terminable middleware отличается от обычного?
У него есть метод terminate(Request $request, Response $response), который Laravel вызывает уже после того, как ответ ушёл клиенту. Это удобно для логирования, метрик и любой работы, которая не должна задерживать пользователя, но реальный выигрыш по времени есть только на PHP-FPM, который умеет закрыть соединение раньше завершения скрипта.
Можно ли отключить middleware для конкретного маршрута?
Да, через withoutMiddleware() прямо на маршруте или на группе маршрутов: Route::get(‘/webhook’, …)->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class). Работает это только с маршрутными middleware: глобальные таким способом не снимаются вообще. И про имя класса: в Laravel 13 CSRF-middleware называется PreventRequestForgery, в версиях до 12 включительно это был VerifyCsrfToken, а ValidateCsrfToken остался устаревшим алиасом.