
在 Laravel 中,若在模型事件(如 updated)中发送通知,而通知逻辑又通过 markAsSent 更新同一模型字段,将引发递归更新,造成无限通知循环。关键在于切断“更新 → 通知 → 更新”的闭环。
在 laravel 中,若在模型事件(如 updated)中发送通知,而通知逻辑又通过 `markassent` 更新同一模型字段,将引发递归更新,造成无限通知循环。关键在于切断“更新 → 通知 → 更新”的闭环。
当你在模型的 updated 事件中调用 $model->notifyNow(new LatiniaNotification($body)),且该通知的 markAsSent 方法执行了 $notifiable->update([...])(例如设置 cc_mail_sent => true),就会再次触发模型的 updated 事件——从而再次调用通知逻辑,形成死循环。
尽管你已定义 shouldSend() 并返回 false,但它不会被自动调用:Laravel 的 notifyNow() 和 notify() 默认跳过 shouldSend 检查,除非你显式启用。该方法仅在通知分发前由 NotificationSender 的 sendNow() 或 send() 内部调用,但前提是通知实例未被手动绕过校验流程(如自定义通道未配合框架约定)。
更根本的问题在于设计耦合:markAsSent() 直接修改模型状态,而该状态变更又激活监听器。解决思路不是禁用事件,而是隔离副作用:
✅ 推荐方案:禁用事件再更新
在 markAsSent() 中临时停用模型事件,更新后恢复:
public function markAsSent($notifiable)
{
if (!is_null($this->sentField)) {
// 临时禁用事件,避免递归触发
$wasDispatching = $notifiable->dispatchesEvents;
$notifiable->dispatchesEvents = [];
$notifiable->update([
$this->sentField => true
]);
// 恢复事件监听(可选,若需后续事件)
$notifiable->dispatchesEvents = $wasDispatching;
}
}
⚠️ 更稳健的做法是使用 withoutEvents() 辅助函数(Laravel 9+)或静态开关:
public function markAsSent($notifiable)
{
if (!is_null($this->sentField)) {
\Illuminate\Database\Eloquent\Model::withoutEvents(function () use ($notifiable) {
$notifiable->update([$this->sentField => true]);
});
}
}
✅ 替代方案:改用数据库原子操作或队列延迟更新
若业务允许,将状态更新移至队列任务中异步执行,彻底脱离当前请求生命周期:
public function markAsSent($notifiable)
{
if (!is_null($this->sentField)) {
UpdateNotificationSentField::dispatch($notifiable, $this->sentField);
}
}
? 额外注意点:
- notifyNow() 不走队列,但依然会触发模型事件;notify() 若实现 ShouldQueue 也仅延迟执行,不规避事件触发。
- TNTSearch 的索引更新队列与此问题无直接关联,但若其监听了同一字段变更,可能加剧日志噪音——建议检查 tntsearch.php 配置中的监听字段是否包含 cc_mail_sent。
- 始终验证 shouldSend() 是否被调用:可通过在方法内添加 Log::debug('shouldSend called') 确认;若未打印,则说明通知未经过标准分发流程(如直接调用通道 send())。
总结:无限循环的本质是同步副作用引发的事件重入。优先采用 Model::withoutEvents() 隔离更新,辅以清晰的职责分离(通知发送 ≠ 状态持久化),即可彻底解决该类陷阱。











