Chain of Responsibility — passing requests down a handler chain
Concept
Chain of Responsibility passes a request along a chain of handlers. Each handler decides to process the request or pass it to the next handler. No single handler needs to know about the whole chain.
The problem: You have a support ticket system. Level 1 support handles basic issues. Level 2 handles complex issues. Level 3 handles critical issues. With a chain: each level tries to handle the ticket — if it can't, passes to the next level.
Structure:
- Handler Interface:
handle($request)— returns a result OR null (pass to next).setNext(HandlerInterface $handler)— links the chain. - Concrete Handlers: Each implements handling logic. If they can't handle it, call
$this->next?->handle($request). - Chain construction:
$handler1->setNext($handler2->setNext($handler3)).
Two approaches to "can't handle":
- Return null (let the caller check) — return
$this->next?->handle($request) ?? null. - Throw an exception — last handler throws if no one handled the request.
Laravel Middleware: The most prominent PHP example. Each middleware either processes the request (possibly short-circuiting) or passes to the next middleware via $next($request). The route action is the last "handler".
Chain of Responsibility vs Strategy: Strategy selects ONE algorithm. CoR tries handlers in sequence until one succeeds. CoR is also similar to the Decorator pattern but Decorator always calls through — CoR can stop.
Real-world uses: HTTP middleware, access control levels, event handling, DOM event bubbling, support escalation, validation pipelines.
Code Example
<?php
// Handler Interface
interface SupportHandler
{
public function setNext(SupportHandler $handler): SupportHandler;
public function handle(SupportTicket $ticket): ?string;
}
abstract class AbstractSupportHandler implements SupportHandler
{
private ?SupportHandler $next = null;
public function setNext(SupportHandler $handler): SupportHandler
{
$this->next = $handler;
return $handler; // fluent for chaining
}
protected function passToNext(SupportTicket $ticket): ?string
{
return $this->next?->handle($ticket);
}
}
class SupportTicket
{
public function __construct(
public readonly string $description,
public readonly string $priority, // 'low', 'medium', 'high', 'critical'
) {}
}
// Concrete Handlers
class Level1Support extends AbstractSupportHandler
{
public function handle(SupportTicket $ticket): ?string
{
if ($ticket->priority === 'low') {
return "Level 1 resolved: '{$ticket->description}'";
}
echo "Level 1 cannot handle {$ticket->priority} priority. Escalating...\n";
return $this->passToNext($ticket);
}
}
class Level2Support extends AbstractSupportHandler
{
public function handle(SupportTicket $ticket): ?string
{
if (in_array($ticket->priority, ['low', 'medium'])) {
return "Level 2 resolved: '{$ticket->description}'";
}
echo "Level 2 cannot handle {$ticket->priority} priority. Escalating...\n";
return $this->passToNext($ticket);
}
}
class Level3Support extends AbstractSupportHandler
{
public function handle(SupportTicket $ticket): ?string
{
// Level 3 handles everything
return "Level 3 (senior engineer) resolved: '{$ticket->description}'";
}
}
// Build the chain
$l1 = new Level1Support();
$l2 = new Level2Support();
$l3 = new Level3Support();
$l1->setNext($l2)->setNext($l3); // fluent chain building
// Test tickets
$tickets = [
new SupportTicket('Password reset help', 'low'),
new SupportTicket('API integration issue', 'medium'),
new SupportTicket('Data breach suspected', 'critical'),
];
foreach ($tickets as $ticket) {
$result = $l1->handle($ticket); // always start at level 1
echo "Result: {$result}\n\n";
}