Documentation Index
Fetch the complete documentation index at: https://docs.apivalk.com/llms.txt
Use this file to discover all available pages before exploring further.
A middleware in Apivalk is any class implementing 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
namespace apivalk\apivalk\Middleware;
interface MiddlewareInterface
{
public function process(
ApivalkRequestInterface $request,
AbstractApivalkController $controller,
callable $next
): AbstractApivalkResponse;
}
Three things to know:
- 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
<?php
declare(strict_types=1);
namespace App\Middleware;
use apivalk\apivalk\Http\Controller\AbstractApivalkController;
use apivalk\apivalk\Http\Request\ApivalkRequestInterface;
use apivalk\apivalk\Http\Response\AbstractApivalkResponse;
use apivalk\apivalk\Middleware\MiddlewareInterface;
use Psr\Log\LoggerInterface;
final class AccessLogMiddleware implements MiddlewareInterface
{
/** @var LoggerInterface */
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function process(
ApivalkRequestInterface $request,
AbstractApivalkController $controller,
callable $next
): AbstractApivalkResponse {
$startedAt = \microtime(true);
$response = $next($request);
$this->logger->info('apivalk.request', [
'method' => $request->getMethod()->getName(),
'controller' => \get_class($controller),
'status' => $response::getStatusCode(),
'duration_ms' => (int)((\microtime(true) - $startedAt) * 1000),
'identity' => $request->getAuthIdentity()->isAuthenticated() ? 'user' : 'guest',
]);
return $response;
}
}
Register it:
$configuration->getMiddlewareStack()->add(new AccessLogMiddleware($logger));
Example: short-circuit feature flag
final class FeatureFlagMiddleware implements MiddlewareInterface
{
/** @var FeatureFlagService */
private $flags;
public function __construct(FeatureFlagService $flags)
{
$this->flags = $flags;
}
public function process(
ApivalkRequestInterface $request,
AbstractApivalkController $controller,
callable $next
): AbstractApivalkResponse {
// e.g. only let traffic through when a flag is on
if (!$this->flags->isEnabled('new_api', $request->getAuthIdentity())) {
return new NotFoundApivalkResponse();
}
return $next($request);
}
}
Returning without calling $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
final class RequestIdMiddleware implements MiddlewareInterface
{
public function process(
ApivalkRequestInterface $request,
AbstractApivalkController $controller,
callable $next
): AbstractApivalkResponse {
if (!$request->header()->has('X-Request-Id')) {
// You'd typically set this on a logger/context store, not on the bag directly —
// ParameterBag is read-mostly in Apivalk.
}
$response = $next($request);
$response->addHeaders(['X-Request-Id' => \bin2hex(\random_bytes(8))]);
return $response;
}
}
Middlewares are the right place to stamp correlation IDs onto outbound responses, because they’re the only layer that sees every response uniformly.
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
AuthenticationMiddleware before SecurityMiddleware, so the identity is populated before SecurityMiddleware reads it.
- Rate limit early. Add
RateLimitMiddleware early so 429s don’t do expensive work. You still want it after any middleware that sets the identity if your RateLimitInterface::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.
Recommended default order:
$stack = $configuration->getMiddlewareStack();
$stack->add(new AccessLogMiddleware($logger)); // outermost
$stack->add(new RequestIdMiddleware());
$stack->add(new AuthenticationMiddleware($authenticator));
$stack->add(new SecurityMiddleware());
$stack->add(new RateLimitMiddleware($cache));
$stack->add(new SanitizeMiddleware());
$stack->add(new RequestValidationMiddleware()); // innermost, last before controller
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.
Use a controller (or a service called from one) when:
- The logic is route-specific — only
CreateAnimalController validates against inventory.
- The logic requires typed access to declared properties — body / path / query — which the middleware only sees as generic bags.
Register via the container (optional)
For a middleware with dependencies (logger, cache, feature-flag service, database), define it in your PSR-11 container and pull it out at bootstrap:
$configuration->getMiddlewareStack()->add($container->get(AccessLogMiddleware::class));
See Configure Apivalk → extending your configuration.