Skip to content

MiddlewarePipeline

Viames Marino edited this page Mar 26, 2026 · 3 revisions

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): void

Builds the middleware chain and executes it.

This is the main method of the class.

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): void
    {
        // Require JSON before continuing.
        if (!$request->isJson()) {
            ApiResponse::error('UNSUPPORTED_MEDIA_TYPE');
        }

        $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.

Clone this wiki locally