跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

PHP中如何实现解耦的日志记录系统?应用观察者模式监听系统异常事件

直接在业务代码中调用 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免费学习笔记(深入) ”; PHP 8.5.5 PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。 下载 检查
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()
;否则异常来临时观察者列表为空,日志就丢了。

相关文章