MidТеория8 min

Middleware

Создание middleware, глобальные vs маршрутные, группы, параметры, terminable middleware, исключение middleware

Что такое 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;
    }
}

Проверь себя

Где в Laravel 11 регистрируются middleware aliases и группы?

Когда вызывается метод `terminate()` в middleware?

Как исключить middleware для определённого маршрута внутри группы?

Как передать параметры middleware при назначении маршруту?

Какой порядок выполнения кода в middleware, если есть логика ДО и ПОСЛЕ вызова `$next($request)`?