Chain of Responsibility Pattern
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints note that automated detection is 'no' and phpstan is listed as a tool but cannot reliably detect structural misuse like missing default handlers or stateful handler reuse — these surface only through careful code review or runtime testing when requests silently disappear or race conditions manifest.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes building a handler chain, but common_mistakes like stateful handlers reused across chains or chains that are too long require revisiting every handler class, adjusting the chain construction, and potentially refactoring across multiple files. It's more than a one-line fix but not necessarily a full architectural rework.
Closest to 'persistent productivity tax' (b5). The pattern applies to both web and cli contexts. Once adopted, every new handler must conform to the chain interface and ordering decisions affect system behaviour. It imposes an ongoing design overhead — new team members must understand chain composition, handler ordering, and the absence of a default handler — but it doesn't define the entire system's shape.
Closest to 'notable trap (a documented gotcha most devs eventually learn)' (t5). The misconception field directly identifies the core trap: developers conflate CoR with middleware, assuming handlers always pass the request along unconditionally. In reality CoR short-circuits when a handler processes the request. This is a well-documented gotcha that experienced developers learn but frequently trips up those coming from middleware-centric frameworks like PSR-15.
Also Known As
TL;DR
Explanation
The Chain of Responsibility decouples senders from receivers by giving multiple objects the chance to handle a request. Each handler in the chain either processes the request and stops, or passes it to the next handler. This is the basis of PHP middleware pipelines (PSR-15), event propagation chains, and request processing pipelines. It replaces large if/elseif chains with composable, single-responsibility handler objects. The chain can be built dynamically, handlers can be reordered, and new handlers can be added without modifying existing ones — following Open/Closed Principle.
Diagram
flowchart LR
REQ2[Request] --> H1[Handler 1<br/>Auth check]
H1 -->|pass| H2[Handler 2<br/>Rate limit]
H2 -->|pass| H3[Handler 3<br/>Validation]
H3 -->|pass| H4[Handler 4<br/>Business logic]
H1 -->|fail| R1[401 Unauthorized]
H2 -->|fail| R2[429 Rate limited]
H3 -->|fail| R3[422 Invalid input]
subgraph PHP_Examples
MW[Laravel Middleware pipeline]
PIPE[PHP League Pipeline]
end
style H4 fill:#238636,color:#fff
style R1 fill:#f85149,color:#fff
style R2 fill:#f85149,color:#fff
style R3 fill:#f85149,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Not providing a default handler at the end of the chain — unhandled requests silently disappear.
- Making handlers stateful and reusing them across chains — causes race conditions and unexpected behaviour.
- Using the pattern when a simple switch or strategy would be clearer — over-engineering for a handful of cases.
- Chains that are too long causing performance issues — every request traverses the full chain even for early matches.
Code Examples
// Monolithic if-else instead of chain — hard to extend:
function handle(Request $r): Response {
if ($r->type === 'auth') return $this->handleAuth($r);
elseif ($r->type === 'cache') return $this->handleCache($r);
elseif ($r->type === 'log') return $this->handleLog($r);
// Adding a new type requires modifying this method
}
abstract class Handler {
private ?Handler $next = null;
public function setNext(Handler $h): Handler { $this->next = $h; return $h; }
protected function passOn(Request $req): ?Response {
return $this->next?->handle($req);
}
abstract public function handle(Request $req): ?Response;
}
class AuthHandler extends Handler {
public function handle(Request $req): ?Response {
if (!$req->user()) return new Response(401);
return $this->passOn($req);
}
}
class RateLimitHandler extends Handler {
public function handle(Request $req): ?Response {
if ($this->isThrottled($req)) return new Response(429);
return $this->passOn($req);
}
}
$auth = new AuthHandler();
$auth->setNext(new RateLimitHandler())->setNext(new AppHandler());