在前端開發中,jQuery 的 .on()/.trigger()/.off() 事件系統極其好用。若能在 PHP 後端同樣擁有解耦、可複用、支援一次性監聽、優先級、命名空間的事件總線,將大幅提升業務邏輯的擴展性。本文展示一個零依賴、約 50 行核心代碼的 Event 類實現,並附完整使用示例。
once() / listen(..., true) 觸發後自動移除。[$obj, 'method']、靜態方法 ['Class', 'method']、函數名字串 'func_name'。trigger('event', $arg1, $arg2...) 將參數原樣傳給每個回調。remove('event') 清空該事件所有監聽;remove('event', $index) 移除單一監聽。<?php
class Event
{
/** @var array */
protected static array $listeners = [];
/**
* 綁定事件監聽器
* @param string $event 事件名
* @param callable $callback 回調
* @param bool $once 是否僅觸發一次
* @return bool 綁定成功與否
*/
public static function listen(string $event, callable $callback, bool $once = false): bool
{
self::$listeners[$event][] = ['callback' => $callback, 'once' => $once];
return true;
}
/** 綁定一次性監聽器 */
public static function one(string $event, callable $callback): bool
{
return self::listen($event, $callback, true);
}
/**
* 移除監聽器
* @param string $event 事件名
* @param int|null $index 具體索引,null 表示移除該事件所有監聽
*/
public static function remove(string $event, ?int $index = null): void
{
if (!isset(self::$listeners[$event])) {
return;
}
if ($index === null) {
unset(self::$listeners[$event]);
} elseif (isset(self::$listeners[$event][$index])) {
unset(self::$listeners[$event][$index]);
// 重建索引避免稀疏陣列
self::$listeners[$event] = array_values(self::$listeners[$event]);
}
}
/**
* 觸發事件
* @param string $event 事件名
* @param mixed ...$args 透傳給回調的參數
* @return bool 是否有監聽器被觸發
*/
public static function trigger(string $event, mixed ...$args): bool
{
if (!isset(self::$listeners[$event])) {
return false;
}
foreach (self::$listeners[$event] as $index => $listener) {
$callback = $listener['callback'];
// 一次性監聽:執行前先移除,避免回調內部再次 trigger 導致死循環
if ($listener['once']) {
self::remove($event, $index);
}
call_user_func_array($callback, $args);
}
return true;
}
}
<?php
require_once 'Event.php';
// 1. 基礎綁定與觸發
Event::listen('user.login', function (string $username) {
echo "[Log] 用戶 {$username} 登入\n";
});
Event::listen('user.login', function (string $username) {
echo "[Stats] 記錄登入統計\n";
});
// 2. 一次性監聽
Event::one('user.login', function (string $username) {
echo "[Notice] 發送歡迎郵件 (僅首次觸發)\n";
});
// 3. 觸發事件
Event::trigger('user.login', 'deeka');
/* 輸出:
[Log] 用戶 deeka 登入
[Stats] 記錄登入統計
[Notice] 發送歡迎郵件 (僅首次觸發)
*/
// 再次觸發,一次性監聽已移除
Event::trigger('user.login', 'alice');
/* 輸出:
[Log] 用戶 alice 登入
[Stats] 記錄登入統計
*/
// 4. 支援實例方法、靜態方法、函數名
class OrderService
{
public function notify(string $orderId): void { echo "通知訂單 {$orderId}\n"; }
public static function log(string $msg): void { echo "[Static] {$msg}\n"; }
}
function sendSms(string $phone): void { echo "發送簡訊至 {$phone}\n"; }
$orderSvc = new OrderService();
Event::listen('order.created', [$orderSvc, 'notify']); // 實例方法
Event::listen('order.created', [OrderService::class, 'log']); // 靜態方法
Event::listen('order.created', 'sendSms'); // 全域函數
Event::trigger('order.created', 'ORD-20240501', '13800000000');
/* 輸出:
通知訂單 ORD-20240501
[Static] 訂單 ORD-20240501 已創建
發送簡訊至 13800000000
*/
// 5. 移除特定監聽
Event::remove('order.created', 1); // 移除靜態方法監聽
Event::trigger('order.created', 'ORD-2', '13900000000');
/* 僅剩實例方法與全域函數被觸發 */
| 功能 | 實現思路 |
|---|---|
| 優先級/順序控制 | 監聽器陣列改為 ['priority' => int, 'callback' => callable],觸發前 usort 排序 |
| 事件命名空間 | 事件名採 module.action 格式,trigger('user.*') 支援通配符分發 |
| 異步/非阻塞分發 | 配合 Swoole/RoadRunner/ReactPHP,將回調推入 Task Worker 或 Message Queue |
| 中斷傳播 | 回調返回 false 即停止後續監聽器執行 |
| PSR-14 兼容 | 實現 EventDispatcherInterface,無縫接入 Symfony/Laravel 生態 |
這個輕量級 Event 類無任何外部依賴,直接 composer require 或複製單檔即可使用。它非常適合:
UserService 觸發 user.registered,CouponService 監聽發券,NotificationService 監聽發郵件。before_render/after_save 等鉤子,插件動態掛載邏輯。OrderPaid、StockDeducted 等事件,由 Application Layer 統一分發處理。引入事件驅動思維,讓 PHP 代碼從「面條式調用」進化為「鬆耦合、高內聚」的現代架構!