1.1. Chain of responsibility

1.1.1. Intent

According to the Gang of Four, the Chain of Responsibility pattern is a way to “avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 223).

1.1.2. When to use it?

You should use the Chain of Responsibility pattern if multiple receivers are able to handle a request and if you don’t need to explicitly specify which receiver has to handle a given request (not known in advance and determined automatically). Moreover, it allows you to keep senders and receivers decoupled.

Keep in mind that the chain doesn’t ensure that a request will be handled before the end of the chain.

1.1.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Chain of Responsibility pattern

1.1.4. Implementation

Order.php

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
<?php

namespace Phpatterns\Behavioral\ChainOfResponsibility;

class Order
{
    const ORDER_STATUS_PENDING = 1; //default
    const ORDER_STATUS_PAID = 2;
    const ORDER_STATUS_PREPARED = 3;
    const ORDER_STATUS_SHIPPED = 4;
    const ORDER_STATUS_DELIVERED = 5;
    const ORDER_STATUS_CANCELED = 6;

    /** @var int */
    private $uid;

    /** @var int */
    private $status;

    /** @var string */
    private $trackingNumber;

    /** @var string */
    private $customerFirstName;

    /** @var string */
    private $customerLastName;

    /**
     * ...
     * Other useful fields: address, items, quantities, VAT, etc.
     * ...
     */

    /**
     * @param int $uid
     * @param string $firstName
     * @param string $lastName
     */
    public function __construct($uid, $firstName, $lastName)
    {
        $this->uid = $uid;
        $this->customerFirstName = $firstName;
        $this->customerLastName = $lastName;
        $this->status = self::ORDER_STATUS_PENDING;
        $this->trackingNumber = null;
    }

    /**
     * @return int
     */
    public function getUid()
    {
        return $this->uid;
    }

    /**
     * @return string
     */
    public function getCustomerFirstName()
    {
        return $this->customerFirstName;
    }

    /**
     * @return string
     */
    public function getCustomerLastName()
    {
        return $this->customerLastName;
    }

    /**
     * @return int
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @param int $status
     * @return $this
     */
    public function setStatus($status)
    {
        $this->status = $status;
        return $this;
    }

    /**
     * @return string
     */
    public function getTrackingNumber()
    {
        return $this->trackingNumber;
    }

    /**
     * @param string $trackingNumber
     * @return $this
     */
    public function setTrackingNumber($trackingNumber)
    {
        $this->trackingNumber = $trackingNumber;
        return $this;
    }
}

AbstractOrderNotification.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

namespace Phpatterns\Behavioral\ChainOfResponsibility;

abstract class AbstractOrderNotification
{
    /** @var int */
    protected $orderStatus;

    /** @var AbstractOrderNotification */
    protected $followingOrderNotification;

    /**
     * Add another OrderNotification object (another responsibility) to the end of the chain
     * @param AbstractOrderNotification $orderNotification
     */
    public function appendNotification(AbstractOrderNotification $orderNotification)
    {
        $this->followingOrderNotification = $orderNotification;
    }

    /**
     * @param Order $order
     * @return string
     */
    public function handleOrder(Order $order)
    {
        if ($order->getStatus() === $this->orderStatus) {
            return $this->sendNotification($order);
        } elseif (! is_null($this->followingOrderNotification)) {
            return $this->followingOrderNotification->handleOrder($order);
        }

        return '';
    }

    /**
     * @param Order $order
     * @return string
     */
    abstract protected function sendNotification(Order $order);
}

PaidOrderNotification.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace Phpatterns\Behavioral\ChainOfResponsibility\OrderNotification;

use Phpatterns\Behavioral\ChainOfResponsibility;

class PaidOrderNotification extends ChainOfResponsibility\AbstractOrderNotification
{
    public function __construct()
    {
        $this->orderStatus = ChainOfResponsibility\Order::ORDER_STATUS_PAID;
        $this->followingOrderNotification = null;
    }

    /**
     * @param ChainOfResponsibility\Order $order
     * @return string
     */
    public function sendNotification(ChainOfResponsibility\Order $order)
    {
        return sprintf(
            'Dear %s %s, thank you for your order (id: %d). We will ship it shortly!',
            $order->getCustomerFirstName(),
            $order->getCustomerLastName(),
            $order->getUid()
        );
    }
}

ShippedOrderNotification.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace Phpatterns\Behavioral\ChainOfResponsibility\OrderNotification;

use Phpatterns\Behavioral\ChainOfResponsibility;

class ShippedOrderNotification extends ChainOfResponsibility\AbstractOrderNotification
{
    public function __construct()
    {
        $this->orderStatus = ChainOfResponsibility\Order::ORDER_STATUS_SHIPPED;
        $this->followingOrderNotification = null;
    }

    /**
     * @param ChainOfResponsibility\Order $order
     * @return string
     */
    public function sendNotification(ChainOfResponsibility\Order $order)
    {
        return sprintf(
            'Dear %s %s, your order has been shipped! Your tracking number: %s',
            $order->getCustomerFirstName(),
            $order->getCustomerLastName(),
            $order->getTrackingNumber()
        );
    }
}

1.1.5. Tests

ChainOfResponsibilityTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php

namespace Test\Phpatterns\Behavioral\ChainOfResponsibility;

use Phpatterns\Behavioral\ChainOfResponsibility;
use Phpatterns\Behavioral\ChainOfResponsibility\OrderNotification;

class ChainOfResponsibilityTest extends \PHPUnit_Framework_TestCase
{
    /** @var ChainOfResponsibility\AbstractOrderNotification */
    private $chainOfResponsibility;

    protected function setUp()
    {
        $this->chainOfResponsibility = new OrderNotification\PaidOrderNotification();
        $this->chainOfResponsibility->appendNotification(new OrderNotification\ShippedOrderNotification());
    }

    public function testPaidOrderNotification()
    {
        $order = new ChainOfResponsibility\Order(12345, 'John', 'Doe');
        $order->setStatus(ChainOfResponsibility\Order::ORDER_STATUS_PAID);

        $this->assertSame(
            'Dear John Doe, thank you for your order (id: 12345). We will ship it shortly!',
            $this->chainOfResponsibility->handleOrder($order)
        );
    }

    public function testShippedOrderNotification()
    {
        $order = new ChainOfResponsibility\Order(56789, 'Albert', 'Einstein');
        $order
            ->setStatus(ChainOfResponsibility\Order::ORDER_STATUS_SHIPPED)
            ->setTrackingNumber('X4RT657P5');

        $this->assertSame(
            'Dear Albert Einstein, your order has been shipped! Your tracking number: X4RT657P5',
            $this->chainOfResponsibility->handleOrder($order)
        );
    }

    /**
     * There is no responsibility set for a canceled order.
     * No notification will be sent.
     */
    public function testCanceledOrderNotification()
    {
        $order = new ChainOfResponsibility\Order(34541, 'Denzel', 'Washington');
        $order->setStatus(ChainOfResponsibility\Order::ORDER_STATUS_CANCELED);

        $this->assertSame('', $this->chainOfResponsibility->handleOrder($order));
    }
}