0

PSR-15 MiddlewareInterface and RequestHandlerInterface

Intermediate5 min read·fw-05-002
psr

Concept

PSR-15 defines two interfaces for HTTP server middleware: MiddlewareInterface and RequestHandlerInterface. PSR-15 middleware is compatible across any compliant framework.

RequestHandlerInterface:

php
interface RequestHandlerInterface {
    public function handle(ServerRequestInterface $request): ResponseInterface;
}

A request handler takes a request and returns a response. The controller/action is the terminal handler. The pipeline-wrapped middleware chain is also a handler.

MiddlewareInterface:

php
interface MiddlewareInterface {
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $next
    ): ResponseInterface;
}

A middleware takes a request and the next handler. It can call $next->handle($request) to continue the pipeline, or return a response without calling next (short-circuit).

Key difference from the functional approach: The $next parameter is an object (RequestHandlerInterface) not a raw callable. This allows type hints and IDE support.

Adapter pattern: Wrapping each middleware in a RequestHandlerInterface so the chain can be composed. The pipeline composes middleware + handler into a single RequestHandlerInterface.

PSR-7 vs custom Request: PSR-15 uses ServerRequestInterface (PSR-7). If your framework uses a custom Request class, you can implement the interfaces while passing your custom class, or create an adapter layer.

Code Example

php
<?php
namespace Framework\Http\Middleware;

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

// PSR-15 compliant middleware
class AuthMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
    {
        $token = $request->getHeaderLine('Authorization');
        if (!$token) {
            return new \Nyholm\Psr7\Response(401, [], 'Unauthorized');
        }
        $user    = $this->resolveUser($token);
        $request = $request->withAttribute('user', $user);
        return $next->handle($request); // continue to next middleware/handler
    }

    private function resolveUser(string $token): ?array { /* ... */ return null; }
}

class LoggingMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
    {
        $start    = microtime(true);
        $response = $next->handle($request);
        $elapsed  = round((microtime(true) - $start) * 1000, 2);
        // Add timing header on the way back
        return $response->withHeader('X-Response-Time', $elapsed . 'ms');
    }
}

// Terminal handler — wraps the route action
class RouteHandler implements RequestHandlerInterface
{
    public function __construct(private readonly \Closure $action) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return ($this->action)($request);
    }
}

// Middleware-to-handler adapter (for pipeline composition)
class MiddlewareHandler implements RequestHandlerInterface
{
    public function __construct(
        private readonly MiddlewareInterface  $middleware,
        private readonly RequestHandlerInterface $next,
    ) {}

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return $this->middleware->process($request, $this->next);
    }
}