最轻量方案是在saved事件中用isDirty()判断后调getChanges()和getOriginal()获取新旧值;updating事件不可用,因其时dirty状态未更新、getChanges()未生成、getOriginal()仍为上次加载值,且无法准确识别本次变更字段。

直接在模型 saved 事件里用 getChanges() 和 getOriginal() 拿新旧值,是最轻量、无额外查询的方案;但必须先判 isDirty(),否则空变更也会写日志。
为什么不能在 updating 事件里记变更
事务还没提交,getOriginal() 可能被中间操作污染;更关键的是,updating 触发时 getChanges() 还没生成,你只能拿到当前属性值,而无法确认哪些字段真被改过。Laravel 的 dirty 机制只在 save() 流程中更新原始快照,updating 阶段它仍是“上一次加载时”的状态,不是本次要存的旧值。
-
saved事件中isDirty()仍为 true,且getOriginal()和getChanges()都可用 -
updating中调getOriginal('email')返回的可能是上一轮 save 后的值,不是本次提交前的值 - 若模型用了批量赋值(
fill())后又手动改某个字段,updating里无法区分该字段是否属于本次变更
getOriginal() 和 getAttribute() 别混用
getAttribute('email') 拿到的是当前值(可能含读取器处理),getOriginal('email') 是数据库原始字符串或数字——比如 status 字段是 tinyint(1),PDO 默认返回 "1",而你表单传的是整数 1,getAttribute() 可能已转成布尔 true,但 getOriginal() 还是 "1"。直接比会恒为 false。
- 比对前统一类型:
(string) $model->getOriginal('status')vs(string) $model->getAttribute('status') - JSON 字段注意:
getOriginal()返回反序列化后的数组,getAttribute()可能是对象或字符串,建议都json_encode(..., JSON_UNESCAPED_UNICODE | JSON_SORT_KEYS)再比 - 时间字段别用
Carbon实例直接比,转成strtotime()或format('Y-m-d H:i:s')统一格式
批量更新(where()->update())不走模型事件
这种写法绕过模型生命周期,saved、updating 全都不触发。如果你业务里有用 User::where('id', $id)->update($data),那审计逻辑就完全失效。
立即学习“PHP免费学习笔记(深入)”;
- 必须改用模型实例方式:
$user = User::find($id); $user->fill($data)->save(); - 若性能敏感、不能查库,就得手动补日志:先
User::where('id', $id)->value('email')拿旧值,再update(),最后写入audit_log - 软删除字段(如
delete_time)也要单独处理:当delete_time从null变成非空,说明执行了delete(),这属于关键状态跃迁,不能漏
敏感字段和系统字段必须过滤
密码、身份证号这类字段,连旧值都不能落库;created_at、updated_at 这类自动填充字段,比对出来永远变,纯属噪音。
- 硬编码白名单比对字段:
['name', 'email', 'status'],别用array_diff_assoc($new, $old)全量扫 - 过滤动作要显式:
unset($diff['password'], $diff['id_card']),别依赖array_diff_key()配置,容易漏 - 日志表的
created_at必须设为数据库默认CURRENT_TIMESTAMP,否则和模型的auto_write_timestamp冲突,分不清是操作时间还是入库时间
真正难的不是取值,而是确定「谁在什么时候改了什么」——isDirty() 只告诉你变了,但不知道是谁触发的;getOriginal() 不带上下文,得自己把操作人 ID、IP、请求路径这些塞进日志。这些信息不在模型里,得从请求生命周期里捞,而且 CLI 或队列里还容易丢。



















