異常信息的捕獲、記錄與通知是系統穩定性的關鍵。利用 觀察者模式 將「異常拋出」(主題)與「具體處理邏輯」(觀察者:郵件、日誌、钉钉、Slack 等)解耦,實現單一職責、開閉原則。本文通過完整代碼示例演示如何在 PHP 中落地該模式。
Exception_Observer<?php
interface Exception_Observer {
/** 當主題通知時調用
* @param Observer_Exception $e 異常對象本身
*/
public function update(Observer_Exception $e);
}
Observer_Exception(繼承自 Exception)<?php
class Observer_Exception extends Exception {
/** 靜態容器:存放所有掛載的觀察者實例 */
private static $_observers = [];
/** 掛載觀察者 */
public static function attach(Exception_Observer $observer) {
self::$_observers[] = $observer;
}
public function __construct($message = null, $code = 0) {
parent::__construct($message, $code);
// 構造時自動通知所有觀察者
$this->notify();
}
/** 遍歷通知 */
private function notify() {
foreach (self::$_observers as $observer) {
// 關鍵點:$this 即當前異常實例,傳遞給觀察者
$observer->update($this);
}
}
}
為什麼用靜態 $_observers?
異常拋出時創建的是新實例(new MyException()),若非靜態,每個實例擁有獨立的觀察者列表,導致在 try-catch 外部掛載的觀察者無法被內部新實例感知。靜態變量實現「全局共享觀察者列表」,甚至子類繼承也共享。
<?php
class Emailing_Exception_Observer implements Exception_Observer {
private $_email = 'admin@example.com';
public function __construct($email = null) {
if ($email !== null && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->_email = $email;
}
}
public function update(Observer_Exception $e) {
$msg = "時間: " . date('Y-m-d H:i:s') . PHP_EOL;
$msg .= "信息: " . $e->getMessage() . PHP_EOL;
$msg .= "追蹤: " . $e->getTraceAsString() . PHP_EOL;
$msg .= "文件: " . $e->getFile() . PHP_EOL;
$msg .= "行號: " . $e->getLine() . PHP_EOL;
// error_log 第 2 參數 1 表示發送郵件(需 php.ini 配置 SMTP)
error_log($msg, 1, $this->_email);
}
}
<?php
class Logging_Exception_Observer implements Exception_Observer {
private $_filename = '/var/log/app_exceptions.log';
public function __construct($filename = null) {
if ($filename !== null && is_string($filename)) {
$this->_filename = $filename;
}
}
public function update(Observer_Exception $e) {
$msg = "時間: " . date('Y-m-d H:i:s') . PHP_EOL;
$msg .= "信息: " . $e->getMessage() . PHP_EOL;
$msg .= "追蹤: " . $e->getTraceAsString() . PHP_EOL;
$msg .= "文件: " . $e->getFile() . PHP_EOL;
$msg .= "行號: " . $e->getLine() . PHP_EOL;
// error_log 第 2 參數 3 表示寫入文件
error_log($msg, 3, $this->_filename);
}
}
<?php
require 'Exception_Observer.php';
require 'Observer_Exception.php';
require 'Logging_Exception_Observer.php';
require 'Emailing_Exception_Observer.php';
// 1. 啟動階段:掛載觀察者(通常在 bootstrap / 入口文件)
Observer_Exception::attach(new Logging_Exception_Observer());
Observer_Exception::attach(new Emailing_Exception_Observer('devops@company.com'));
// 2. 定義業務異常(可擴展自定義方法)
class MyException extends Observer_Exception {
public function test() { echo 'this is a test'; }
public function handleMe() { echo "自定義異常處理邏輯"; }
}
// 3. 業務邏輯中拋出異常
try {
// 模擬業務錯誤
throw new MyException("訂單支付回調簽名驗證失敗", 4001);
} catch (MyException $e) {
// 此處僅做頁面友好提示,實際記錄/通知已在構造函數自動完成
echo $e->getMessage();
echo "<hr>";
}
| 傳統 try-catch 硬編碼 | 觀察者模式 |
|---|---|
每個 catch 塊寫死 error_log、mail() | 異常拋出自動觸發所有掛載的處理器 |
| 新增通知渠道需修改所有 catch | 新增觀察者類,一行 attach() 接入,零侵入業務代碼 |
| 單元測試難以驗證副作用 | 可 Mock 觀察者斷言 update() 被調用 |
| 異常處理邏輯分散、不一致 | 統一入口,格式標準化,便於接入 ELK、Sentry 等平台 |
$e->getCode() 或自定義屬性決定是否發送郵件(如僅 5xx 級別發郵件)。update() 內將任務推入消息隊列,避免阻塞主流程。觀察者模式讓「發生什麼」與「怎麼處理」徹底分離。在異常處理場景,它是通向可維護、可觀測系統的基石。