
Laravel 中调用 notifyNow() 或 notify() 触发模型更新,进而再次触发 updated 事件监听器,形成无限递归通知循环;根本原因在于通知发送后通过 markAsSent() 修改了模型字段,意外重入监听逻辑。
laravel 中调用 `notifynow()` 或 `notify()` 触发模型更新,进而再次触发 `updated` 事件监听器,形成无限递归通知循环;根本原因在于通知发送后通过 `markassent()` 修改了模型字段,意外重入监听逻辑。
该问题并非 Laravel 通知系统本身缺陷,而是事件监听与模型变更耦合不当导致的典型递归陷阱。核心链路如下:
- 模型 updated 事件被触发 → 调用 $model->notifyNow(new LatiniaNotification(...))
- LatiniaNotification::markAsSent() 内部执行 $notifiable->update([...]) → 再次触发 updated 事件
- 循环重复,直至内存耗尽或达到 PHP 递归限制
✅ 正确解决方案:避免在监听器中触发二次模型持久化
方案一:临时禁用事件分发(推荐)
在 markAsSent() 中使用 withoutEvents() 阻止后续事件传播:
public function markAsSent($notifiable)
{
if (!is_null($this->sentField)) {
\Illuminate\Support\Facades\Event::withoutEvents(function () use ($notifiable) {
$notifiable->update([
$this->sentField => true
]);
});
}
}⚠️ 注意:Event::withoutEvents() 仅禁用事件广播,不影响数据库操作本身,安全可靠。
方案二:在事件监听器中添加守卫逻辑
在 updated 事件监听器中判断是否已处于通知发送流程,例如通过静态标记或请求上下文:
// 在监听器中
if (property_exists($model, 'skipNotification') && $model->skipNotification) {
return;
}
// 发送通知前设置标记
$model->skipNotification = true;
$model->notifyNow(new LatiniaNotification($body));
$model->skipNotification = false; // 可选:重置(若需复用)方案三:改用 saving 事件 + 状态比对(更健壮)
避免依赖 updated,改用 saving 事件并精确判断字段变更:
protected static function booted()
{
static::saving(function ($model) {
// 仅当关键业务字段变更且未标记已通知时触发
if (
$model->isDirty('important_field') &&
!$model->isDirty('cc_mail_sent') && // 避免因 markAsSent 引起的脏检查
!$model->wasChanged('cc_mail_sent')
) {
$model->notifyNow(new LatiniaNotification(...));
}
});
}? 补充说明:shouldSend() 为何失效?
shouldSend() 方法仅在通知分发前由 Laravel 通知分发器调用,但你的 notifyNow() 是直接调用通道 send() 方法,绕过了标准分发流程(包括 shouldSend 校验)。因此该方法在此场景下不会被执行——这也是你观察到“被忽略”的原因。
✅ 最佳实践建议
- ❌ 避免在 updated 事件中直接修改模型(尤其涉及通知、索引更新等副作用操作)
- ✅ 优先使用 saved 或 created 事件替代 updated,或结合 isDirty() 精确控制触发条件
- ✅ 若必须更新字段,请统一使用 withoutEvents() 包裹,防止事件链污染
- ✅ 对 TNTSearch 等索引更新逻辑,建议通过队列异步处理,与通知解耦
通过以上任一方案,即可彻底终止无限循环,确保通知逻辑稳定、可预测、可维护。


















