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:

  1. FirstMiddleware::handle()
  2. SecondMiddleware::handle()
  3. 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): callable Creates the nested closure chain used by run().

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.