MiddlewareInterface. It sits in the MiddlewareStack between the router and the controller, gets a typed request + the resolved controller + a $next callable, and must return an AbstractApivalkResponse.
The contract
- Call
$next($request)exactly once to continue the pipeline. Return its value (or a wrapper) to the caller. - Short-circuit by returning a response without calling
$next. That’s how auth, rate limit, validation failures, feature flags, etc. bail out. - The stack is onion-shaped. Middlewares added last wrap the controller innermost. See the order rules below.
Example: log request + response
Example: short-circuit feature flag
$next() stops the pipeline. Everything outside this middleware still runs on the response — notably, the MiddlewareStack still appends Content-Language and (if applicable) X-RateLimit-* headers after all middlewares return.
Example: mutate the request
Order rules
MiddlewareStack::handle() reverses the list internally, so the last middleware you add() is the innermost layer (closest to the controller). Practical consequences:
- Authentication → Security. Add
AuthenticationMiddlewarebeforeSecurityMiddleware, so the identity is populated beforeSecurityMiddlewarereads it. - Rate limit early. Add
RateLimitMiddlewareearly so 429s don’t do expensive work. You still want it after any middleware that sets the identity if yourRateLimitInterface::getKey()reads identity. - Sanitize + Validate last. They touch the actual request body and should be closest to the controller so everything they produce is fresh.
- Cross-cutting observability (log, request id, metrics). Add first — they wrap everything, see both entry and exit of the whole stack.
Middleware vs controller logic
Use a middleware when:- The behaviour applies to many routes — auth, rate limit, logging, request id.
- You want to short-circuit before the controller builds state.
- You need to see both request and response uniformly.
- The logic is route-specific — only
CreateAnimalControllervalidates against inventory. - The logic requires typed access to declared properties — body / path / query — which the middleware only sees as generic bags.