Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Middleware

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

7 питань

Middleware - це шар фільтрації HTTP-запитів, що «обгортає» обробку: код може виконатися до того, як запит дійде до контролера, і/або після формування відповіді. Уявіть це як серію «застав», крізь які проходить кожен запит.

Створити middleware:

php artisan make:middleware EnsureUserIsActive

Логіка - у методі handle(); ключове - викликати $next($request), щоб передати запит далі конвеєром:

public function handle(Request $request, Closure $next): Response
{
    if (! $request->user()?->is_active) {
        return redirect('login'); // перервати конвеєр
    }

    return $next($request); // пропустити далі
}

Реєстрація та призначення (у Laravel 11+ - у bootstrap/app.php), застосування до маршрутів:

Route::get('/dashboard', ...)->middleware('auth');
Route::middleware(['auth', 'verified'])->group(fn () => ...);

Типові вбудовані middleware: auth (автентифікація), throttle (обмеження частоти), verified (підтверджений email), signed (підписані URL). Middleware - основа для авторизації, логування, CORS, локалізації.

Докладніше в документації: Middleware

Клас створює команда:

php artisan make:middleware EnsureUserIsSubscribed
public function handle(Request $request, Closure $next): Response
{
    if (! $request->user()?->subscribed()) {
        return redirect()->route('billing');
    }

    return $next($request);
}

Як підключити - залежно від того, де він потрібен:

// на маршруті чи групі
Route::get('/reports', ReportController::class)->middleware(EnsureUserIsSubscribed::class);

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->append(LogRequestId::class);               // глобальний, на кожен запит
    $middleware->web(append: [SetLocale::class]);           // до групи web
    $middleware->alias(['subscribed' => EnsureUserIsSubscribed::class]); // коротке ім'я
})

Після аліасу на маршруті можна писати ->middleware('subscribed').

Що змінилося: до Laravel 11 усе це жило в app/Http/Kernel.php (властивості $middleware, $middlewareGroups, $routeMiddleware). У нових застосунках цього класу немає - налаштування в bootstrap/app.php.

У Laravel 13 middleware можна призначити й атрибутом прямо на контролері: #[Middleware('subscribed')].

Докладніше в документації: Реєстрація middleware

Звичайне middleware працює навколо запиту: щось робить до $next($request), щось - після, але завжди до того, як відповідь піде користувачу.

Terminable middleware виконується після відправлення відповіді:

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

    public function terminate(Request $request, Response $response): void
    {
        // Користувач уже отримав сторінку - це його не затримує.
        RequestLog::create([
            'path' => $request->path(),
            'status' => $response->getStatusCode(),
            'duration' => microtime(true) - LARAVEL_START,
        ]);
    }
}

Метод terminate() викликається з $app->terminate() після send().

Важлива умова: це працює лише коли сервер уміє віддати відповідь і продовжити виконання - тобто на FastCGI з fastcgi_finish_request(). За іншої конфігурації користувач усе одно чекатиме.

Ще одна деталь: за замовчуванням у terminate() потрапляє новий екземпляр middleware. Якщо потрібен той самий - зареєструйте його синглтоном:

$this->app->singleton(LogRequestDuration::class);

Для чого доречно: запис аналітики, логування тривалості, дрібне прибирання - те, що не впливає на відповідь.

Для чого ні: будь-що довге. Воно все одно тримає PHP-воркер зайнятим, тож ця робота належить у чергу, а не в terminate().

Докладніше в документації: Middleware

Параметри пишуть після двокрапки, кілька - через кому. У handle() вони приходять після $next.

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('role:editor,admin');
class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (! $request->user()?->hasAnyRole($roles)) {
            abort(403);
        }

        return $next($request);
    }
}

Знайомі приклади з фреймворку:

  • throttle:60,1 - 60 запитів на хвилину;
  • can:update,post - перевірка політики з моделлю з параметра маршруту;
  • auth:sanctum - гард для автентифікації;
  • cache.headers:public;max_age=3600 - параметри через крапку з комою.

Без рядків: вбудовані middleware мають статичні методи using(), що збирають цей рядок, - так IDE бачить посилання на клас:

->middleware(ThrottleRequests::using('uploads'))
->middleware(Authorize::using('update', 'post'))

