Что такое Middleware
Middleware -- это фильтры HTTP-запросов, которые выполняются до и/или после обработки запроса контроллером. Они реализуют паттерн Chain of Responsibility (цепочка обязанностей).
<?php
declare(strict_types=1);
// Middleware pipeline visualization:
//
// Request → [Auth] → [CORS] → [Throttle] → Controller
// Response ← [Auth] ← [CORS] ← [Throttle] ←
//
// Each middleware can:
// 1. Inspect/modify the request (before controller)
// 2. Inspect/modify the response (after controller)
// 3. Reject the request entirely (return early)
// 4. Perform cleanup after response is sent (terminable)
Создание Middleware
# Create middleware using artisan
php artisan make:middleware EnsureUserIsAdmin
Before Middleware (выполняется ДО контроллера)
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class EnsureUserIsAdmin
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
// BEFORE middleware: logic executes BEFORE the controller
if (! $request->user()?->isAdmin()) {
// Reject the request — return early, don't call $next
abort(403, 'Access denied. Admin only.');
}
// Pass the request to the next middleware in the chain
return $next($request);
}
}
After Middleware (выполняется ПОСЛЕ контроллера)
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class AddSecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
// Get the response from the next middleware/controller
$response = $next($request);
// AFTER middleware: modify the response AFTER the controller
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
return $response;
}
}
Before + After Middleware (комбинированный)
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
final class MeasureResponseTime
{
public function handle(Request $request, Closure $next): Response
{
// BEFORE: Record start time
$startTime = microtime(true);
// Pass to next middleware / controller
$response = $next($request);
// AFTER: Calculate and add header
$duration = (microtime(true) - $startTime) * 1000;
$response->headers->set('X-Response-Time', round($duration, 2) . 'ms');
Log::debug('Request completed', [
'url' => $request->fullUrl(),
'duration_ms' => round($duration, 2),
]);
return $response;
}
}
Для экзамена: Порядок выполнения важен. Всё что ДО
$next($request)-- это before middleware. Всё что ПОСЛЕ -- это after middleware. Вызов$next($request)передаёт управление следующему middleware в цепочке (или контроллеру, если это последний middleware).
Регистрация Middleware
В Laravel 11 (bootstrap/app.php)
<?php
declare(strict_types=1);
// bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// 1. Append to global middleware stack
$middleware->append(\App\Http\Middleware\MeasureResponseTime::class);
// 2. Prepend to global middleware (runs first)
$middleware->prepend(\App\Http\Middleware\TrustProxies::class);
// 3. Append to 'web' middleware group
$middleware->web(append: [
\App\Http\Middleware\TrackVisitors::class,
]);
// 4. Prepend to 'api' middleware group
$middleware->api(prepend: [
\App\Http\Middleware\ForceJsonResponse::class,
]);
// 5. Register middleware aliases (for route-level use)
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
'role' => \App\Http\Middleware\CheckRole::class,
'locale' => \App\Http\Middleware\SetLocale::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
]);
// 6. Define custom middleware group
$middleware->group('admin', [
'auth',
'admin',
\App\Http\Middleware\LogAdminActions::class,
]);
// 7. Add middleware priority (execution order)
$middleware->priority([
\App\Http\Middleware\StartSession::class,
\App\Http\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\Authorize::class,
]);
// 8. Remove middleware from a group
$middleware->removeFromGroup('web', \App\Http\Middleware\VerifyCsrfToken::class);
// 9. Replace middleware in a group
$middleware->replaceInGroup(
'web',
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\App\Http\Middleware\CustomEncryptCookies::class,
);
})
->create();
Важно для Laravel 11: Middleware больше не регистрируются в
app/Http/Kernel.php(этот файл удалён). Вся конфигурация middleware перенесена вbootstrap/app.phpчерез методwithMiddleware().
Глобальные, групповые и маршрутные Middleware
Глобальные Middleware
<?php
declare(strict_types=1);
// Global middleware runs on EVERY request
// Built-in global middleware in Laravel 11:
// 1. TrustProxies — handle trusted proxies (load balancers)
// 2. HandleCors — CORS headers
// 3. PreventRequestsDuringMaintenance — maintenance mode check
// 4. ValidatePostSize — reject oversized POST requests
// 5. TrimStrings — trim whitespace from input
// 6. ConvertEmptyStringsToNull — convert "" to null
Middleware группы
<?php
declare(strict_types=1);
// 'web' middleware group (routes/web.php):
// - EncryptCookies
// - AddQueuedCookiesToResponse
// - StartSession
// - ShareErrorsFromSession
// - VerifyCsrfToken
// - SubstituteBindings
// 'api' middleware group (routes/api.php):
// - SubstituteBindings
// - (plus Sanctum if configured)
Маршрутные Middleware
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Apply middleware to specific routes
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])
->middleware('admin');
// Multiple middleware
Route::get('/admin/users', [AdminController::class, 'users'])
->middleware(['auth', 'admin', 'verified']);
// Middleware on route group
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/admin/dashboard', [AdminController::class, 'dashboard']);
Route::get('/admin/users', [AdminController::class, 'users']);
});
// Controller-level middleware
namespace App\Http\Controllers;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
final class AdminController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
'auth',
new Middleware('admin', except: ['index']),
new Middleware('role:super-admin', only: ['destroy']),
];
}
}
Middleware с параметрами
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class CheckRole
{
/**
* Handle an incoming request.
*
* @param string ...$roles Additional parameters from route definition
*/
public function handle(Request $request, Closure $next, string ...$roles): Response
{
$user = $request->user();
if (! $user || ! $user->hasAnyRole($roles)) {
abort(403, 'You do not have the required role.');
}
return $next($request);
}
}
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Pass parameters to middleware using colon syntax
Route::get('/admin/settings', [SettingsController::class, 'index'])
->middleware('role:admin');
// Multiple parameters separated by comma
Route::delete('/admin/users/{user}', [UserController::class, 'destroy'])
->middleware('role:admin,super-admin');
// In controller
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
final class SettingsController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
new Middleware('role:admin,manager'),
];
}
}
Для экзамена: Параметры middleware передаются через двоеточие (
middleware('role:admin')). Несколько параметров разделяются запятой. В методеhandle()параметры идут после$next:handle(Request $request, Closure $next, string ...$roles).
Terminable Middleware
Terminable middleware выполняется ПОСЛЕ отправки ответа клиенту. Идеально для операций, которые не должны влиять на время ответа.
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
final class LogDatabaseQueries
{
public function handle(Request $request, Closure $next): Response
{
// Enable query log for this request
DB::enableQueryLog();
return $next($request);
}
/**
* Called AFTER the response has been sent to the client.
* The user has already received the response.
*/
public function terminate(Request $request, Response $response): void
{
$queries = DB::getQueryLog();
$totalTime = array_sum(array_column($queries, 'time'));
if ($totalTime > 1000) { // More than 1 second total
logger()->warning('Slow request detected', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'status' => $response->getStatusCode(),
'query_count' => count($queries),
'total_query_time_ms' => $totalTime,
'slow_queries' => array_filter($queries, fn ($q) => $q['time'] > 100),
]);
}
}
}
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class TrackAnalytics
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
// Send analytics data AFTER response is sent
// User doesn't wait for this
app(\App\Services\AnalyticsService::class)->track([
'url' => $request->fullUrl(),
'method' => $request->method(),
'status' => $response->getStatusCode(),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'user_id' => $request->user()?->id,
'timestamp' => now()->toIso8601String(),
]);
}
}
Важно: Для terminable middleware контейнер разрешает НОВЫЙ экземпляр для вызова
terminate(). Если middleware зарегистрирован как singleton, тот же экземпляр будет использован и вhandle(), и вterminate(). Если нет -- это будут РАЗНЫЕ экземпляры.
Исключение Middleware
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Exclude middleware from specific routes
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/admin/dashboard', [AdminController::class, 'dashboard']);
// This route skips CSRF verification
Route::post('/admin/webhook', [WebhookController::class, 'handle'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
});
// Exclude middleware in controller
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
final class ApiController extends Controller implements HasMiddleware
{
public static function middleware(): array
{
return [
new Middleware('auth', except: ['status', 'health']),
];
}
public function status(): JsonResponse
{
// No auth required
return response()->json(['status' => 'ok']);
}
}
Middleware Priority
<?php
declare(strict_types=1);
// Middleware priority determines the ORDER of execution
// when multiple middleware are applied to a route
// By default, middleware runs in the order they are listed
// But priority list overrides this
// Example: StartSession MUST run before ShareErrorsFromSession
// Priority ensures correct order regardless of listing order
// In bootstrap/app.php:
$middleware->priority([
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
]);
Практический пример: API middleware
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class ForceJsonResponse
{
/**
* Force all responses to be JSON for API routes.
*/
public function handle(Request $request, Closure $next): Response
{
// Set Accept header to application/json
// This ensures Laravel returns JSON errors (not HTML)
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class SetLocale
{
public function handle(Request $request, Closure $next, ?string $locale = null): Response
{
$locale = $locale
?? $request->header('Accept-Language')
?? $request->query('lang')
?? config('app.locale');
// Validate locale
$supported = ['en', 'ru', 'de', 'fr'];
$locale = in_array($locale, $supported, true) ? $locale : config('app.locale');
app()->setLocale($locale);
$response = $next($request);
$response->headers->set('Content-Language', $locale);
return $response;
}
}