Middleware

Middleware is a PSR-15 layer between routing and the handler: it filters the request, adds context, catches exceptions, or modifies the response. Concept Core uses Psr\Http\Server\MiddlewareInterface and League Route lazyMiddleware().

Middleware is not the same as route interceptors: interceptors run in RouteStrategy after middleware, before the handler is called, and do not form a PSR-15 chain.

See also: request lifecycle → middleware, registration on routes.

PSR-15 contract

<?php declare(strict_types=1);

namespace Concept\App\Http\Middlewares;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class ExampleMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Before handler: checks, modify $request

        $response = $handler->handle($request);

        // After handler: headers, logging

        return $response;
    }
}

The class is registered in the container; League Route creates an instance via DI when calling lazyMiddleware(MyMiddleware::class) — constructor dependencies are injected automatically (ReflectionContainer).

Where to register

  • Global — at the top of routes/web.php (all routes in the file).
  • Group$router->group(...)->lazyMiddleware(...).
  • Single route — chain after get() / post().
  • Component — in Component/routes.php (for example AuthMiddleware on the /admin group).

Registration order defines wrapping order: the first registered middleware is the outermost layer (runs first on the way in). The minimal skeleton does not attach a global stack;

Core middleware

Classes in Concept\Core\Http\Middlewares:

Class Purpose
HandleValidationExceptionMiddleware Catches ValidationException from FormRequest: JSON 422 (RequestFormat::expectsJson()) or flash + ResponseFactory::back()
VerifyCsrfTokenMiddleware CSRF for POST, PUT, PATCH, DELETE; on failure — CsrfException
StorePreviousUrlMiddleware On GET (non-AJAX) stores _url_current / _url_previous in session; sets Concept\Core\Http\Requests\RequestAttribute::SAFE_BACK_URL
ShareViewDataMiddleware Flash, validation errors, old input, csrf_tokenConcept\Core\Http\Requests\RequestAttribute::VIEW_PAYLOAD
ParseJsonBodyMiddleware If Content-Type contains json — parses body into getParsedBody()
ForceJsonResponseMiddleware Sets Accept: application/json for internal logic; response gets Content-Type: application/json

Typical global stack

$router->lazyMiddleware(HandleValidationExceptionMiddleware::class);
$router->lazyMiddleware(VerifyCsrfTokenMiddleware::class);
$router->lazyMiddleware(StorePreviousUrlMiddleware::class);
$router->lazyMiddleware(ShareViewDataMiddleware::class);
  1. Outer validation try/catch (wraps the entire chain)
  2. CSRF check before state changes
  3. URL capture before render
  4. Shared data for Twig (ViewResponseFactory)

CSRF

The token is stored in the session under SessionKey::CSRF_TOKEN (_csrf_token) via CsrfTokenManager. In forms — field _csrf_token; for AJAX — headers X-CSRF-TOKEN or X-XSRF-TOKEN (HttpHeader constants).

<input type="hidden" name="_csrf_token" value="{{ csrf_token }}">

In Twig, the csrf_token variable (ViewKey::CSRF_TOKEN) comes through ShareViewDataMiddlewareViewResponseFactory::create(). More details — CSRF Protection.

Authorization middleware

In the application — custom classes on route groups:

  • AuthMiddleware (AuthAdmin) — session, admin role, ACTIVE status; redirect to /admin/login
  • ActiveUserMiddleware (UserCabinet) — authentication, pending/blocked statuses; redirectByName('cabinet.login')
$router->group('/admin', function (RouteGroup $route) {
    // ...
})->lazyMiddleware(AuthMiddleware::class);

$router->group('/cabinet', function (RouteGroup $route) {
    // ...
})->lazyMiddleware(ActiveUserMiddleware::class);

On failure the controller is not executed — middleware returns a redirect response.

Request attributes

Middleware passes data forward via $request->withAttribute():

ConstantSet byRead by
Concept\Core\Http\Requests\RequestAttribute::VIEW_PAYLOAD ShareViewDataMiddleware ViewResponseFactoryViewInterface::share()
Concept\Core\Http\Requests\RequestAttribute::SAFE_BACK_URL StorePreviousUrlMiddleware ResponseFactory::back()

Keys inside VIEW_PAYLOAD (ViewKey): errors, old, flashes, csrf_token.

ResponseFactory::back() first checks the Referer header, then SAFE_BACK_URL, and only internal URLs (see safe redirects).

Custom middleware

  1. Create a class implementing MiddlewareInterface (for example in src/App/Middlewares/ or in a component).
  2. Dependencies — via constructor (the container injects them automatically).
  3. Attach with lazyMiddleware() globally, on a group, or on a route.
  4. Check order relative to CSRF, auth, and HandleValidationExceptionMiddleware.

Middleware should be "narrow": one responsibility. Complex business logic belongs in a service or controller.

Inspecting middleware

php bin/console.php route:list
php bin/console.php route:list --full-middleware
php bin/console.php route:list -F

The middleware column shows global and local classes for each route.