Паттерн Наблюдатель (Observer) в PHP
Короткий скринкаст: паттерн «Наблюдатель» (Observer) — издатель уведомляет подписчиков об изменениях, не зная их конкретных классов. Реализуем рассылку новостей по email и SMS.
Описание
Наблюдатель задаёт зависимость «один ко многим»: при изменении состояния издателя все подписчики получают уведомление. Интерфейс Observer описывает метод update(), который должен уметь каждый подписчик. Базовый класс Subject хранит список наблюдателей: attach() добавляет подписчика, detach() убирает, а notify() проходит по списку и вызывает у каждого update(). Конкретный издатель NewsAgency наследует Subject и через publish() запускает рассылку. Конкретные подписчики EmailSubscriber и SmsSubscriber по-своему реагируют на новость, но издатель не знает ни их типов, ни того, что именно они делают.
UML-диаграмма
Код
Observer.php
interface Observer
{
public function update(string $news): void;
}
Subject.php
abstract class Subject
{
/** @var Observer[] */
private array $observers = [];
public function attach(Observer $observer): void
{
$this->observers[] = $observer;
}
public function detach(Observer $observer): void
{
$this->observers = array_filter(
$this->observers,
fn (Observer $item): bool => $item !== $observer
);
}
protected function notify(string $news): void
{
foreach ($this->observers as $observer) {
$observer->update($news);
}
}
}
EmailSubscriber.php
class EmailSubscriber implements Observer
{
public function __construct(private readonly string $email) {}
public function update(string $news): void
{
echo "Email to {$this->email}: {$news}".PHP_EOL;
}
}
SmsSubscriber.php
class SmsSubscriber implements Observer
{
public function __construct(private readonly string $phone) {}
public function update(string $news): void
{
echo "SMS to {$this->phone}: {$news}".PHP_EOL;
}
}
NewsAgency.php
class NewsAgency extends Subject
{
public function publish(string $news): void
{
$this->notify($news);
}
}
demo.php
require_once 'Observer.php';
require_once 'Subject.php';
require_once 'EmailSubscriber.php';
require_once 'SmsSubscriber.php';
require_once 'NewsAgency.php';
$agency = new NewsAgency;
$email = new EmailSubscriber('user@example.com');
$sms = new SmsSubscriber('+1 555 0100');
$agency->attach($email);
$agency->attach($sms);
$agency->publish('New PHP release!');
$agency->detach($sms);
$agency->publish('Second news');
Пример результата
После php demo.php первую новость получают оба подписчика, а после detach — только email:
Email to user@example.com: New PHP release!
SMS to +1 555 0100: New PHP release!
Email to user@example.com: Second news
Преимущества
- Издатель и подписчики слабо связаны: издатель не знает конкретных классов наблюдателей.
- Новых подписчиков можно добавлять, не меняя код издателя.
- Подписку можно включать и выключать во время выполнения.
- Работает принцип открытости/закрытости: расширение без правки существующих классов.
Когда использовать
- Когда при изменении одного объекта должны обновляться другие, и их число заранее неизвестно.
- Когда объекты должны уведомлять других, не зная, кто они.
- Когда часть объектов должна следить за состоянием другого объекта.
- Когда нужна событийная модель с подпиской и отпиской.
Использование в Laravel
В Laravel Наблюдатель используется повсюду. События и слушатели: Event::dispatch() рассылает событие, а слушатели в app/Listeners реагируют на него через метод handle(). Eloquent-наблюдатели app/Observers подписываются на события модели (creating, saved, deleted) и срабатывают автоматически. Механизм Illuminate\Support\Facades\Notification рассылает уведомления по нескольким каналам (mail, SMS, database) — канал выбирается уже при отправке, а не в коде отправителя.