For a deeper understanding of how it works, you can have a look at the class \Illuminate\Database\Eloquent\Model, in the method performUpdate(Builder $query, array $options = []) :
protected function performUpdate(Builder $query, array $options = [])
// [...]
// First we need to create a fresh query instance and touch the creation and
// update timestamp on the model which are maintained by us for developer
// convenience. Then we will just continue saving the model instances.
if ($this->timestamps && Arr::get($options, 'timestamps', true)) {
$this->updateTimestamps();
}
// [...]
The timestamps fields are updated only if the public property timestamps equals true or Arr::get($options, 'timestamps', true) returns true (which it does by default if the $options array does not contain the key timestamps).
As soon as one of these two returns false, the timestamps fields are not updated.
I ran into the situation of needing to do a mass update that involves a join, so updated_at was causing duplicate column conflicts. I fixed it with this code without needing a scope:
Which works fine, but if I use firstOrNew then the $item->save(['timestamps' => false]); does not work.
// This does not work on Seeder file
$item = Model::firstOrNew(['name' => 'item_name']);
$item->description = 'some description';
$item->save(['timestamps' => false]);
// Doing the following works tho
$item = Model::firstOrNew(['name' => 'item_name']);
$item->description = 'some description';
$item->timestamps = false;
$item->save();
So in some cases you would use one over the other... Just check with die and dump to see whether +timestamps: false