0

Chain of Responsibility — passing requests down a handler chain

Intermediate5 min read·eng-04-004
interviewlaravel-srccompare

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":

  1. Return null (let the caller check) — return $this->next?->handle($request) ?? null.
  2. 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
<?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";
}