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.