觀察者模式讓物件間保持鬆耦合:主題不需知道觀察者細節,觀察者自行決定如何回應。本文以「錯誤處理系統」為例,實作多通道日誌記錄 (檔案、Email、Syslog、資料庫)。
<?php
// 1. 主題介面 (可被觀察者)
interface SplSubject {
public function attach(SplObserver $observer): void;
public function detach(SplObserver $observer): void;
public function notify(): void;
}
// 2. 觀察者介面
interface SplObserver {
public function update(SplSubject $subject): void;
}
// 3. 具體主題:錯誤處理器 (單例模式)
class ErrorHandler implements SplSubject {
private array $observers = [];
private string $lastError = '';
private static ?self $instance = null;
private function __construct() {}
public static function getInstance(): self {
return self::$instance ??= new self();
}
public function attach(SplObserver $observer): void {
$this->observers[] = $observer;
}
public function detach(SplObserver $observer): void {
$key = array_search($observer, $this->observers, true);
if ($key !== false) unset($this->observers[$key]);
}
public function notify(): void {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function triggerError(string $message, int $level = E_USER_ERROR): void {
$this->lastError = $message;
$this->notify();
// 可選:觸發 PHP 原生錯誤
trigger_error($message, $level);
}
public function getLastError(): string {
return $this->lastError;
}
}
?>
<?php
// 1. 檔案日誌觀察者
class FileErrorLogger implements SplObserver {
private $fileHandle;
public function __construct($fileHandle) {
$this->fileHandle = $fileHandle;
}
public function update(SplSubject $subject): void {
if ($subject instanceof ErrorHandler) {
$msg = "[" . date('Y-m-d H:i:s') . "] {$subject->getLastError()}\n";
fwrite($this->fileHandle, $msg);
}
}
}
// 2. Email 通知觀察者
class EmailErrorLogger implements SplObserver {
private string $email;
public function __construct(string $email) {
$this->email = $email;
}
public function update(SplSubject $subject): void {
if ($subject instanceof ErrorHandler) {
$subject = "系統錯誤通知";
$body = "時間: " . date('Y-m-d H:i:s') . "\n錯誤: {$subject->getLastError()}";
mail($this->email, $subject, $body);
}
}
}
// 3. Syslog 觀察者
class SyslogErrorLogger implements SplObserver {
public function update(SplSubject $subject): void {
if ($subject instanceof ErrorHandler) {
syslog(LOG_ERR, $subject->getLastError());
}
}
}
// 4. 資料庫觀察者 (PDO)
class DatabaseErrorLogger implements SplObserver {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function update(SplSubject $subject): void {
if ($subject instanceof ErrorHandler) {
$stmt = $this->pdo->prepare(
"INSERT INTO error_log (message, created_at) VALUES (?, NOW())"
);
$stmt->execute([$subject->getLastError()]);
}
}
}
?>
<?php
// bootstrap.php 或初始化檔案
require_once 'ErrorHandler.php';
require_once 'FileErrorLogger.php';
require_once 'EmailErrorLogger.php';
// require_once 'DatabaseErrorLogger.php';
$handler = ErrorHandler::getInstance();
// 註冊觀察者
$logFile = fopen(__DIR__ . '/logs/error.log', 'a');
$handler->attach(new FileErrorLogger($logFile));
$handler->attach(new EmailErrorLogger('admin@example.com'));
// $handler->attach(new DatabaseErrorLogger($pdo));
// 設定為 PHP 預設錯誤處理器
set_error_handler(function($severity, $message, $file, $line) use ($handler) {
$handler->triggerError("[$severity] $message in $file:$line");
return true; // 防止 PHP 內建處理
});
// 設定例外處理器
set_exception_handler(function(Throwable $e) use ($handler) {
$handler->triggerError("Uncaught Exception: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}");
});
// 設定致命錯誤捕獲 (PHP 7+)
register_shutdown_function(function() use ($handler) {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$handler->triggerError("Fatal Error: {$error['message']} in {$error['file']}:{$error['line']}");
}
});
// 測試
trigger_error("這是一個測試警告", E_USER_WARNING);
throw new RuntimeException("測試例外");
?>
<?php
use PHPUnit\Framework\TestCase;
class ErrorHandlerTest extends TestCase {
public function testObserversNotified() {
$handler = ErrorHandler::getInstance();
// 清理之前的觀察者 (測試隔離)
$reflection = new ReflectionClass($handler);
$prop = $reflection->getProperty('observers');
$prop->setAccessible(true);
$prop->setValue($handler, []);
$mockLogger = $this->createMock(SplObserver::class);
$mockLogger->expects($this->once())
->method('update')
->with($this->identicalTo($handler));
$handler->attach($mockLogger);
$handler->triggerError('Test error');
}
public function testFileLoggerWritesCorrectFormat() {
$tempFile = sys_get_temp_dir() . '/test_error_' . uniqid() . '.log';
$fh = fopen($tempFile, 'w');
$logger = new FileErrorLogger($fh);
$handler = ErrorHandler::getInstance();
// ... 同理測試檔案寫入格式
fclose($fh);
unlink($tempFile);
}
}
?>
<?php
class LeveledErrorHandler extends ErrorHandler {
private array $levelObservers = []; // level => observers[]
public function attach(SplObserver $observer, int $level = E_ALL): void {
$this->levelObservers[$level][] = $observer;
}
public function notify(int $level = E_ALL): void {
// 通知完全匹配等級 + E_ALL 觀察者
foreach ([$level, E_ALL] as $lvl) {
foreach ($this->levelObservers[$lvl] ?? [] as $observer) {
$observer->update($this);
}
}
}
public function triggerError(string $message, int $level = E_USER_ERROR): void {
$this->lastError = $message;
$this->notify($level);
trigger_error($message, $level);
}
}
// 使用:只有嚴重錯誤發 Email
$handler = LeveledErrorHandler::getInstance();
$handler->attach(new FileErrorLogger($fh), E_ALL); // 所有錯誤寫檔
$handler->attach(new EmailErrorLogger('admin@site.com'), E_USER_ERROR | E_ERROR); // 只發嚴重
?>
| 特性 | 觀察者模式 | 發布/訂閱 |
|---|---|---|
| 耦合度 | 主題直接持有觀察者引用 | 透過事件總線/中介者解耦 |
| 通訊方式 | 同步、直接呼叫 | 通常非同步、佇列 |
| 適用範圍 | 單一應用程式內 | 跨服務、微服務架構 |
| PHP 實作 | SplSubject/SplObserver | Symfony EventDispatcher、Laravel Events |