Framework: Infinite loop when a model is updated in an event listener, and that event is cuased by a preceding update on that same model.

Created on 23 Apr 2018  路  5Comments  路  Source: laravel/framework

Description:

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.

Steps To Reproduce:

  1. Create a dummy model.

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;
    }
}
  1. Create a listener which triggers an update to the same model when the status changes to 'APPROVED'.

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']);
        }
    }
}
  1. In your EventServiceProvider.php, bind the listener to the event:
/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    // ....

    MyModelStatusChangedEvent::class => [
        TouchMyModelStatusChangedDate::class,
    ],
];
  1. Trigger an update to the status of a dummy model.
$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...

Most helpful comment

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.

screenshot_1363

I do a clone and syncOriginal on the cloned event, which then gets passed to custom events.

All 5 comments

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.

screenshot_1363

I do a clone and syncOriginal on the cloned event, which then gets passed to custom events.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

iivanov2 picture iivanov2  路  3Comments

SachinAgarwal1337 picture SachinAgarwal1337  路  3Comments

CupOfTea696 picture CupOfTea696  路  3Comments

lzp819739483 picture lzp819739483  路  3Comments

felixsanz picture felixsanz  路  3Comments