In this article, we will explain how to implement the Chain of Responsibility (CoR) design pattern to properly structure and design a system. This pattern helps to achieve loose coupling and allows compliance with such SOLID principles as SRP and OCP. Your code will become cleaner and easier to support for the majority of tasks that can be implemented with this pattern.
What is CoR
Chain of Responsibility is a behavioral design pattern whose main task is to build a chain of objects (called handlers) to sequentially process input data. Upon receiving a request, each handler should independently decide to either process the request or pass it to the next handler. Any handler can be responsible for the performance of the whole chain, and, therefore, their sequence defines if a handler will stop any further processing. The pattern is represented on the scheme below:
Chain of Responsibility:
+-------------------------------------------+
+-------+ | +---------+ +---------+ +---------+ | +--------+
| Input | -> | | Handler | -> | Handler | -> | Handler | | -> | Output |
+-------+ | +---------+ +---------+ +---------+ | +--------+
+-------------------------------------------+
Any data, from scalar values to objects, can be processed in these responsibility chains. The main factor is that handlers can receive and process the data. There are also no limits in terms of output data, which can be typed as needed. The handlers in the chain should know nothing about one another so that the chain can be configured in the client's code without dependencies.
The main conditions of the pattern’s implementation are:
- A handler should be able to process the data of the predefined type.
- A handler decides whether the data can be passed along the chain.
- Handlers in the chain should be linked by their responsibility.
- All handlers should be independent of one another.
- A handler can initiate exceptions if necessary.
Let’s take a look at three ways of implementing this pattern: Chain, Pipeline, and Middleware. They have much in common but solve the tasks in their own ways.
Chain implementation
I found the first implementation in the crocodile2u/chainy package. It helps sequentially go through all the added handlers, passing the input value to each after it’s processed by the previous handler. Look at the snippet below to convert the idea into code:
class Chain
{
/** @var callable[] */
private array $handlers = [];
public function add(callable $handler): self
{
$self = clone $this;
$self->handlers[] = $handler;
return $self;
}
public function apply(int $payload): int
{
foreach ($this->handlers as $handler) {
$payload = $handler($payload);
}
return $payload;
}
}
As you can see, introduction of new handlers in the chain is based on a fluent interface that simplifies building the chain and makes the code more legible. You can add any callable function with the right signature to the chain:
$chain = (new Chain())
->add(fn(int $payload): int => $payload + 5)
->add(new class {
public function __invoke(int $payload): int {
if (5 < $payload) {
throw new \LogicException();
}
return $payload + 2;
}
})
;
The client’s code should initiate the handlers’ chain and catch exceptions:
try {
$result = $chain->apply(1);
} catch (\LogicException $e) {
// ...
}
This is the easiest and most popular way to implement Chain of Responsibility. In most cases, I will prefer this method because it’s easy to apply and not complex for other developers to understand.
Pipeline implementation
I took this implementation from the league/pipeline package, which is very similar to the previous one, with the only difference being that all callables are processed through the Processor
interface. Processor
decides how to process the callable chain and what output it should return. This implementation method implies the following file structure:
.
├── Chain
├── Processor
├── FingersCrossedProcessor
└── InterruptibleProcessor
The Chain
class collects handlers via a fluent interface and then sends them to Processor
, together with the payload:
class Chain
{
/** @var callable[] */
private array $handlers = [];
public function __construct(
private Processor $processor,
) {
}
public function add(callable $handler): self
{
$self = clone $this;
$self->handlers[] = $handler;
return $self;
}
public function apply(int $payload): int
{
return $this->processor->process($payload, ...$this->handlers);
}
}
The Processor
interface specifies the type of the input as well as the resulting output, and takes a set of callable functions as a second argument:
interface Processor
{
public function process(int $payload, callable ...$handlers): int;
}
FingersCrossedProcessor
works the same as the Chain implementation, iterating through all the handlers and passing the value to the each handler in the chain:
class FingersCrossedProcessor implements Processor
{
public function process(int $payload, callable ...$handlers): int
{
foreach ($handlers as $handler) {
$payload = $handler($payload);
}
return $payload;
}
}
InterruptibleProcessor
includes logic that defines whether to break the handlers’ chain and return the current result:
class InterruptibleProcessor implements Processor
{
public function __construct(
private callable $checker,
) {
}
public function process(int $payload, callable ...$handlers): int
{
foreach ($handlers as $handler) {
$payload = $handler($payload);
if (false === ($this->checker)($payload)) {
return $payload;
}
}
return $payload;
}
}
To utilize one of the processors, you should send it to Chain
, build a chain by adding handlers, and apply the chain to the payload:
$processor = new InterruptibleProcessor(function (int $payload): bool {
return 8 > $payload;
});
$chain = (new Chain($processor))
->add(fn(int $payload): int => $payload + 5)
->add(fn(int $payload): int => $payload + 2) // -> break here
->add(fn(int $payload): int => $payload + 3)
;
try {
$result = $chain->apply(1);
} catch (\Throwable $e) {
// ...
}
This implementation looks more interesting since we can now replace the method of calling the same chain of handlers by applying different processors. On the other hand, the Pipeline version is redundant if you only need to send the payload to each handler.
Middleware implementation
This implementation comes from the relay/relay package, which is a PSR-15: HTTP Server Request Handler. The package implements sequential processing of a queue of PSR-15 middleware entries. The file structure in your project can be as follows:
.
├── Input
├── Output
├── Middleware
└── Broker
Input
is a class to read data only; however, it can be modified if necessary. The Output
class is used to record the output data. In this example, you can only assign properties for both classes:
class Input extends \stdClass
{
}
class Output extends \stdClass
{
}
The Middleware
interface is defined by the handler, which receives Input
and Broker
and returns Output
. The Broker
class is the orchestrator of all middleware entries. Since we have access to Broker
from the Middleware
interface, every handler decides for itself if it can launch the following middleware entry:
interface Middleware
{
public function process(Input $input, Broker $next): Output;
}
The Broker
class receives Input
and sequentially launches each middleware entry. When the queue reaches the end, it returns Output
:
class Broker
{
public function __construct(
private \SplQueue $queue,
) {
}
public function handle(Input $input): Output
{
/** @var Middleware $middleware */
$middleware = $this->queue->dequeue();
return $middleware->process($input, $this);
}
}
The peculiarity of this implementation method is that it uses \SplQueue
, and each middleware entry calls the next before doing its own work. Therefore, the added middleware entries will be processed in reverse from how they are added to the queue. So, we need to ensure that the Output
instance is created in the very first middleware:
$queue = new \SplQueue();
$queue[] = new MiddlewareX();
$queue[] = new MiddlewareY();
$queue[] = new MiddlewareZ(); // -> must return new Output()
$broker = new Broker($queue);
$input = new Input();
/** @var Output $output */
$output = $broker->handle($input);
The first middleware entry processed (the last one added) should always ensure the initialization of the result, in our case, Output
. Simply speaking, the first middleware is like a manufacturing unit that creates the output data with a default state:
class MiddlewareZ implements Middleware
{
public function process(Input $input, Broker $next): Output
{
return new Output();
}
}
The rest of the middleware entries should first pass the call along the chain and then do any needed work on the output, or possibly not do any work at all:
class MiddlewareX implements Middleware
{
public function process(Input $input, Broker $next): Output
{
$output = $next->handle($input);
$output->payload = 42;
return $output;
}
}
This implementation allows improving the work with a queue: define the type signature for the middleware, set up a sequence of middleware entries, and make it possible to reuse the queue. In my opinion, it’s the most interesting implementation method as we can decide what data processing is needed in the handlers.
Summary
We discussed three implementations of the Chain of Responsibility design pattern: Chain, Pipeline, and Middleware. Each implementation method allows you to split the code according to the SRP principle if sequential data processing is necessary. We can make code extensions possible, according to the OCP principle, as well as decrease the dependency between classes and dynamically compose chains of handlers.
The main idea of the Chain of Responsibility pattern is to build a chain of handlers to sequentially process requests, where each handler explicitly or implicitly makes a decision of whether to pass the request to the next handler. Each handler should be able to receive and return data of a certain type as well as process this data.