1.7. Observer

1.7.1. Intent

According to the Gang of Four, the Observer pattern defines “a one-to-many dependency between objects so that when one object [the subject] changes state, all its dependents [the observers] are notified and updated automatically” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 293).

Traditionally observers will be notified by calling one of their methods (like update() in the examples below). To illustrate it, I have decided to write code where observers want to monitor products’ prices. Each time the price evolves, the observers will be notified!

1.7.2. When to use it?

The Observer pattern allows you to apply the Open Closed Principle (OCP) which states that you should be able to extend classes without modifying them (thanks to Bertrand Meyer for that principle).

Indeed you can register an unlimited number of observers without the need to change the class of the Subject (you may simply don’t have any idea of how many Observers objects will be linked to the Subject).

In a nutshell, you should use the Observer pattern when you don’t want your objects to be tightly coupled (here, only one class, the Observer, is directly aware about the existence of the subject).

1.7.3. Diagram

Created using PhpStorm and yFiles.

1.7.3.1. Using Standard PHP Library (SPL)

Class diagram of the Observer pattern (using SPL)

1.7.3.2. Without Standard PHP Library (SPL)

Class diagram of the Observer pattern (without SPL)

1.7.4. Implementation

1.7.4.1. Using Standard PHP Library (SPL)

Product.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
<?php

namespace Phpatterns\Behavioral\Observer\UsingSPL;

/**
 * A Subject as defined in the Observer design pattern
 */
class Product implements \SplSubject
{
    /** @var int */
    private $price;

    /**
     * Observers attached to the Product
     *
     * @var \SplObjectStorage
     */
    private $observers;

    public function __construct()
    {
        $this->price = 10;
        $this->observers = new \SplObjectStorage();
    }

    /**
     * Attach an SplObserver
     * @link http://php.net/manual/en/splsubject.attach.php
     * @param \SplObserver $observer The SplObserver to attach.
     */
    public function attach(\SplObserver $observer)
    {
        $this->observers->attach($observer);
    }

    /**
     * Detach an observer
     * @link http://php.net/manual/en/splsubject.detach.php
     * @param \SplObserver $observer The SplObserver to detach.
     */
    public function detach(\SplObserver $observer)
    {
        $this->observers->detach($observer);
    }

    /**
     * Notify an observer
     * @link http://php.net/manual/en/splsubject.notify.php
     */
    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * Set the price for the product and notify observers
     * @param float $price
     */
    public function setPrice($price)
    {
        $this->price = $price;

        // notify the observers about the change
        $this->notify();
    }

    /**
     * Get the price for the product
     * @return int
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * Get observers
     * @return \SplObjectStorage
     */
    public function getObservers()
    {
        return $this->observers;
    }
}

ProductPriceObserver.php

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

namespace Phpatterns\Behavioral\Observer\UsingSPL;

/**
 * An Observer as defined in the Observer design pattern
 */
class ProductPriceObserver implements \SplObserver
{
    /**
     * Receive update from subject (our Product object)
     * @link http://php.net/manual/en/splobserver.update.php
     * @param \SplSubject $subject The SplSubject notifying the observer of an update.
     */
    public function update(\SplSubject $subject)
    {
        echo sprintf(
            'Product has a new price: %d€.',
            $subject->getPrice()
        );
    }
}

1.7.4.2. Without Standard PHP Library (SPL)

SubjectInterface.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
<?php

namespace Phpatterns\Behavioral\Observer\WithoutSPL;

/**
 * Interface SubjectInterface used to implement the Observer design pattern.
 * (have to be used with ObserverInterface)
 */
interface SubjectInterface
{
    /**
     * Attach an ObserverInterface
     * @param ObserverInterface $observer The observer to attach.
     */
    public function attach(ObserverInterface $observer);

    /**
     * Detach an observer
     * @param ObserverInterface $observer The observer to detach.
     */
    public function detach(ObserverInterface $observer);

    /**
     * Notify an observer
     */
    public function notify();
}

ObserverInterface.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

namespace Phpatterns\Behavioral\Observer\WithoutSPL;

/**
 * Interface ObserverInterface used to implement the Observer design pattern.
 * (have to be used with SubjectInterface)
 */
interface ObserverInterface
{
    /**
     * Receive update from subject
     * @param SubjectInterface $subject The subject notifying the observer of an update.
     */
    public function update(SubjectInterface $subject);
}

Product.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
<?php

namespace Phpatterns\Behavioral\Observer\WithoutSPL;

/**
 * A Subject as defined in the Observer design pattern
 */
class Product implements SubjectInterface
{
    /** @var int */
    private $price;

    /** @var array */
    private $observers;

    public function __construct()
    {
        $this->price = 10;
        $this->observers = [];
    }

    /**
     * Attach an SplObserver
     * @param ObserverInterface $observer The observer to attach.
     */
    public function attach(ObserverInterface $observer)
    {
        if (! in_array($observer, $this->observers, true)) {
            $this->observers[] = $observer;
        }
    }

    /**
     * Detach an observer
     * @param ObserverInterface $observer The observer to detach.
     */
    public function detach(ObserverInterface $observer)
    {
        $key = array_search($observer, $this->observers, true);
        if ($key !== false) {
            unset($this->observers[$key]);
        }
    }

    /**
     * Notify an observer
     */
    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * Set the price for the product and notify observers
     * @param float $price
     */
    public function setPrice($price)
    {
        $this->price = $price;

        // notify the observers about the change
        $this->notify();
    }

    /**
     * Get the price for the product
     * @return int
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * Get observers
     * @return array
     */
    public function &getObservers()
    {
        return $this->observers;
    }
}

