PHP PSR-15 Middleware — Complete Guide to HTTP Middleware Interfaces
In this tutorial, you will learn about PHP PSR. We cover key concepts, practical examples, and best practices to help you master this topic.
PHP PSR-15 defines standard interfaces for HTTP middleware and request handlers, enabling reusable request processing pipelines that work across any PSR-7 compatible framework.
What You'll Learn
By the end of this tutorial, you'll implement PSR-15 middleware, build middleware pipelines, handle errors in middleware, create reusable middleware components, and integrate with frameworks.
Why PSR-15 Matters
PSR-15 standardizes middleware, the most common pattern for HTTP request processing. Middleware enables cross-cutting concerns like authentication, logging, and CORS in reusable packages.
Real-World Use
A Slim application uses PSR-15 middleware for request logging, JWT authentication, CORS headers, Rate Limiting, and JSON body Parsing, all implemented as standalone middleware classes.
PSR-15 Path
flowchart LR
A[PSR-14 Events] --> B[PSR-15 Middleware]
B --> C[PSR-17 HTTP Factory]
C --> D[PSR-11 Container]
D --> E[PHP Middleware Slim]
B --> F{You Are Here}
style F fill:#f90,color:#fff
MiddlewareInterface
PSR-15 middleware receives a request and a handler, processes the request, and returns a response.
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class JsonResponseMiddleware implements MiddlewareInterface {
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$response = $handler->handle($request);
return $response->withHeader("Content-Type", "application/json");
}
}
RequestHandlerInterface
A request handler processes the request and returns a response. In a pipeline, it represents the next middleware or the final application handler.
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AppHandler implements RequestHandlerInterface {
public function __construct(
private array $routes
) {}
public function handle(ServerRequestInterface $request): ResponseInterface {
$path = $request->getUri()->getPath();
$handler = $this->routes[$path] ?? null;
if (!$handler) {
$response = new \GuzzleHttp\Psr7\Response(404);
$response->getBody()->write("Not Found");
return $response;
}
return $handler($request);
}
}
Middleware Pipeline
Create a pipeline that chains multiple middleware together.
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class MiddlewarePipeline implements RequestHandlerInterface {
private array $middleware;
private RequestHandlerInterface $fallback;
public function __construct(array $middleware, RequestHandlerInterface $fallback) {
$this->middleware = $middleware;
$this->fallback = $fallback;
}
public function handle(ServerRequestInterface $request): ResponseInterface {
if (empty($this->middleware)) {
return $this->fallback->handle($request);
}
$middleware = array_shift($this->middleware);
return $middleware->process($request, $this);
}
}
Authentication Middleware
Validate JWT tokens in middleware.
<?php
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class JwtAuthMiddleware implements MiddlewareInterface {
public function __construct(
private string $secret
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$auth = $request->getHeaderLine("Authorization");
if (!str_starts_with($auth, "Bearer ")) {
$response = new \GuzzleHttp\Psr7\Response(401);
$response->getBody()->write("Unauthorized");
return $response;
}
$token = substr($auth, 7);
$payload = json_decode(base64_decode(explode(".", $token)[1]), true);
$request = $request->withAttribute("user_id", $payload["sub"]);
return $handler->handle($request);
}
}
Error Handling Middleware
Catch exceptions from inner middleware and return appropriate responses.
<?php
class ErrorHandlingMiddleware implements MiddlewareInterface {
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
try {
return $handler->handle($request);
} catch (\InvalidArgumentException $e) {
$response = new \GuzzleHttp\Psr7\Response(400);
$response->getBody()->write(json_encode(["error" => $e->getMessage()]));
return $response->withHeader("Content-Type", "application/json");
} catch (\Throwable $e) {
$response = new \GuzzleHttp\Psr7\Response(500);
$response->getBody()->write("Internal Server Error");
return $response;
}
}
}
Common Mistakes
1. Not Calling $handler->handle()
Middleware that does not call the handler breaks the pipeline. Always call $handler->handle() unless returning early.
2. Modifying Request After Passing to Handler
The request object should be immutable. Use withAttribute() to add data for downstream handlers.
3. Returning Non-PSR-7 Responses
Always return a PSR-7 ResponseInterface. Returning strings or arrays breaks the pipeline.
4. Throwing Exceptions in Middleware
Catch exceptions and return error responses. Uncaught exceptions leave the pipeline incomplete.
5. Heavy Processing in Middleware
Middleware runs on every request. Keep it fast. Offload heavy work to the application handler.
Practice Questions
1. What does MiddlewareInterface::Process receive?
A ServerRequestInterface and a RequestHandlerInterface representing the next middleware or handler.
2. What must middleware return?
A ResponseInterface object. The response flows back through the middleware chain.
3. How do you pass data from middleware to the application?
Use $request->withAttribute(name, value) to attach data to the request object.
4. What happens if middleware does not call $handler->handle()?
The inner middleware and application handler never execute. The middleware short-circuits the pipeline.
5. Challenge: Create a rate-limiting middleware using PSR-15.
<?php
class RateLimitMiddleware implements MiddlewareInterface {
private array $requests = [];
public function __construct(
private int $maxRequests = 100,
private int $windowMs = 3600000
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
$ip = $request->getServerParams()["REMOTE_ADDR"] ?? "unknown";
$this->cleanup();
$this->requests[$ip][] = microtime(true);
if (count($this->requests[$ip]) > $this->maxRequests) {
$response = new \GuzzleHttp\Psr7\Response(429);
$response->getBody()->write("Rate limit exceeded");
return $response;
}
return $handler->handle($request);
}
private function cleanup(): void {
$cutoff = microtime(true) - $this->windowMs;
foreach ($this->requests as $ip => $times) {
$this->requests[$ip] = array_filter($times, fn($t) => $t > $cutoff);
}
}
}
FAQ
Mini Project: CORS Middleware
Build a reusable CORS middleware implementing PSR-15.
<?php
class CorsMiddleware implements MiddlewareInterface {
public function __construct(
private array $allowedOrigins = ["*"],
private array $allowedMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
private array $allowedHeaders = ["Content-Type", "Authorization"],
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
if ($request->getMethod() === "OPTIONS") {
$response = new \GuzzleHttp\Psr7\Response(204);
} else {
$response = $handler->handle($request);
}
$origin = $request->getHeaderLine("Origin");
if (in_array("*", $this->allowedOrigins) || in_array($origin, $this->allowedOrigins)) {
$response = $response
->withHeader("Access-Control-Allow-Origin", $origin ?: "*")
->withHeader("Access-Control-Allow-Methods", implode(", ", $this->allowedMethods))
->withHeader("Access-Control-Allow-Headers", implode(", ", $this->allowedHeaders))
->withHeader("Access-Control-Max-Age", "86400");
}
return $response;
}
}
What's Next
PHP PSR-17 HTTP Factory PHP Middleware Slim PHP PSR-11 Container
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro