PHP 内置 SplSubject/SplObserver 不推荐使用:接口无参数与返回值约束,易致方法未实现或传参错误;通知同步阻塞、顺序不可控、无事件类型区分;手动实现轻量事件分发器更可控、清晰、易维护。

PHP 中用 SPL 的 SplSubject 和 SplObserver 接口写观察者容易踩什么坑
直接说结论:PHP 内置的 SplSubject / SplObserver 接口不推荐在新项目中使用。它强制要求被观察对象必须实现 attach()、detach()、notify(),但接口本身不定义参数类型和返回值,导致实际调用时极易出错。
常见错误现象:Call to undefined method MySubject::notify()(忘了自己实现),或 ArgumentCountError(传参不一致);更隐蔽的是通知顺序不可控、无法中断传播、不支持异步。
- 接口只规定方法名,不约束参数 —— 你传
$event,别人传$data,协作时靠猜 -
notify()是同步阻塞调用,一个 Observer 报错,整个链路中断 - 没有事件名称/类型区分,所有 Observer 都收到全部变更,耦合反而加重
手动实现轻量级事件分发器比用 SPL 更可控
核心就三件事:注册监听、触发事件、传递上下文。不用框架也能几行写清楚。
使用场景:用户注册后发邮件 + 记日志 + 更新推荐权重,这三件事逻辑无关,但都依赖“用户已创建”这个事实。
立即学习“PHP免费学习笔记(深入)”;
实操建议:
- 用数组存储事件名 → 回调列表映射,键是字符串事件名(如
'user.created'),值是callable数组 - 触发时遍历回调,把事件数据(数组或对象)作为唯一参数传入,避免参数数量/顺序混乱
- 回调里自己决定是否抛异常 —— 不想让日志失败影响发邮件,就 try/catch 包一层
示例:
class EventDispatcher {
private array $listeners = [];
public function on(string $event, callable $callback): void {
$this->listeners[$event][] = $callback;
}
public function dispatch(string $event, array $data = []): void {
foreach ($this->listeners[$event] ?? [] as $callback) {
$callback($data);
}
}
}
调用:$dispatcher->on('user.created', fn($d) => send_welcome_email($d['user']));
Laravel 的 Event 和 Listener 类怎么对应到原生逻辑
不是要你抄 Laravel 源码,而是理解它解决的实际问题:命名事件、自动发现监听器、队列延迟执行、事件广播。
如果你项目用了 Laravel,别自己造轮子;如果没用,就别照搬它的类结构(比如 UserRegistered 事件类 + SendWelcomeEmail 监听器类),那会引入不必要的复杂度。
关键差异点:
- Laravel 的事件是“可序列化对象”,方便推到队列;原生用数组或简单 DTO 就够用
- 它的
dispatch()支持延迟(delay(60))、批量(dispatchSync()),原生需自己封装 - 监听器里用
$event->user而不是$data['user'],只是语法糖,本质还是传参
什么时候该放弃观察者模式,改用更简单的方案
当你的“通知”只有 1–2 个固定下游,且它们和主体生命周期强绑定(比如构造函数里 new 一个 Logger 并存为属性),那就别硬套观察者。
容易被忽略的地方:
- 观察者模式的价值在于「运行时动态增删响应者」,如果监听器列表上线后从不变化,就是过度设计
- 调试困难:事件触发路径是隐式的(谁注册了?谁触发了?在哪断的?),比直接调用函数难追踪
- 性能敏感场景(如高频计数器更新),每次
foreach所有监听器有开销,不如直接写死几个函数调用
简单替代方案:用闭包字段存回调,或者直接在业务方法末尾加几行明确的钩子调用 —— 清晰、易测、无抽象泄漏。










