首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 觀察者模式完整實作:標準介面、SPL 內建支援與實戰案例 (用戶註冊/訂單通知)

     編輯:PHP 相關知識

觀察者模式定義物件間一對多依賴,主題狀態變更時自動通知所有觀察者。本文提供標準介面實作、PHP SPL 內建介面應用、完整用戶註冊通知案例與最佳實踐指南。

模式結構角色

角色介面/類職責
抽象主題Subject / SplSubject管理觀察者清單、attach/detach/notify
具體主題ConcreteSubject / User持有狀態、觸發通知
抽象觀察者Observer / SplObserver定義 update() 介面
具體觀察者ConcreteObserver / SendMail, SendSms, LogActivity實作具體通知邏輯

實作一:標準介面版 (純 PHP,無框架依賴)

<?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();

實作二:SPL 內建介面版 (PHP 5.1+ 內建,標準化、可互操作) ✅ 推薦

<?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'); // 觸發庫存釋放

觀察者模式 vs 事件分發器 vs 消息佇列

特性觀察者模式事件分發器消息佇列
耦合度低 (介面綁定)極低 (字串事件名)極低 (佇列中介)
同步/非同步同步同步/非同步可選非同步
跨進程否否是
持久化/重試無需自行實作內建
適用場景同一請求內多邏輯聯動框架級事件系統分散式系統、耗時任務

最佳實踐原則

  • 單一職責:每個觀察者只做一件事 (發郵件、發簡訊、記日誌)
  • 異常隔離:觀察者內部 try-catch,避免單一失敗阻斷其他
  • 非同步化:耗時操作 (郵件、API) 推送至佇列 (Redis/RabbitMQ),觀察者只負責入佇列
  • 避免循環依賴:觀察者不應修改主題狀態觸發新通知
  • 記憶體洩漏防護:長生命週期主題需及時 detach() 不再需要的觀察者

PHP SPL 內建 SplSubject/SplObserver 介面是標準化契約,建議優先使用。框架 (Laravel Events, Symfony EventDispatcher) 提供更強大的事件系統,大型專案建議直接採用框架機制。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved