Middleware
Middleware is a layer the request passes through on its way to the handler, and the response on its way back. Middleware holds what repeats from route to route: a login check, starting the session, service headers. In Slim it is the main way to extend an application.
Where middleware lives
Middleware classes go into src/Middleware/, and they are attached in a separate file, config/middleware.php. Both are created by hand.
The first middleware
// src/Middleware/ResponseTimeMiddleware.php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class ResponseTimeMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$start = microtime(true);
$response = $handler->handle($request);
$ms = round((microtime(true) - $start) * 1000, 1);
return $response->withHeader('X-Response-Time', $ms . ' ms');
}
}The key line is $handler->handle($request): it passes the request on, to the next middleware or to the action, and returns the finished response. Code before it runs before the action, code after it runs when the response is ready. MiddlewareInterface is part of the PSR-15 standard, so ready-made middleware from third-party packages works in Slim too.
Attaching it
Create config/middleware.php:
// config/middleware.php
use App\Middleware\ResponseTimeMiddleware;
use Slim\App;
return function (App $app): void {
$app->add(ResponseTimeMiddleware::class);
};And load it in config/bootstrap.php before the routes:
// config/bootstrap.php
(require __DIR__ . '/middleware.php')($app);
(require __DIR__ . '/routes.php')($app);Open any page and look at the response headers on the Network tab of the developer tools — X-Response-Time is among them.
A group and a single route
Middleware added with $app->add() runs for every request. It can be attached more narrowly:
// config/routes.php
$app->get('/posts', ListPostsAction::class)
->add(ResponseTimeMiddleware::class); // one route
$app->group('/admin', function (RouteCollectorProxy $group) {
// section routes
})->add(ResponseTimeMiddleware::class); // the whole groupStopping the request and passing data
A middleware does not have to call $handler->handle(): it can return a response itself, for example a redirect to the login page. That is how an access check works — it is shown in the authentication lesson.
To pass data to the action, a middleware adds an attribute to the request:
return $handler->handle($request->withAttribute('started_at', $start));The action reads the value with $request->getAttribute('started_at').
Execution order
Middleware forms layers, like an onion. The one added last ends up outermost and runs first:
// config/middleware.php
$app->add(FirstMiddleware::class);
$app->add(SecondMiddleware::class);The request passes through SecondMiddleware, then FirstMiddleware, then reaches the action; the response travels back in reverse order. Route and group middleware always runs after application middleware.
Common mistake
withAttribute(), like every with method, returns a new request. If you call it and pass the old $request on, the action will not see the attribute. Pass to $handler->handle() exactly the object that withAttribute() returned.