PHP Design Patterns: Observer Pattern
This is the eight post in a series of articles on how to implement design patterns in PHP.
In this part, we’ll see how we can implement the Observer Pattern.
As the name suggests, the Observer Pattern is used for notifying other parts of your code when a specific event happens. It’s a way of decoupling your code so that you don’t have to explicitly call on methods from different classes whenever something happens.
The Observer Pattern is so useful that PHP has its own class for implementing it.
To implement the Observer Pattern, we have two components:
- Subject - this is the one that performs a specific action.
- Observer - this is the one which listens for when a specific action happens on the subject, thus also triggering its own action.
The relationship between a subject and an observer is one-to-many. So a subject can have many observers.
Let’s start by defining the class which will interact with the subject:
<?php
// observerpattern/app/Client.php
namespace ObserverPattern\App;
use ObserverPattern\App\Store;
class Client
{
public function buyFrom(Store $store, string $item)
{
$store->sellItem($item);
}
}
Next, we define our subject. As mentioned earlier, PHP has a built-in classes for easily implementing the Observer Pattern:
- SplObjectStorage - instead of a standard array, we use this for storing observers for a specific subject.
- SplSubject - used for implementing the Observer Pattern for dealing with the subject side of things.
- SplObserver - the observer counterpart of
SplSubject
.
All classes which implements SplSubject
needs to define the following methods:
attach
- for attaching observers. This is how observers are linked to a specific subject.detach
- for detaching an observer.notify
- for notifying an observer when an event happens.
<?php
// observerpattern/app/Store.php
namespace ObserverPattern\App;
use SplSubject;
use SplObjectStorage;
use SplObserver;
class Store implements SplSubject
{
private $observers;
private $item;
public function __construct()
{
$this->observers = new SplObjectStorage();
}
public function sellItem(string $item)
{
$this->item = $item;
$this->notify();
}
public function getItem()
{
return $this->item;
}
public function attach(SplObserver $observer): void
{
$this->observers->attach($observer);
}
public function detach(SplObserver $observer): void
{
$key = array_search($observer, $this->observers);
if ($key !== false) {
$this->observers[$key];
}
}
public function notify(): void
{
if ($this->observers->count() > 0)
{
foreach ($this->observers as $value)
{
$value->update($this);
}
}
}
}
Observers need to implement the SplObserver
class. All this requires is for the class to define an update
method. If you’ve noticed earlier, we call this method for each observer when the notify
method is called in the subject class. In the Inventory observer, this will simply get the item that was bought and removes it from the $items
array:
<?php
// observerpattern/app/Observers/Inventory.php
namespace ObserverPattern\App\Observers;
use SplObserver;
use SplSubject;
class Inventory implements SplObserver
{
private $items = [
'star apple',
'sugar apple',
'meyer lemon',
'mango',
'java plum',
'spanish plum',
];
public function update(SplSubject $subject): void
{
$key = array_search($subject->getItem(), $this->items);
if ($key !== false) {
unset($this->items[$key]);
$this->items = array_values($this->items);
}
}
public function getItems()
{
return $this->items;
}
}
We also have another observer called Mailer
, all this does is push a new item to the $sent_mail
array when its update
method is called:
<?php
// observerpattern/app/Observer/Mailer.php
namespace ObserverPattern\App\Observers;
use SplObserver;
use SplSubject;
class Mailer implements SplObserver
{
private $sent_mail = [];
public function update(SplSubject $subject): void
{
$this->sent_mail[] = $subject->getItem();
}
public function getSentMail()
{
return $this->sent_mail;
}
}
Here’s the client code. First, we initialize everything we need then assert the initial value we expect. Then we trigger the action on the subject, in this case it’s the buyFrom
method. This then calls the update
method in each of the attached observers. We expect that to remove the “mango” from the items in the inventory and add it to the sent mail of the mailer:
<?php
// observerpattern/ObserverTest.php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPUnit\Framework\TestCase;
class ObserverTest extends TestCase
{
public function test_observer() : void
{
$client = new ObserverPattern\App\Client;
$store = new ObserverPattern\App\Store;
$inventory = new ObserverPattern\App\Observers\Inventory;
$mailer = new ObserverPattern\App\Observers\Mailer;
$this->assertEquals($inventory->getItems(), [
'star apple',
'sugar apple',
'meyer lemon',
'mango',
'java plum',
'spanish plum',
]);
$this->assertEquals($mailer->getSentMail(), []);
$store->attach($inventory);
$store->attach($mailer);
$client->buyFrom($store, 'mango');
$this->assertEquals($inventory->getItems(), [
'star apple',
'sugar apple',
'meyer lemon',
'java plum',
'spanish plum',
]);
$this->assertEquals($mailer->getSentMail(), ['mango']);
}
}
To run this. Be sure you have the following on your composer.json
file then run composer dump-autoload
:
"autoload": {
"psr-4": {
"ObserverPattern\\App\\": "observerpattern/app"
}
},
Also install phpunit (composer require phpunit/phpunit
) if you haven’t done so already.
Then you can run the tests:
vendor/bin/phpunit observerpattern/ObserverTest.php
That’s it for the Observer Pattern. You basically have to create a one-to-many relationship between between a subject and its observers. So that when something happens to the subject, all of its observers can immediately be notified so they could do their own thing.
You can find the source code used in this tutorial on this GitHub repo.