3.1. Chain Of Responsibilities

3.1.1. Purpose

To build a chain of objects to handle a call in sequential order. If one object cannot handle a call, it delegates the call to the next in the chain and so forth.

3.1.2. Examples

  • logging framework, where each chain element decides autonomously what to do with a log message

  • a Spam filter

  • Caching: first object is an instance of e.g. a Memcached Interface, if that «misses» it delegates the call to the database interface

3.1.3. UML Diagram

Alt ChainOfResponsibility UML Diagram

3.1.4. Code

You can also find this code on GitHub

Handler.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
 6
 7use Psr\Http\Message\RequestInterface;
 8
 9abstract class Handler
10{
11    public function __construct(private ?Handler $successor = null)
12    {
13    }
14
15    /**
16     * This approach by using a template method pattern ensures you that
17     * each subclass will not forget to call the successor
18     */
19    final public function handle(RequestInterface $request): ?string
20    {
21        $processed = $this->processing($request);
22
23        if ($processed === null && $this->successor !== null) {
24            // the request has not been processed by this handler => see the next
25            $processed = $this->successor->handle($request);
26        }
27
28        return $processed;
29    }
30
31    abstract protected function processing(RequestInterface $request): ?string;
32}

Responsible/FastStorage.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
 6
 7use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
 8use Psr\Http\Message\RequestInterface;
 9
10class HttpInMemoryCacheHandler extends Handler
11{
12    public function __construct(private array $data, ?Handler $successor = null)
13    {
14        parent::__construct($successor);
15    }
16
17    protected function processing(RequestInterface $request): ?string
18    {
19        $key = sprintf(
20            '%s?%s',
21            $request->getUri()->getPath(),
22            $request->getUri()->getQuery()
23        );
24
25        if ($request->getMethod() == 'GET' && isset($this->data[$key])) {
26            return $this->data[$key];
27        }
28
29        return null;
30    }
31}

Responsible/SlowStorage.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
 6
 7use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
 8use Psr\Http\Message\RequestInterface;
 9
10class SlowDatabaseHandler extends Handler
11{
12    protected function processing(RequestInterface $request): ?string
13    {
14        // this is a mockup, in production code you would ask a slow (compared to in-memory) DB for the results
15
16        return 'Hello World!';
17    }
18}

3.1.5. Test

Tests/ChainTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests;
 6
 7use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
 8use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\HttpInMemoryCacheHandler;
 9use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowDatabaseHandler;
10use PHPUnit\Framework\TestCase;
11use Psr\Http\Message\RequestInterface;
12use Psr\Http\Message\UriInterface;
13
14class ChainTest extends TestCase
15{
16    private Handler $chain;
17
18    protected function setUp(): void
19    {
20        $this->chain = new HttpInMemoryCacheHandler(
21            ['/foo/bar?index=1' => 'Hello In Memory!'],
22            new SlowDatabaseHandler()
23        );
24    }
25
26    public function testCanRequestKeyInFastStorage()
27    {
28        $uri = $this->createMock(UriInterface::class);
29        $uri->method('getPath')->willReturn('/foo/bar');
30        $uri->method('getQuery')->willReturn('index=1');
31
32        $request = $this->createMock(RequestInterface::class);
33        $request->method('getMethod')
34            ->willReturn('GET');
35        $request->method('getUri')->willReturn($uri);
36
37        $this->assertSame('Hello In Memory!', $this->chain->handle($request));
38    }
39
40    public function testCanRequestKeyInSlowStorage()
41    {
42        $uri = $this->createMock(UriInterface::class);
43        $uri->method('getPath')->willReturn('/foo/baz');
44        $uri->method('getQuery')->willReturn('');
45
46        $request = $this->createMock(RequestInterface::class);
47        $request->method('getMethod')
48            ->willReturn('GET');
49        $request->method('getUri')->willReturn($uri);
50
51        $this->assertSame('Hello World!', $this->chain->handle($request));
52    }
53}