直接在业务代码中调用 error_log() 或 file_put_contents() 会导致日志与业务逻辑紧耦合,难以扩展通知渠道或处理敏感信息,且无法捕获未捕获的致命错误;应通过 SplSubject/SplObserver 模式将异常作为事件广播,由统一的 ExceptionSubject 在 set_exception_handler() 和 register_shutdown_function() 中触发 notify(),各观察者(如 FileLogger、SlackLogger)仅专注自身职责;需注意 shutdown 时须将 error_get_last() 数组包装为 Error 实例,观察者中禁止阻塞、重抛异常或依赖已销毁的超全局变量,并确保 ExceptionSubject 为提前初始化的全局单例。

为什么直接在业务代码里调用 error_log() 或 file_put_contents() 会出问题
因为日志写入逻辑和业务逻辑绑死,一旦要加 Slack 通知、切 Elasticsearch、过滤敏感字段,就得改所有 try/catch 块。更麻烦的是,系统异常(比如未捕获的 Fatal error)根本进不了常规业务流程,set_exception_handler() 和 register_shutdown_function() 是唯一入口,但它们没法直接触发“可插拔”的日志行为。
用 SplObserver + SplSubject 实现异常事件广播
核心不是“记录日志”,而是“把异常当作事件发出去”。PHP 原生的观察者接口足够轻量,且不依赖框架——关键是让异常发生时只通知,不耦合具体处理方式。
-
SplSubject由异常分发器实现,负责维护观察者列表和notify() - 每个日志处理器(如
FileLogger、SlackLogger)实现SplObserver,只关心自己该做什么 - 在
set_exception_handler()和register_shutdown_function()里统一调用$subject->notify($exception)
示例关键片段:
// 异常分发器
class ExceptionSubject implements SplSubject {
private array $observers = [];
public function attach(SplObserver $observer): void { $this->observers[] = $observer; }
public function notify($exception): void {
foreach ($this->observers as $o) $o->update($this, $exception);
}
}
// 文件日志观察者
class FileLogger implements SplObserver {
public function update(SplSubject $subject, $exception): void {
error_log(sprintf('[%s] %s: %s in %s:%d',
date('c'),
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
), 3, '/var/log/app.log');
}
}
如何让 register_shutdown_function() 正确捕获致命错误
它只能拿到 error_get_last(),而这个函数返回的是数组,不是 Throwable。直接传给观察者会类型不匹配,必须手动包装成 Error 实例。
立即学习“PHP免费学习笔记(深入)”;
- 检查
error_get_last()的type是否属于E_ERROR、E_PARSE等致命类型 - 用
new Error($message, $code)构造(PHP 7+),不能用Exception - 注意:某些致命错误(如内存耗尽)可能连
register_shutdown_function()都不执行,这是 PHP 层级限制,无法绕过
典型误用:if (error_get_last()) { $subject->notify(error_get_last()); } —— 错,error_get_last() 返回数组,观察者 expecting Throwable。
日志观察者里不该做哪些事
观察者是响应式组件,不是调度中心。常见反模式包括:
- 在
update()里调用sleep(1)或远程 API 同步阻塞,拖慢整个异常处理链路 - 尝试重新抛出异常(
throw $exception),这会让set_exception_handler()失效或引发二次崩溃 - 依赖
$_SERVER或$_SESSION——register_shutdown_function()执行时这些可能已销毁或不可靠 - 写日志时不做
is_writable()检查,导致静默失败,连错误都记不下来
真正该做的只有三件事:格式化、路由、落盘/转发。其他都该交给外部配置或独立服务。
最易被忽略的一点:SplSubject 实例必须是全局单例,且要在 set_exception_handler() 注册前就完成所有 attach();否则异常来临时观察者列表为空,日志就丢了。



















