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
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}