Middleware: The Wrap
Middleware is airport security between the gate and the plane. You don't write it for every flight. You nod at the TSA agent.
You will learn
- What PSR-15 middleware actually is (in 30 seconds)
- The six built-ins ZealPHP ships with — and when each one matters
- How to write a custom middleware in about 10 lines
- Why the registration order is reversed when the stack actually runs
What middleware is
A middleware is a function that gets the request before your handler, and gets the response after. It can short-circuit the request (returning 401 before authentication even reaches your route), add a header to every response (compression, CORS, ETag), measure timing, log, anything that should apply to many routes rather than one.
ZealPHP uses the PSR-15 middleware shape. A middleware implements:
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface;
You either return a response yourself (short-circuit) or call $handler->handle($request)
to delegate to the next middleware in the chain. Same shape Slim, Symfony, Laravel (via adapters),
and most modern PHP frameworks use.
The six built-ins
ZealPHP ships these out of the box. Most apps register the first four.
| Middleware | What it does | Configure with |
|---|---|---|
CorsMiddleware | Preflight (OPTIONS) handling and Access-Control-* headers on every response. | Constructor args or ZEALPHP_CORS_ORIGINS |
ETagMiddleware | Weak ETag on GET responses; returns 304 on If-None-Match match. | None |
CompressionMiddleware | gzip/deflate when client supports it. Skip if OpenSwoole’s built-in compression is on. | None |
RangeMiddleware | RFC 7233 Range requests: 206 Partial Content, multi-range, If-Range. | None |
SessionStartMiddleware | Eager session start for first-time visitors. Without this, only returning visitors get sessions. | ZEALPHP_SESSION_SECURE for HTTPS override |
IniIsolationMiddleware | Snapshots php.ini changes per request so ini_set() doesn’t leak. | ZEALPHP_INI_ISOLATE=1 |
Register them in app.php before $app->run():
$app->addMiddleware(new CorsMiddleware());
$app->addMiddleware(new ETagMiddleware());
$app->addMiddleware(new SessionStartMiddleware());
Order is reversed at execution time
You register A, B, C. The stack ZealPHP builds is C wraps B wraps A wraps
ResponseMiddleware. At request time, C runs first. The last middleware
you add is the outermost wrapper — same convention as Slim, Express, Laravel.
This means: register the inner-most concerns first, the outer-most last. Session handling is closest to the route handler — register early. CORS handles every response including 404s — register last.
Writing your own
Here’s a complete rate-limit-header middleware in 10 lines:
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RateLimitHeader implements MiddlewareInterface {
public function __construct(private int $limit = 60) {}
public function process($request, RequestHandlerInterface $handler) {
$response = $handler->handle($request);
return $response
->withHeader('X-RateLimit-Limit', (string)$this->limit)
->withHeader('X-RateLimit-Remaining', (string)($this->limit - 1));
}
}
// in app.php
$app->addMiddleware(new RateLimitHeader(100));
The pattern: call $handler->handle(), modify the returned response, return it.
For short-circuit behavior (e.g., auth that returns 401 before the route runs), build a response
yourself with new \OpenSwoole\Core\Psr\Response(...) and return it without calling
$handler->handle() at all.
When NOT to write middleware
If the logic applies to one route, it goes in the handler. If it’s really tangled with request-specific data, it goes in the handler. If you find yourself reaching for middleware to validate a single form, you’ve over-engineered — just validate in the handler and return 422.
Middleware shines for cross-cutting concerns: things every route benefits from (CORS, logging, sessions) or things you can turn on/off as a feature flag (compression, rate-limiting, auth-required gates).
Try it live
The demo app registers CORS + ETag + Session-start + Range. See the headers in action:
- CORS:
Access-Control-*headers - ETag: 304 Not Modified on repeat
- Gzip compression:
Content-Encoding: gzip
You register middleware in the order: A, B, C. A request arrives. Which middleware sees the inbound request first?
Key Takeaways
- A middleware is a function that wraps every request:
(request, $handler) => response. - ZealPHP follows PSR-15 — same shape as Slim, Symfony, modern Laravel.
- Six built-ins: CORS, ETag, Compression, Range, SessionStart, IniIsolation.
- Register order is reversed at execution: last-added runs first (outermost).
- Middleware is for cross-cutting concerns — per-route logic still belongs in the handler.