When a model is updated in an event listener, and that event is caused by a preceding update on that same model, an infinite loop occurs.
This is only the case when the event listener runs synchronously, not asynchronously on a queue.
In the model, listen for a model updated event; and if the "status" attribute changes, trigger a MyModelStatusChangedEvent event.
App\Models\MyModel.php
<?php
namespace App\Models;
use App\Events\MyModelStatusChangedEvent;
use Illuminate\Database\Eloquent\Model;
class MyModel extends Model
{
public static function boot()
{
static::updated(function (MyModel $model) {
if ($model->isDirty('status')) {
event(
new MyModelStatusChangedEvent(
$model,
$model->getOriginal('status'),
$model->status
)
);
}
});
parent::boot();
}
}
App\EventsMyModelStatusChangedEvent.php
<?php
namespace App\Events;
use App\Models\MyModel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
class MyModelStatusChangedEvent
{
use Dispatchable, SerializesModels;
/**
* @var MyModel
*/
public $model;
/**
* @var string
*/
public $oldStatus;
/**
* @var string
*/
public $newStatus;
/**
* @param MyModel $model
* @param string $oldStatus
* @param string $newStatus
*/
public function __construct(MyModel $model, $oldStatus, $newStatus)
{
$this->model = $model;
$this->oldStatus = $oldStatus;
$this->newStatus = $newStatus;
}
}
App\Listeners\TouchMyModelStatusChangedDate.php
<?php
namespace App\Listeners;
use App\Events\MyModelStatusChangedEvent;
class TouchMyModelStatusChangedDate
{
/**
* Handle the event.
*
* @param MyModelStatusChangedEvent $event
*/
public function handle(MyModelStatusChangedEvent $event)
{
if ($event->oldStatus === 'APPROVED') {
\Log::info('Updating blah to bleh after status change to APPROVED.');
$event->model->update(['blah' => 'bleh']);
}
}
}
EventServiceProvider.php, bind the listener to the event:/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
// ....
MyModelStatusChangedEvent::class => [
TouchMyModelStatusChangedDate::class,
],
];
$model = \App\Models\MyModel::find(1);
$model->update(['status' => 'APPROVED']);
You would expect the listener to trigger a single update, but it fires multiple times.
This is because the update happens in a listener, on the same model, between the model firing the "updated" event and a call to both syncChanges and syncOriginal.
The status field is seen as dirty in the 2nd update $event->model->update(['blah' => 'bleh']), even though it's already been updated, so this causes a subsequent MyModelStatusChangedEvent to fire, and so on...
I wouldn鈥檛 say it鈥檚 a bug, it鈥檚 expected behavior. Did you consider using MyModel::updating event?
It feels weird that this is expected behaviour?
I would expect this behaviour on an updating event, as the entity isn't updated yet. In updated, the data should no longer be dirty.
Closing as it's the expected behaviour.
How did you fix this @matthewgoslett?
How did you fix this @matthewgoslett?
There's no pretty solution here unfortunately.
Here's a snippet from a current code base which prevents this unexpected behaviour.

I do a clone and syncOriginal on the cloned event, which then gets passed to custom events.
Most helpful comment
There's no pretty solution here unfortunately.
Here's a snippet from a current code base which prevents this unexpected behaviour.
I do a
cloneandsyncOriginalon the cloned event, which then gets passed to custom events.