php观察者模式需定义含事件类型、变更快照和上下文的auditobserver接口;subject应通过可注入的auditable trait解耦;审计日志须异步写入并带重试与本地落盘;laravel中优先使用模型事件+serviceprovider注册。

Observer接口定义必须包含事件类型和变更快照
PHP里观察者模式不是靠继承强制约束,而是靠接口约定行为。如果只定义一个 update() 方法,后续扩展日志字段(比如操作人、IP、事务ID)时会不断改接口,破坏现有实现。必须在接口中明确要求传入事件类型和数据快照:
interface AuditObserver
{
public function onAudit(string $event, array $snapshot, array $context = []);
}
其中 $event 是字符串标识(如 "user_updated" 或 "order_paid"),$snapshot 是变更前后的结构化数据(不是原始对象,而是 ['before' => [...], 'after' => [...]]),$context 用于透传请求级上下文(如 ['user_id' => 123, 'ip' => '192.168.1.5'])。漏掉 $context 会导致日志无法关联真实操作者。
Subject基类要避免直接持有Observer实例数组
常见错误是把 $observers = [] 写死在业务模型里(比如 User 类),导致测试难、耦合高、无法统一开关审计。正确做法是抽离为可注入的 AuditSubject 装饰器或 trait:
trait Auditable
{
private ?AuditDispatcher $dispatcher = null;
public function setAuditDispatcher(AuditDispatcher $dispatcher): void
{
$this->dispatcher = $dispatcher;
}
protected function notifyAudit(string $event, array $snapshot): void
{
if ($this->dispatcher) {
$this->dispatcher->notify($event, $snapshot);
}
}
}
这样业务类只需 use Auditable,并在创建时注入 AuditDispatcher 实例。不注入就完全不触发日志,不影响主流程性能。
审计日志写入必须异步且带失败重试
同步写文件或数据库会拖慢主请求,尤其当审计表加了全文索引或日志服务暂时不可用时。AuditDispatcher 内部不能直接调 file_put_contents() 或 PDO::insert():
- 应通过
pcntl_fork()派生子进程(CLI场景)或投递到消息队列(如 Redis List + worker) - 日志实体必须序列化后持久化,不能依赖闭包或资源句柄
- 重试逻辑要基于时间窗口(如首次失败后 1s、5s、30s 重试三次),而非无限轮询
- 失败日志需落盘到本地临时文件(路径如
/tmp/audit_failed_20240521.log),避免丢失
否则线上高峰期可能因日志写入超时引发雪崩。
Laravel中用模型事件+ServiceProvider注册最稳妥
如果你用 Laravel,别自己手写 Observer 类挂载到每个模型——容易漏、难维护。利用框架原生机制更可靠:
- 在
App\Providers\EventServiceProvider的$listen数组里声明:ModelUpdated::class => [AuditLogger::class] -
AuditLogger实现ShouldQueue接口,自动进队列 - 在
boot()方法里用static::updating()和static::updated()钩子捕获变更前后状态,组装$snapshot - 注意:Eloquent 的
$model->getChanges()只返回「本次修改的字段」,要完整快照得手动 clone 原始属性再 diff
手动 diff 比依赖 getDirty() 多两行代码,但能避免「字段未变但值被设为 null 导致审计缺失」这类静默遗漏。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