Для власного middleware такий метод легко додати самому.

Якщо параметрів стає багато чи вони складні, це сигнал, що перевірку краще перенести в політику чи запит форми.

Докладніше в документації: Параметри middleware

Групи підключаються автоматично: web - до routes/web.php, api - до routes/api.php.

Група web - усе для сторінок у браузері:

  • EncryptCookies і AddQueuedCookiesToResponse - шифровані cookie;
  • StartSession - сесія;
  • ShareErrorsFromSession - змінна $errors у шаблонах;
  • PreventRequestForgery - захист від CSRF (у Laravel 13 спершу перевіряє заголовок Sec-Fetch-Site, потім токен);
  • SubstituteBindings - прив'язка моделей до параметрів маршруту.

Група api - лише SubstituteBindings. Немає сесії, cookie й CSRF: API зазвичай автентифікується токеном (auth:sanctum) і не тримає стану.

Наслідки, про які питають:

  • маршрут з routes/api.php не бачить сесії й old();
  • SPA на тому самому домені через Sanctum працює з cookie - для цього Sanctum додає потрібні middleware до api через statefulApi();
  • routes/api.php у свіжому Laravel 11+ немає - його створює php artisan install:api.

Додати своє в групу:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [SetLocale::class]);
    $middleware->api(prepend: [ForceJsonResponse::class]);
})

Докладніше в документації: Типові групи middleware

Middleware утворюють «цибулю» навколо обробки запиту: кожен бачить запит на вході й відповідь на виході у зворотному порядку. Порядок вирішує, бо одні залежать від роботи інших.

Зазвичай він визначається місцем реєстрації: глобальні → групові → маршрутні. Але middleware, призначені маршруту, сортуються за списком пріоритетів, а не за тим, як їх перелічили в ->middleware([...]).

Чому це важливо. SubstituteBindings резолвить {post} у модель. Політика, що перевіряє власника поста, має спрацювати після цього - інакше отримає рядок замість моделі. Так само аутентифікація мусить йти раніше за авторизацію: перевіряти права нема в кого, поки користувач не визначений.

Задати свій порядок у bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->priority([
        \Illuminate\Cookie\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\Auth\Middleware\Authenticate::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\EnsureSubscribed::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ]);
})

Ті, що є в списку, ідуть у вказаному порядку; решта - за порядком призначення.

Ознака, що ви натрапили саме на це: middleware працює на одному маршруті й «не бачить» даних на іншому, хоча код той самий. Зазвичай це означає, що щось потрібне ще не встигло відпрацювати.

Порада: не переставляйте наосліп. Спершу зʼясуйте, від чого залежить ваше middleware - від сесії, від резолвленої моделі, від автентифікованого користувача, - і поставте його після цього.

Докладніше в документації: Middleware

Код до $next($request) виконується до контролера, код після - коли відповідь уже готова.

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

    $response = $next($request);

    $response->headers->set('Server-Timing', 'app;dur='.(hrtime(true) - $start) / 1e6);
    $response->headers->set('X-Frame-Options', 'SAMEORIGIN');

    return $response;
}

Типові задачі «після»: заголовки безпеки, Cache-Control, кореляційний ID запиту, вимірювання часу.

На що зважати:

  • Порядок. На вході middleware виконуються за списком, на виході - у зворотному порядку. Middleware, що ставить Cache-Control, має йти після того, що може його змінити. Laravel дозволяє задати пріоритет (priority() у bootstrap/app.php).
  • Потокові й файлові відповіді. StreamedResponse і BinaryFileResponse не мають готового тіла - не можна читати чи змінювати getContent(), лише заголовки.
  • Глобальний чи маршрутний. Глобальний middleware спрацьовує навіть для 404 і на запити до Livewire чи ресурсів, тож важка логіка там коштує на кожен запит.
  • Чужі middleware можуть перетерти заголовок. Наприклад, Livewire ставить no-store глобально, тож власний middleware, що робить сторінку кешованою, мусить бути в тому самому глобальному стеку й іти після нього.
  • Робота після відповіді - не тут, а в terminate() чи defer(): інакше користувач чекає.

Докладніше в документації: Middleware і відповіді