觀察者模式定義物件間一對多依賴,主題狀態變更時自動通知所有觀察者。本文提供標準介面實作、PHP SPL 內建介面應用、完整用戶註冊通知案例與最佳實踐指南。
| 角色 | 介面/類 | 職責 |
|---|---|---|
| 抽象主題 | Subject / SplSubject | 管理觀察者清單、attach/detach/notify |
| 具體主題 | ConcreteSubject / User | 持有狀態、觸發通知 |
| 抽象觀察者 | Observer / SplObserver | 定義 update() 介面 |
| 具體觀察者 | ConcreteObserver / SendMail, SendSms, LogActivity | 實作具體通知邏輯 |
<?php
// === 抽象主題介面 ===
interface Subject {
public function attach(Observer $observer);
public function detach(Observer $observer);
public function notifyObservers();
}
// === 具體主題 ===
class ConcreteSubject implements Subject {
private $_observers = [];
public function attach(Observer $observer) {
return array_push($this->_observers, $observer);
}
public function detach(Observer $observer) {
$index = array_search($observer, $this->_observers, true);
if ($index === false) return false;
unset($this->_observers[$index]);
return true;
}
public function notifyObservers() {
foreach ($this->_observers as $observer) {
$observer->update($this); // 傳遞主題自身
}
}
// 業務方法範例
public function doSomething() {
echo "主題執行業務邏輯...\n";
$this->notifyObservers();
}
}
// === 抽象觀察者介面 ===
interface Observer {
public function update(Subject $subject);
}
// === 具體觀察者 ===
class ConcreteObserver implements Observer {
private $_name;
public function __construct($name) {
$this->_name = $name;
}
public function update(Subject $subject) {
echo "觀察者 [{$this->_name}] 收到通知,主題類別: " . get_class($subject) . "\n";
}
}
// === 使用範例 ===
$subject = new ConcreteSubject();
$observer1 = new ConcreteObserver('Martin');
$subject->attach($observer1);
echo "--- 第一次通知 ---\n";
$subject->notifyObservers();
$observer2 = new ConcreteObserver('phppan');
$subject->attach($observer2);
echo "--- 第二次通知 ---\n";
$subject->notifyObservers();
$subject->detach($observer1);
echo "--- 第三次通知 (移除 Martin) ---\n";
$subject->notifyObservers();
<?php
/**
* 主題:用戶模型
* 實作 SplSubject,支援多種事件類型
*/
class User implements SplSubject {
private $observers = [];
public $email;
public $name;
// 事件類型常數
const EVENT_REGISTER = 'register';
const EVENT_LOGIN = 'login';
const EVENT_ORDER = 'order';
const EVENT_PROFILE_UPDATE = 'profile_update';
/**
* 註冊觀察者
* @param SplObserver $observer
* @param string|array $events 事件類型,支援多事件
*/
public function attach(SplObserver $observer, $events = []) {
$events = (array)$events;
foreach ($events as $event) {
$this->observers[$event][] = $observer;
}
}
public function detach(SplObserver $observer, $events = []) {
$events = (array)$events;
foreach ($events as $event) {
if (isset($this->observers[$event])) {
$key = array_search($observer, $this->observers[$event], true);
if ($key !== false) unset($this->observers[$event][$key]);
}
}
}
/**
* 觸發通知
*/
public function notify($event) {
if (empty($this->observers[$event])) return;
foreach ($this->observers[$event] as $observer) {
$observer->update($this, $event); // 傳遞事件類型
}
}
// === 業務方法 ===
public function register($name, $email, $password) {
$this->name = $name;
$this->email = $email;
// 執行資料庫插入...
$success = true; // 模擬成功
if ($success) {
$this->notify(self::EVENT_REGISTER);
}
return $success;
}
public function placeOrder($orderId, $amount) {
// 建立訂單...
$success = true;
if ($success) {
$this->notify(self::EVENT_ORDER);
}
return $success;
}
}
/**
* 觀察者:發送歡迎郵件
*/
class SendWelcomeMail implements SplObserver {
public function update(SplSubject $subject, $event = null) {
if ($event !== User::EVENT_REGISTER) return;
$user = $subject;
$this->sendMail($user->email, '歡迎註冊', "親愛的 {$user->name},歡迎加入!");
}
private function sendMail($to, $subject, $body) {
// 實際對接郵件服務 (PHPMailer, SwiftMailer, API...)
mail($to, $subject, $body);
error_log("郵件已發送至: $to");
}
}
/**
* 觀察者:發送簡訊通知
*/
class SendSmsNotification implements SplObserver {
public function update(SplSubject $subject, $event = null) {
if ($event !== User::EVENT_ORDER) return;
$user = $subject;
$this->sendSms($user->phone, "您的訂單已確認,金額: $amount");
}
private function sendSms($phone, $msg) {
// 對接簡訊 API
error_log("簡訊發送至 $phone: $msg");
}
}
/**
* 觀察者:記錄活動日誌
*/
class LogActivity implements SplObserver {
public function update(SplSubject $subject, $event = null) {
$user = $subject;
$log = "[".date('Y-m-d H:i:s')."] 用戶 {$user->name} 觸發事件: $event\n";
file_put_contents('activity.log', $log, FILE_APPEND);
}
}
// === 使用範例 ===
$user = new User();
// 註冊觀察者 (依事件類型綁定)
$user->attach(new SendWelcomeMail(), User::EVENT_REGISTER);
$user->attach(new LogActivity(), [User::EVENT_REGISTER, User::EVENT_ORDER, User::EVENT_LOGIN]);
$user->attach(new SendSmsNotification(), User::EVENT_ORDER);
// 模擬用戶註冊
$user->register('John Doe', 'john@example.com', 'secret123');
// 輸出: 郵件發送 + 日誌記錄
// 模擬下單
$user->phone = '+886912345678';
$user->placeOrder('ORD-20240115001', 1299);
// 輸出: 簡訊發送 + 日誌記錄
<?php
// 訂單主題
class Order implements SplSubject {
private $observers = [];
public $id;
public $user_id;
public $status; // pending, paid, shipped, delivered, cancelled
public $total;
const STATUS_CHANGED = 'status_changed';
public function attach(SplObserver $observer) {
$this->observers[] = $observer;
}
public function detach(SplObserver $observer) {
$key = array_search($observer, $this->observers, true);
if ($key !== false) unset($this->observers[$key]);
}
public function notify() {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setStatus($newStatus) {
$oldStatus = $this->status;
$this->status = $newStatus;
if ($oldStatus !== $newStatus) {
$this->notify(); // 狀態變更才通知
}
}
}
// 觀察者:庫存釋放 (訂單取消時)
class InventoryRelease implements SplObserver {
public function update(SplSubject $subject) {
$order = $subject;
if ($order->status === 'cancelled') {
// 釋放庫存邏輯
error_log("釋放訂單 {$order->id} 庫存");
}
}
}
// 觀察者:發票開立 (訂單付款時)
class InvoiceGenerator implements SplObserver {
public function update(SplSubject $subject) {
$order = $subject;
if ($order->status === 'paid') {
// 開立發票邏輯
error_log("為訂單 {$order->id} 開立發票,金額: {$order->total}");
}
}
}
// 觀察者:物流通知 (訂單出貨時)
class ShippingNotifier implements SplObserver {
public function update(SplSubject $subject) {
$order = $subject;
if ($order->status === 'shipped') {
// 通知物流、發送追蹤碼
error_log("訂單 {$order->id} 已出貨,通知用戶追蹤資訊");
}
}
}
// 使用
$order = new Order();
$order->id = 'ORD-001';
$order->total = 2999;
$order->attach(new InventoryRelease());
$order->attach(new InvoiceGenerator());
$order->attach(new ShippingNotifier());
$order->setStatus('paid'); // 觸發發票開立
$order->setStatus('shipped'); // 觸發物流通知
$order->setStatus('cancelled'); // 觸發庫存釋放
| 特性 | 觀察者模式 | 事件分發器 | 消息佇列 |
|---|---|---|---|
| 耦合度 | 低 (介面綁定) | 極低 (字串事件名) | 極低 (佇列中介) |
| 同步/非同步 | 同步 | 同步/非同步可選 | 非同步 |
| 跨進程 | 否 | 否 | 是 |
| 持久化/重試 | 無 | 需自行實作 | 內建 |
| 適用場景 | 同一請求內多邏輯聯動 | 框架級事件系統 | 分散式系統、耗時任務 |
try-catch,避免單一失敗阻斷其他detach() 不再需要的觀察者PHP SPL 內建
SplSubject/SplObserver介面是標準化契約,建議優先使用。框架 (Laravel Events, Symfony EventDispatcher) 提供更強大的事件系統,大型專案建議直接採用框架機制。