Conceptual Example
This example illustrates the structure of the Observer design pattern and focuses on the following questions:
What classes does it consist of?
What roles do these classes play?
In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world PHP use case.
index.php: Conceptual example
<?php
namespace RefactoringGuru\Observer\Conceptual;
/**
* PHP has a couple of built-in interfaces related to the Observer pattern.
*
* Here's what the Subject interface looks like:
*
* @link http://php.net/manual/en/class.splsubject.php
*
* interface SplSubject
* {
* // Attach an observer to the subject.
* public function attach(SplObserver $observer);
*
* // Detach an observer from the subject.
* public function detach(SplObserver $observer);
*
* // Notify all observers about an event.
* public function notify();
* }
*
* There's also a built-in interface for Observers:
*
* @link http://php.net/manual/en/class.splobserver.php
*
* interface SplObserver
* {
* public function update(SplSubject $subject);
* }
*/
/**
* The Subject owns some important state and notifies observers when the state
* changes.
*/
class Subject implements \SplSubject
{
/**
* @var int For the sake of simplicity, the Subject's state, essential to
* all subscribers, is stored in this variable.
*/
public $state;
/**
* @var \SplObjectStorage List of subscribers. In real life, the list of
* subscribers can be stored more comprehensively (categorized by event
* type, etc.).
*/
private $observers;
public function __construct()
{
$this->observers = new \SplObjectStorage();
}
/**
* The subscription management methods.
*/
public function attach(\SplObserver $observer): void
{
echo "Subject: Attached an observer.\n";
$this->observers->attach($observer);
}
public function detach(\SplObserver $observer): void
{
$this->observers->detach($observer);
echo "Subject: Detached an observer.\n";
}
/**
* Trigger an update in each subscriber.
*/
public function notify(): void
{
echo "Subject: Notifying observers...\n";
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
/**
* Usually, the subscription logic is only a fraction of what a Subject can
* really do. Subjects commonly hold some important business logic, that
* triggers a notification method whenever something important is about to
* happen (or after it).
*/
public function someBusinessLogic(): void
{
echo "\nSubject: I'm doing something important.\n";
$this->state = rand(0, 10);
echo "Subject: My state has just changed to: {$this->state}\n";
$this->notify();
}
}
/**
* Concrete Observers react to the updates issued by the Subject they had been
* attached to.
*/
class ConcreteObserverA implements \SplObserver
{
public function update(\SplSubject $subject): void
{
if ($subject->state < 3) {
echo "ConcreteObserverA: Reacted to the event.\n";
}
}
}
class ConcreteObserverB implements \SplObserver
{
public function update(\SplSubject $subject): void
{
if ($subject->state == 0 || $subject->state >= 2) {
echo "ConcreteObserverB: Reacted to the event.\n";
}
}
}
/**
* The client code.
*/
$subject = new Subject();
$o1 = new ConcreteObserverA();
$subject->attach($o1);
$o2 = new ConcreteObserverB();
$subject->attach($o2);
$subject->someBusinessLogic();
$subject->someBusinessLogic();
$subject->detach($o2);
$subject->someBusinessLogic();
Output.txt: Execution result
Subject: Attached an observer.
Subject: Attached an observer.
Subject: I'm doing something important.
Subject: My state has just changed to: 2
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
ConcreteObserverB: Reacted to the event.
Subject: I'm doing something important.
Subject: My state has just changed to: 4
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event.
Subject: Detached an observer.
Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
Real World Example
In this example the Observer pattern allows various objects to observe events that are happening inside a user repository of an app.
The repository emits various types of events and allows observers to listen to all of them, as well as only individual ones.
index.php: Real world example
<?php
namespace RefactoringGuru\Observer\RealWorld;
/**
* The UserRepository represents a Subject. Various objects are interested in
* tracking its internal state, whether it's adding a new user or removing one.
*/
class UserRepository implements \SplSubject
{
/**
* @var array The list of users.
*/
private $users = [];
// Here goes the actual Observer management infrastructure. Note that it's
// not everything that our class is responsible for. Its primary business
// logic is listed below these methods.
/**
* @var array
*/
private $observers = [];
public function __construct()
{
// A special event group for observers that want to listen to all
// events.
$this->observers["*"] = [];
}
private function initEventGroup(string $event = "*"): void
{
if (!isset($this->observers[$event])) {
$this->observers[$event] = [];
}
}
private function getEventObservers(string $event = "*"): array
{
$this->initEventGroup($event);
$group = $this->observers[$event];
$all = $this->observers["*"];
return array_merge($group, $all);
}
public function attach(\SplObserver $observer, string $event = "*"): void
{
$this->initEventGroup($event);
$this->observers[$event][] = $observer;
}
public function detach(\SplObserver $observer, string $event = "*"): void
{
foreach ($this->getEventObservers($event) as $key => $s) {
if ($s === $observer) {
unset($this->observers[$event][$key]);
}
}
}
public function notify(string $event = "*", $data = null): void
{
echo "UserRepository: Broadcasting the '$event' event.\n";
foreach ($this->getEventObservers($event) as $observer) {
$observer->update($this, $event, $data);
}
}
// Here are the methods representing the business logic of the class.
public function initialize($filename): void
{
echo "UserRepository: Loading user records from a file.\n";
// ...
$this->notify("users:init", $filename);
}
public function createUser(array $data): User
{
echo "UserRepository: Creating a user.\n";
$user = new User();
$user->update($data);
$id = bin2hex(openssl_random_pseudo_bytes(16));
$user->update(["id" => $id]);
$this->users[$id] = $user;
$this->notify("users:created", $user);
return $user;
}
public function updateUser(User $user, array $data): User
{
echo "UserRepository: Updating a user.\n";
$id = $user->attributes["id"];
if (!isset($this->users[$id])) {
return null;
}
$user = $this->users[$id];
$user->update($data);
$this->notify("users:updated", $user);
return $user;
}
public function deleteUser(User $user): void
{
echo "UserRepository: Deleting a user.\n";
$id = $user->attributes["id"];
if (!isset($this->users[$id])) {
return;
}
unset($this->users[$id]);
$this->notify("users:deleted", $user);
}
}
/**
* Let's keep the User class trivial since it's not the focus of our example.
*/
class User
{
public $attributes = [];
public function update($data): void
{
$this->attributes = array_merge($this->attributes, $data);
}
}
/**
* This Concrete Component logs any events it's subscribed to.
*/
class Logger implements \SplObserver
{
private $filename;
public function __construct($filename)
{
$this->filename = $filename;
if (file_exists($this->filename)) {
unlink($this->filename);
}
}
public function update(\SplSubject $repository, string $event = null, $data = null): void
{
$entry = date("Y-m-d H:i:s") . ": '$event' with data '" . json_encode($data) . "'\n";
file_put_contents($this->filename, $entry, FILE_APPEND);
echo "Logger: I've written '$event' entry to the log.\n";
}
}
/**
* This Concrete Component sends initial instructions to new users. The client
* is responsible for attaching this component to a proper user creation event.
*/
class OnboardingNotification implements \SplObserver
{
private $adminEmail;
public function __construct($adminEmail)
{
$this->adminEmail = $adminEmail;
}
public function update(\SplSubject $repository, string $event = null, $data = null): void
{
// mail($this->adminEmail,
// "Onboarding required",
// "We have a new user. Here's his info: " .json_encode($data));
echo "OnboardingNotification: The notification has been emailed!\n";
}
}
/**
* The client code.
*/
$repository = new UserRepository();
$repository->attach(new Logger(__DIR__ . "/log.txt"), "*");
$repository->attach(new OnboardingNotification("1@example.com"), "users:created");
$repository->initialize(__DIR__ . "/users.csv");
// ...
$user = $repository->createUser([
"name" => "John Smith",
"email" => "john99@example.com",
]);
// ...
$repository->deleteUser($user);
Output.txt: Execution result
UserRepository: Loading user records from a file.
UserRepository: Broadcasting the 'users:init' event.
Logger: I've written 'users:init' entry to the log.
UserRepository: Creating a user.
UserRepository: Broadcasting the 'users:created' event.
OnboardingNotification: The notification has been emailed!
Logger: I've written 'users:created' entry to the log.
UserRepository: Deleting a user.
UserRepository: Broadcasting the 'users:deleted' event.
Logger: I've written 'users:deleted' entry to the log.