3.8. Observer

3.8.1. Purpose

To implement a publish/subscribe behaviour to an object, whenever a “Subject” object changes its state, the attached “Observers” will be notified. It is used to shorten the amount of coupled objects and uses loose coupling instead.

3.8.2. Examples

  • a message queue system is observed to show the progress of a job in a GUI

3.8.3. Note

PHP already defines two interfaces that can help to implement this pattern: SplObserver and SplSubject.

3.8.4. UML Diagram

Alt Observer UML Diagram

3.8.5. Code

You can also find this code on GitHub

User.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Observer;
 6
 7use SplSubject;
 8use SplObjectStorage;
 9use SplObserver;
10
11/**
12 * User implements the observed object (called Subject), it maintains a list of observers and sends notifications to
13 * them in case changes are made on the User object
14 */
15class User implements SplSubject
16{
17    private SplObjectStorage $observers;
18    private $email;
19
20    public function __construct()
21    {
22        $this->observers = new SplObjectStorage();
23    }
24
25    public function attach(SplObserver $observer): void
26    {
27        $this->observers->attach($observer);
28    }
29
30    public function detach(SplObserver $observer): void
31    {
32        $this->observers->detach($observer);
33    }
34
35    public function changeEmail(string $email): void
36    {
37        $this->email = $email;
38        $this->notify();
39    }
40
41    public function notify(): void
42    {
43        /** @var SplObserver $observer */
44        foreach ($this->observers as $observer) {
45            $observer->update($this);
46        }
47    }
48}

UserObserver.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Observer;
 6
 7use SplObserver;
 8use SplSubject;
 9
10class UserObserver implements SplObserver
11{
12    /**
13     * @var SplSubject[]
14     */
15    private array $changedUsers = [];
16
17    /**
18     * It is called by the Subject, usually by SplSubject::notify()
19     */
20    public function update(SplSubject $subject): void
21    {
22        $this->changedUsers[] = clone $subject;
23    }
24
25    /**
26     * @return SplSubject[]
27     */
28    public function getChangedUsers(): array
29    {
30        return $this->changedUsers;
31    }
32}

3.8.6. Test

Tests/ObserverTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Observer\Tests;
 6
 7use DesignPatterns\Behavioral\Observer\User;
 8use DesignPatterns\Behavioral\Observer\UserObserver;
 9use PHPUnit\Framework\TestCase;
10
11class ObserverTest extends TestCase
12{
13    public function testChangeInUserLeadsToUserObserverBeingNotified()
14    {
15        $observer = new UserObserver();
16
17        $user = new User();
18        $user->attach($observer);
19
20        $user->changeEmail('foo@bar.com');
21        $this->assertCount(1, $observer->getChangedUsers());
22    }
23}