<?php
class Row {
private $observers = array();
function insert($data) {
foreach ($this->observers as $observer) {
call_user_func($observer, $data);
}
}
function addObserver($observer) {
$this->observers[] = $observer;
}
}
class Counter {
private $counter = 0;
function increment($data) {
$this->counter++;
echo "$this->counter\n";
}
}
$counter = new Counter;
$row = new Row;
$row->addObserver(array($counter, 'increment'));
$row->insert(5); // automaticky zavolá $counter->increment(5)
<?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();
/*
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.
*/
<?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";
}
}
// USAGE
/**
* 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);
/*
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.
*/
<?php
use SplSubject;
use SplObjectStorage;
use SplObserver;
/**
* User implements the observed object (called Subject), it maintains a list of observers and sends notifications to
* them in case changes are made on the User object
*/
class User implements SplSubject
{
private string $email;
private SplObjectStorage $observers;
public function __construct()
{
$this->observers = new SplObjectStorage();
}
public function attach(SplObserver $observer)
{
$this->observers->attach($observer);
}
public function detach(SplObserver $observer)
{
$this->observers->detach($observer);
}
public function changeEmail(string $email)
{
$this->email = $email;
$this->notify();
}
public function notify()
{
/** @var SplObserver $observer */
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}
use SplObserver;
use SplSubject;
class UserObserver implements SplObserver
{
/**
* @var SplSubject[]
*/
private array $changedUsers = [];
/**
* It is called by the Subject, usually by SplSubject::notify()
*/
public function update(SplSubject $subject)
{
$this->changedUsers[] = clone $subject;
}
/**
* @return SplSubject[]
*/
public function getChangedUsers(): array
{
return $this->changedUsers;
}
}
// USAGE
$observer = new UserObserver();
$user = new User();
$user->attach($observer);
$user->changeEmail('foo@bar.com');
$this->assertCount(1, $observer->getChangedUsers());
<?php
use SplObjectStorage, SplObserver, SplSubject;
class Communicator implements SplSubject {
private $employees;
public $message;
public function __construct () {
$this->employees = new SplObjectStorage();
}
public function attach (SplObserver $employee): void {
$this->employees->attach($employee);
}
public function detach (SplObserver $employee): void {
$this->employees->detach($employee);
}
public function inform (string $message): void {
$this->message = $message;
$this->notify();
}
public function notify (): void {
foreach ($this->employees as $employee) {
$employee->update($this);
}
}
}
use SplObserver, SplSubject;
class Employee implements SplObserver {
public $name;
private $email;
public function __construct (string $name, string $email) {
$this->name = $name;
$this->email = $email;
}
public function update (SplSubject $communicator): void {
$this->sendEmail($communicator->message);
}
protected function sendEmail (string $message) {
echo "Sending email to: " . $this->email . " - Hello " . $this->name . ", " . $message . PHP_EOL;
}
}
class HumanResources {
private $communicator;
public function __construct (array $employees) {
$this->communicator = new Communicator();
foreach ($employees as $employee) {
$this->communicator->attach($employee);
}
}
public function inform (string $message): void {
$this->communicator->inform($message);
}
public function layOf (Employee $employee) {
$this->communicator->detach($employee);
}
public function employ (Employee $employee) {
$this->communicator->attach($employee);
}
}
// USAGE
$testEmployees = [
new Employee("Jonny", 'CEO'),
new Employee("Donny", "software"),
new Employee("Monny", 'hardware')
];
$hr = new HumanResources($testEmployees);
$hr->inform("Important news everyone");
$software = new Employee("Ben", "software");
$hr->employ($software);
$hr->inform("New employee: " . $software->name);
$hr->layOf($software);
$hr->inform("New employee laid off: " . $software->name);