0

Pipeline pattern — the chain of responsibility

Intermediate5 min read·fw-05-001
compare

Concept

The pipeline pattern (also called chain of responsibility) passes a request through a series of handlers, each deciding whether to process the request, modify it, or pass it to the next handler. Middleware in web frameworks is an application of this pattern.

Key participants:

  • Handler/Stage: A unit of processing. Receives the request and a reference to the "next" handler.
  • Pipeline: Assembles the chain and provides the entry point.
  • Terminator: The final handler — typically the actual route action.

Execution model: Middleware wraps the next handler:

text
Middleware A wraps:
  Middleware B wraps:
    Middleware C wraps:
      Route Action

When the pipeline runs, A executes first. A can run code before calling next(), which invokes B. B can run before calling next(), which invokes C. C calls next(), which runs the route action. Then control returns back up: C continues after next(), then B, then A.

"Before" and "after" behavior: Code before $next($request) runs going inward (before the action). Code after $next($request) runs coming back out (after the action). This is how logging middleware captures both request and response.

Return value: The handler returns a Response. The response flows back up through the chain. Each middleware can inspect or modify the Response on the way back.

Pure function alternative: Each stage is fn(Request $req, callable $next): Response. No objects needed.

Code Example

php
<?php
// Functional pipeline — simplest form
function pipeline(array $middleware, callable $handler): callable
{
    return array_reduce(
        array_reverse($middleware),
        fn($carry, $mw) => fn($request) => $mw($request, $carry),
        $handler
    );
}

$middleware = [
    // Each stage: fn(Request $req, callable $next): Response
    function($request, $next) {
        // BEFORE: log request
        echo "→ Request: {$request->getMethod()} {$request->getUri()}\n";
        $response = $next($request); // call next in chain
        // AFTER: log response
        echo "← Response: {$response->getStatus()}\n";
        return $response;
    },
    function($request, $next) {
        // Authentication check
        $token = $request->getHeader('authorization');
        if (!$token) {
            return new \Framework\Http\Response('Unauthorized', 401);
        }
        // Add user to request and continue
        $request = $request->withAttribute('user', ['id' => 1, 'name' => 'Alice']);
        return $next($request);
    },
];

$handler = fn($request) => new \Framework\Http\Response('Hello, ' . $request->getAttribute('user')['name']);

$pipeline = pipeline($middleware, $handler);
$response = $pipeline($request);

// Execution order:
// 1. Logger BEFORE runs
// 2. Auth check runs
// 3. Handler runs (returns Response)
// 4. Auth returns Response (no after code)
// 5. Logger AFTER runs with the Response
// 6. Logger returns Response to caller