Middleware
Middleware wrap the handling of a request. They run on the way in, before the request is validated and evaluated, and on the way out, after the result is produced. You use them for things that apply to the whole request, like authentication, logging, or wrapping the result in an envelope.
How it works
Middleware nest like an onion. Each middleware gets the next handler and decides when
to call it. Code you write before calling $next runs on the way in. Code you write
after runs on the way out.
A middleware implements MiddlewareInterface:
use ArekX\RestFn\Parser\Context;
use ArekX\RestFn\Runner\Contracts\MiddlewareInterface;
use ArekX\RestFn\Runner\Request;
class TimingMiddleware implements MiddlewareInterface
{
public function process(Request $request, Context $context, callable $next): mixed
{
$start = microtime(true); // on the way in
$result = $next($request, $context);
error_log('took ' . (microtime(true) - $start)); // on the way out
return $result;
}
}
$next is the rest of the chain, ending in the core handler that validates and
evaluates the request. It returns the result, which you can return as is or change.
Registering middleware
Middleware are configured on the Runner as a list of class names. They're resolved
through the container, so a middleware can declare its own dependencies in its
constructor and they'll be injected.
WebApp::createDefault() wires a default stack for you: ErrorMiddleware (outermost)
and AuthenticationMiddleware. Setting the runner.middleware config value replaces
that stack entirely, so include the defaults yourself if you still want them:
'global' => ['runner' => ['middleware' => [
ErrorMiddleware::class, // keep error handling outermost
AuthenticationMiddleware::class,
TimingMiddleware::class,
]]],
Order
The first middleware in the list is the outermost. It runs first on the way in and
last on the way out. With two middleware [Outer, Inner] the order is:
Outer (before)
Inner (before)
validate + evaluate
Inner (after)
Outer (after)
So if you want something to wrap the final result, put it first in the list.
Changing the result
Because a middleware returns whatever it wants, it can transform the result. This wraps every result in an envelope:
class EnvelopeMiddleware implements MiddlewareInterface
{
public function process(Request $request, Context $context, callable $next): mixed
{
return ['ok' => true, 'data' => $next($request, $context)];
}
}
Short-circuiting
A middleware doesn't have to call $next. If it returns without calling it, the
request is short-circuited and the rest of the chain (including validation and
evaluation) never runs. This is how you reject a request early:
public function process(Request $request, Context $context, callable $next): mixed
{
if (!$this->allowed($request)) {
return ['error' => 'forbidden'];
}
return $next($request, $context);
}
A note on errors
The on-the-way-out part of a middleware only runs if $next returns normally. If
validation fails or an action throws, the exception travels up past your after-code.
If a middleware needs to run no matter what (for logging, say), wrap $next in a
try/finally. If it needs to turn an error into a result, catch the exception
around $next.
That's what the built-in ErrorMiddleware does, which is why it sits outermost by
default: it catches anything thrown below it and turns it into a clean response. See
Error handling.
Error handling
The outermost middleware in the default stack is ErrorMiddleware. It wraps the whole
pipeline in a try/catch and turns anything thrown below it into a JSON error,
hiding internal details unless debug mode is on. It has its own page:
Error handling.
Where middleware live
The built-in middleware are in ArekX\RestFn\Runner\Middleware: the authentication
middleware and the error middleware. Put your own middleware
wherever you like in your project. They only need to implement MiddlewareInterface.