ProductPriceObserver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php

namespace Phpatterns\Behavioral\Observer\WithoutSPL;

class ProductPriceObserver implements ObserverInterface
{
    /**
     * Receive update from subject
     * @param SubjectInterface $subject The subject notifying the observer of an update.
     */
    public function update(SubjectInterface $subject)
    {
        echo sprintf(
            'Product has a new price: %d€.',
            $subject->getPrice()
        );
    }
}

1.7.5. Tests

1.7.5.1. Using Standard PHP Library (SPL)

ProductTest.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
<?php

namespace Test\Phpatterns\Behavioral\Observer\UsingSPL;

use Phpatterns\Behavioral\Observer\UsingSPL;
use PHPUnit_Framework_TestCase;

class ProductTest extends PHPUnit_Framework_TestCase
{
    public function testAttachDetachMethods()
    {
        $product = new UsingSPL\Product();
        $productPriceObserver = new UsingSPL\ProductPriceObserver();
        $observers = $product->getObservers();

        $this->assertInstanceOf('SplObjectStorage', $observers);
        $this->assertFalse(
            $observers->contains($productPriceObserver)
        );

        $product->attach($productPriceObserver);
        $this->assertTrue(
            $observers->contains($productPriceObserver)
        );

        $product->detach($productPriceObserver);
        $this->assertFalse(
            $observers->contains($productPriceObserver)
        );
    }

    /**
     * Testing if notify method is called when modifying the product's price
     */
    public function testNotifyCall()
    {
        $productMock = $this->getMock(
            UsingSPL\Product::class,
            array('notify')
        );

        $productMock
            ->expects($this->once())
            ->method('notify');

        /** @var $productMock UsingSPL\Product */
        $productMock->setPrice(25);
    }
}

ProductPriceObserverTest.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
<?php

namespace Test\Phpatterns\Behavioral\Observer\UsingSPL;

use Phpatterns\Behavioral\Observer\UsingSPL;
use PHPUnit_Framework_TestCase;

class ProductPriceObserverTest extends PHPUnit_Framework_TestCase
{
    public function testObserverIsUpdated()
    {
        // Create a mock for the ProductPriceObserver class and only mock the update() method.
        $productPriceObserver = $this
            ->getMockBuilder(UsingSPL\ProductPriceObserver::class)
            ->setMethods(array('update'))
            ->getMock();

        // Set up the expectation for the update() method (called only once
        // and with an instance of SplSubject as its parameter).
        $productPriceObserver
            ->expects($this->once())
            ->method('update')
            ->with($this->isInstanceOf('SplSubject'));

        // Create the Product, attach the ProductPriceObserver object and notify it.
        $product = new UsingSPL\Product();
        $product->attach($productPriceObserver);
        $product->notify();
    }
}

1.7.5.2. Without Standard PHP Library (SPL)

ProductTest.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
<?php

namespace Test\Phpatterns\Behavioral\Observer\WithoutSPL;

use Phpatterns\Behavioral\Observer\WithoutSPL;
use PHPUnit_Framework_TestCase;

class ProductTest extends PHPUnit_Framework_TestCase
{
    public function testProductInstance()
    {
        $product = new WithoutSPL\Product();
        $this->assertInstanceOf(WithoutSPL\SubjectInterface::class, $product);
    }

    public function testAttachDetachMethods()
    {
        $product = new WithoutSPL\Product();
        $productPriceObserver = new WithoutSPL\ProductPriceObserver();
        $observers = &$product->getObservers();

        $this->assertTrue(is_array($observers));
        $this->assertFalse(
            in_array($productPriceObserver, $observers, true)
        );

        $product->attach($productPriceObserver);
        $this->assertTrue(
            in_array($productPriceObserver, $observers, true)
        );

        $product->detach($productPriceObserver);
        $this->assertFalse(
            in_array($productPriceObserver, $observers, true)
        );
    }

    /**
     * Testing if notify method is called when modifying the product's price
     */
    public function testNotifyCall()
    {
        $productMock = $this->getMock(
            WithoutSPL\Product::class,
            array('notify')
        );

        $productMock
            ->expects($this->once())
            ->method('notify');

        /** @var $productMock WithoutSPL\Product */
        $productMock->setPrice(25);
    }
}

ProductPriceObserverTest.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
<?php

namespace Test\Phpatterns\Behavioral\Observer\WithoutSPL;

use Phpatterns\Behavioral\Observer\WithoutSPL;
use PHPUnit_Framework_TestCase;

class ProductPriceObserverTest extends PHPUnit_Framework_TestCase
{
    public function testProductPriceObserverInstance()
    {
        $productPriceObserver = new WithoutSPL\ProductPriceObserver();
        $this->assertInstanceOf(WithoutSPL\ObserverInterface::class, $productPriceObserver);
    }

    public function testObserverIsUpdated()
    {
        // Create a mock for the ProductPriceObserver class and only mock the update() method.
        $productPriceObserver = $this
            ->getMockBuilder(WithoutSPL\ProductPriceObserver::class)
            ->setMethods(array('update'))
            ->getMock();

        // Set up the expectation for the update() method (called only once
        // and with an instance of SubjectInterface as its parameter).
        $productPriceObserver
            ->expects($this->once())
            ->method('update')
            ->with($this->isInstanceOf(WithoutSPL\SubjectInterface::class));

        // Create the Product, attach the ProductPriceObserver object and notify it.
        $product = new WithoutSPL\Product();
        $product->attach($productPriceObserver);
        $product->notify();
    }
}