Zebra Codes

Laravel’s touch() function is broken

4th of September, 2021

After losing a few hours debugging an obscure and difficult to reproduce issue, I can say that the touch() function in Eloquent’s database model is best avoided.

What it Should Do

The operation is simple: Model::touch() will update the modified_at timestamp on your model. This might be useful if you want to mark a record as having been changed without actually changing it, for example you might touch() a parent record after modifying a child record.

A model can also call touch() automatically on another model by setting the $touches magic property to an array of model names.

What it Doesn’t Do

Calling touch() will update the database, which triggers the update() callback method on your model. The problem is that the modified_at timestamp has a resolution of one second, calling touch() twice in the same second will only call your update() callback once. This will cause a failure if you are expecting the update() callback to be triggered each time you call touch().

The code snippet below demonstrates the problem. You register an update event callback in the Model’s boot() function that will perform some action – perhaps updating another system, audit logging, or anything else. In your doWork() function you trigger the update event twice. Although you trigger the event twice, your event handler may only be called once.

class Widget extends Model
{
    /**
     * This is called automatically by
     * Laravel when the model is created.
     */
    public function boot()
    {
        // Register an event handler for when the model is
        // saved to the database.
        static::updated(function () {
            doSomethingImportant();
        }
    }

    /**
     * Some function...
     */
    public function doWork()
    {
        // Do some work.
        // Trigger the 'update' event.
        $this->touch(); // Calls doSomethingImportant().

        // Do some more work.
        // Trigger the 'update' event again.
        $this->touch(); // May or may not call doSomethingImportant(),
                        // depending on how long the work took.
    }
}

If more than a second passed between the two calls to $this->touch() then doSomethingImportant() will be called twice; if less than a second passed, then it will only be called once because the updated_at value will not change due its one second resolution.

The Remedy

Unfortunately there is no fix for this coming in Laravel, so the only solution is to avoid using the touch() function and avoid doing anything important in the updated event handler.