0

Building the pipeline — composing middleware into a single handler

Advanced5 min read·fw-05-003
laravel-src

Concept

Building the pipeline — composing multiple middleware and a terminal handler into a single RequestHandlerInterface that can be called with a request to get a response.

Composition: The pipeline wraps middleware from the outside in. The LAST middleware in the array is CLOSEST to the handler. array_reduce() with array_reverse() achieves this.

Mental model: Think of an onion. The outer layers (first middleware in array) run first on the way in, and last on the way out. The core is the route handler.

Pipeline class: Accepts an ordered list of middleware classes (or instances) and a terminal handler. run(Request $request) processes the request through all middleware and returns a Response.

Lazy instantiation: Middleware can be provided as class names (strings) and resolved from the container only when needed. This avoids instantiating all middleware for every request.

Partial application with through(): Some pipeline implementations let you set middleware separately from running:

php
$pipeline->through($middleware)->send($request)->via('process')->thenReturn();

Laravel's Pipeline class uses this API.

Short-circuit: Any middleware can return a Response WITHOUT calling $next->handle(). This aborts the pipeline. Used by auth middleware (return 401 if not authenticated) and rate limiting.

Code Example

php
<?php
namespace Framework\Http;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Framework\Container\Container;

class Pipeline implements RequestHandlerInterface
{
    private array $middleware = [];
    private ?RequestHandlerInterface $handler = null;

    public function __construct(private readonly Container $container) {}

    public function through(array $middleware): static
    {
        $this->middleware = $middleware;
        return $this;
    }

    public function then(RequestHandlerInterface $handler): static
    {
        $this->handler = $handler;
        return $this;
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return $this->buildChain()->handle($request);
    }

    public function run(ServerRequestInterface $request): ResponseInterface
    {
        return $this->handle($request);
    }

    private function buildChain(): RequestHandlerInterface
    {
        // Start from the terminal handler and wrap outward
        $handler = $this->handler;
        foreach (array_reverse($this->middleware) as $mw) {
            $resolved = $this->resolve($mw);
            $handler  = new class($resolved, $handler) implements RequestHandlerInterface {
                public function __construct(
                    private readonly MiddlewareInterface     $mw,
                    private readonly RequestHandlerInterface $next,
                ) {}
                public function handle(ServerRequestInterface $request): ResponseInterface
                {
                    return $this->mw->process($request, $this->next);
                }
            };
        }
        return $handler;
    }

    private function resolve(string|MiddlewareInterface $mw): MiddlewareInterface
    {
        if ($mw instanceof MiddlewareInterface) return $mw;
        return $this->container->make($mw);
    }
}

// Usage in the application
$response = (new Pipeline($container))
    ->through([
        \Framework\Http\Middleware\LoggingMiddleware::class,
        \Framework\Http\Middleware\AuthMiddleware::class,
        \Framework\Http\Middleware\CorsMiddleware::class,
    ])
    ->then(new RouteHandler($action))
    ->run($request);