直接在业务代码中调用 error_log() 或 file_put_contents() 会导致日志与业务逻辑紧耦合,难以扩展通知渠道或处理敏感信息,且无法捕获未捕获的致命错误;应通过 SplSubject/SplObserver 模式将异常作为事件广播,由统一的 ExceptionSubject 在 set_exception_handler() 和 register_shutdown_function() 中触发 notify(),各观察者(如 FileLogger、SlackLogger)仅专注自身职责;需注意 shutdown 时须将 error_get_last() 数组包装为 Error 实例,观察者中禁止阻塞、重抛异常或依赖已销毁的超全局变量,并确保 ExceptionSubject 为提前初始化的全局单例。
为什么直接在业务代码里调用
或
会出问题
因为日志写入逻辑和业务逻辑绑死,一旦要加 Slack 通知、切 Elasticsearch、过滤敏感字段,就得改所有
块。更麻烦的是,系统异常(比如未捕获的
)根本进不了常规业务流程,
和
是唯一入口,但它们没法直接触发“可插拔”的日志行为。
用
+
实现异常事件广播
核心不是“记录日志”,而是“把异常当作事件发出去”。PHP 原生的观察者接口足够轻量,且不依赖框架——关键是让异常发生时只通知,不耦合具体处理方式。
由异常分发器实现,负责维护观察者列表和
每个日志处理器(如
、
)实现
,只关心自己该做什么
在
和
里统一调用
示例关键片段:
如何让
正确捕获致命错误
它只能拿到
,而这个函数返回的是数组,不是
。直接传给观察者会类型不匹配,必须手动包装成
实例。
立即学习
“
PHP免费学习笔记(深入)
”;
PHP 8.5.5
PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。
下载
检查
的
是否属于
、
等致命类型
用
构造(PHP 7+),不能用
注意:某些致命错误(如内存耗尽)可能连
都不执行,这是 PHP 层级限制,无法绕过
典型误用:
—— 错,
返回数组,观察者 expecting
。
日志观察者里不该做哪些事
观察者是响应式组件,不是调度中心。常见反模式包括:
在
里调用
或远程 API 同步阻塞,拖慢整个异常处理链路
尝试重新抛出异常(
),这会让
失效或引发二次崩溃
依赖
或
——
执行时这些可能已销毁或不可靠
写日志时不做
检查,导致静默失败,连错误都记不下来
真正该做的只有三件事:格式化、路由、落盘/转发。其他都该交给外部配置或独立服务。
最易被忽略的一点:
实例必须是全局单例,且要在
注册前就完成所有
;否则异常来临时观察者列表为空,日志就丢了。
error_log()file_put_contents()try/catchFatal errorset_exception_handler()register_shutdown_function()SplObserverSplSubjectSplSubjectnotify()FileLoggerSlackLoggerSplObserverset_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()ThrowableErrorerror_get_last()typeE_ERRORE_PARSEnew Error($message, $code)Exceptionregister_shutdown_function()if (error_get_last()) { $subject->notify(error_get_last()); }error_get_last()Throwableupdate()sleep(1)throw $exceptionset_exception_handler()$_SERVER$_SESSIONregister_shutdown_function()is_writable()SplSubjectset_exception_handler()attach()