Урок 04

Middleware: паттерн Chain of Responsibility

Что такое Middleware

Middleware — слой между запросом и контроллером.

Задачи:

  • Аутентификация.
  • CORS.
  • Rate limiting.
  • Логирование.
  • Локализация.

Паттерн Chain of Responsibility

Цепочка обработчиков:

  1. Запрос идёт через первый middleware.
  2. Первый вызывает следующий.
  3. И так далее — до контроллера.
  4. Ответ идёт в обратном порядке.

Каждый может:

  • Прервать (вернуть ответ).
  • Изменить запрос/ответ.
  • Пропустить дальше через $next.

Простой пример

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureTokenIsValid
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->input('token') !== 'secret') {
            return response('Access denied', 401);
        }

        return $next($request);
    }
}

$next($request) — передать дальше.

Возврат ответа

Middleware может вернуть ответ раньше:

if (!auth()->check()) {
    return redirect('/login');
}

return $next($request);

Цепочка прерывается.

Изменение запроса

public function handle(Request $request, Closure $next): Response
{
    $request->merge(['source' => 'api']);

    return $next($request);
}

merge() — добавить параметры.

Изменение ответа

public function handle(Request $request, Closure $next): Response
{
    $response = $next($request);

    $response->headers->set('X-App-Version', '1.0');

    return $response;
}

После $next можно модифицировать ответ.

Before vs After

До $next — до обработки:

public function handle(Request $request, Closure $next): Response
{
    // до
    $response = $next($request);
    // после

    return $response;
}

Регистрация

Глобальный middleware

Для всех запросов — в bootstrap/app.php (Laravel 11+):

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(YourMiddleware::class);
    $middleware->prepend(AnotherMiddleware::class);
})

append — в конец. prepend — в начало.

Группа

->withMiddleware(function (Middleware $middleware) {
    $middleware->group('web', [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
    ]);
})

На маршрут

Route::get('/admin', fn() => view('admin'))
    ->middleware('auth', 'admin');

На контроллер

class AdminController
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('admin')->only(['destroy']);
    }
}

Параметры middleware

Передача параметров:

Route::get('/user/{id}', ...)->middleware('can:view,user');

В middleware:

public function handle(
    Request $request,
    Closure $next,
    string $action,
    string $model,
): Response { }

Порядок

Важен!

Порядок выполнения:

  1. Global (в порядке регистрации).
  2. Group (web/api).
  3. Route (на маршруте).

Возврат — в обратном порядке.

Встроенные Middleware

Основные:

  • Authenticate — auth.
  • Authorize — can.
  • VerifyCsrfToken — web.
  • EnsureEmailIsVerified — verified.
  • ThrottleRequests — throttle.
  • EnsureFrontendRequestsAreStateful — sanctum.

Создание

php artisan make:middleware EnsureUserIsAdmin

Файл в app/Http/Middleware/.

Регистрация в bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
    ]);
})

Практика: Middleware для админа

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()?->is_admin) {
            abort(403, 'Доступ только для администраторов');
        }

        return $next($request);
    }
}

Использование:

Route::middleware('admin')->group(function () {
    Route::get('/admin', [AdminController::class, 'index']);
    Route::delete('/users/{user}', [UserController::class, 'destroy']);
});

Практика: логирование запросов

class LogRequests
{
    public function __construct(
        private LoggerInterface $logger,
    ) {}

    public function handle(Request $request, Closure $next): Response
    {
        $start = microtime(true);

        $response = $next($request);

        $this->logger->info('Request', [
            'method' => $request->method(),
            'path' => $request->path(),
            'status' => $response->getStatusCode(),
            'time_ms' => round((microtime(true) - $start) * 1000, 2),
        ]);

        return $response;
    }
}

Terminable Middleware

Обработка после ответа:

class LogResponse
{
    public function handle(Request $request, Closure $next): Response
    {
        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        // выполнится после отправки ответа клиенту
    }
}

После Response отправлен — для фоновых задач.

Kernel в Laravel 10

app/Http/Kernel.php:

protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
];

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
    ],
    'api' => [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
    ],
];

protected $middlewareAliases = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
];

Laravel 11+ — в bootstrap/app.php.

Практика

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Models\User;

class TrackUserActivity
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        /** @var User|null $user */
        $user = $request->user();

        if ($user !== null && $this->shouldTrack($request)) {
            $user->forceFill([
                'last_login_at' => now(),
            ])->saveQuietly();
        }

        return $response;
    }

    private function shouldTrack(Request $request): bool
    {
        return $request->isMethod('GET')
            && !$request->ajax()
            && !$request->is('admin/*');
    }
}

Регистрация:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\App\Http\Middleware\TrackUserActivity::class);
})

Итог

  • Middleware — Chain of Responsibility.
  • $next($request) — передать дальше.
  • До/после — по расположению кода.
  • Прерывание — вернуть Response.
  • Порядок важен.
  • Алиасы — для удобства.
  • terminate — после ответа.

Дальше — Eloquent: Active Record vs Data Mapper.