MiddlewarePipeline - viames/pair GitHub Wiki
Pair framework: MiddlewarePipeline
Pair\Api\MiddlewarePipeline is the execution engine behind Pair API middleware.
It is the class that turns a list of middleware into one deterministic request flow.
Mental model
The pipeline works in FIFO order:
- first middleware added = first executed
- last middleware added = closest to the final action
Internally it builds nested closures from the last middleware back to the first, then runs the resulting callable with the current Request.
Main methods
add(Middleware $middleware): static
Appends one middleware to the stack and returns the pipeline itself.
Example:
$pipeline = new \Pair\Api\MiddlewarePipeline();
// Adds CORS first.
$pipeline->add(new \Pair\Api\CorsMiddleware());
// Adds throttling after CORS.
$pipeline->add(new \Pair\Api\ThrottleMiddleware(60, 60));
run(Request $request, callable $destination): mixed
Builds the middleware chain, executes it, and returns the first explicit value produced by the chain or destination.
This is the main method of the class.
When Observability is enabled, run() records an api.middleware span with the middleware count.
Example:
$pipeline = new \Pair\Api\MiddlewarePipeline();
$pipeline->add(new FirstMiddleware());
$pipeline->add(new SecondMiddleware());
$pipeline->run($request, function () {
// Final action executed only after all middleware pass.
});
Execution order in that example:
FirstMiddleware::handle()SecondMiddleware::handle()- destination callable
How it composes the chain
The current implementation:
- stores middleware in an internal array
- reverses that array when building the pipeline
- wraps the destination callable step by step
That is why the observable order is FIFO even though the internal closure-building loop runs in reverse.
Practical example with inline middleware
use Pair\Api\ApiResponse;
use Pair\Api\Middleware;
use Pair\Api\MiddlewarePipeline;
use Pair\Api\Request;
$pipeline = new MiddlewarePipeline();
$pipeline->add(new class implements Middleware {
public function handle(Request $request, callable $next): mixed
{
// Require JSON before continuing.
if (!$request->isJson()) {
return ApiResponse::errorResponse('UNSUPPORTED_MEDIA_TYPE');
}
return $next($request);
}
});
$pipeline->run($request, function () {
// Final destination.
ApiResponse::respond(['ok' => true]);
});
Typical ApiController usage
In normal Pair projects you usually do not instantiate MiddlewarePipeline manually.
Instead, ApiController owns one internally and exposes:
$this->middleware(...)$this->runMiddleware(...)
Example:
protected function _init(): void
{
parent::_init();
// Adds CORS to this controller pipeline.
$this->middleware(new \Pair\Api\CorsMiddleware());
// Adds a stricter throttle after the default one.
$this->middleware(new \Pair\Api\ThrottleMiddleware(120, 60));
}
public function meAction(): void
{
$this->runMiddleware(function () {
// Runs only after the middleware stack passes.
\Pair\Api\ApiResponse::respond(['ok' => true]);
});
}
Secondary detail worth knowing
MiddlewarePipeline has one important non-public helper:
buildPipeline(callable $destination): callableCreates the nested closure chain used byrun().
You normally do not call it directly, but it explains the FIFO behavior.
Common pitfalls
- Middleware that never calls
$next($request)when it should allow the request. - Middleware that calls
$next()more than once. - Side effects executed after the destination already returned a response.
- Assuming the pipeline reorders middleware automatically. It does not; order is exactly the order you registered.
See also: Middleware, CorsMiddleware, ThrottleMiddleware, ApiController, Observability.