laravel脏检测靠快照比对而非实时监听;isdirty()返回false仅表示当前值与$original一致,未必未修改。根本原因是$original未正确建立或被覆盖,常见于new模型、手动赋值、syncoriginal()调用、类型转换或访问器干扰。

直接说结论:Laravel 的脏数据检测不是“实时监听”,而是靠快照比对;isDirty() 返回 false 不代表没改,只代表当前值和 $original 里存的原始值一致。
isDirty() 为什么有时返回 false 却实际改了值
根本原因在于 $original 快照没被正确建立或已被覆盖。常见触发点:
- 模型是 new 出来的(比如
new User),还没从数据库加载过,$original为空数组,isDirty()永远返回 false - 手动赋值
$user->attributes['name'] = 'xxx',绕过了 Eloquent 的 setter,快照不更新 - 调用了
$user->syncOriginal(),相当于主动把当前值设为“原始值”,后续所有修改都变“不脏” - 字段有
$casts = ['meta' => 'array']或自定义访问器,getOriginal('meta')返回的是数据库原始字符串,而$user->meta是 PHP 数组,用==比对会误判
getDirty() 返回空数组但字段确实变了怎么办
先确认你比对的是“语义变更”,而不是“内存值差异”。尤其注意:
- 用
!==而非==比较,避免'0'和0、null和''被当成相等 - JSON 字段必须用
json_encode($a) !== json_encode($b),否则数组键序、null/empty 区分都会出错 - 不要依赖
$user->name做判断——如果定义了getNameAttribute(),它可能做了 trim 或格式化,而getOriginal('name')才是入库原始值 - 测试时用
User::find(1)而非factory()->create()后立刻查,后者没走完整加载流程,$original可能未填充
想跳过未修改字段更新,但又不想丢掉事件和时间戳
直接用 update($dirty) 是最简方案,但它会绕过 saving/updating 事件、不自动更新 updated_at、也不触发观察者。折中做法:
- 手动补上时间戳:
$dirty['updated_at'] = now() - 若需保留事件,改用
saveQuietly()+ 自定义逻辑拦截,或在updating事件里提前拿到getDirty()结果 - 软删除字段
deleted_at不进getDirty(),要单独判断:!$model->wasChanged('deleted_at') && $model->isDirty('deleted_at')不成立,得看$model->getOriginal('deleted_at') !== $model->deleted_at - 关联模型变更不会让主模型变脏,
Post的comments()->attach()不触发Post::isDirty(),得监听中间表或用MorphToMany的事件
真正容易被忽略的,是 $original 这个数组的生命周期——它只在模型从数据库加载、refresh()、replicate() 或显式调用 syncOriginal() 时写入。中间任何一次手动操作、强制类型转换、或访问器副作用,都可能让“脏”和“真改了”脱节。











