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 exampleAuthMiddlewareon the/admingroup).
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_token → Concept\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);
- Outer validation try/catch (wraps the entire chain)
- CSRF check before state changes
- URL capture before render
- 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
ShareViewDataMiddleware → ViewResponseFactory::create().
More details — CSRF Protection.
Authorization middleware
In the application — custom classes on route groups:
AuthMiddleware(AuthAdmin) — session, admin role,ACTIVEstatus; redirect to/admin/loginActiveUserMiddleware(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():
| Constant | Set by | Read by |
|---|---|---|
Concept\Core\Http\Requests\RequestAttribute::VIEW_PAYLOAD |
ShareViewDataMiddleware |
ViewResponseFactory → ViewInterface::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
- Create a class implementing
MiddlewareInterface(for example insrc/App/Middlewares/or in a component). - Dependencies — via constructor (the container injects them automatically).
- Attach with
lazyMiddleware()globally, on a group, or on a route. - 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.