Ok so in many cases you want to update integers in a database incrementally, this helps ensure that the data is as correct as possible (of course you may also want to look at locking but that depends on your particular application).
Laravel offers the increment() method on models but this makes no effort to keep the model's local properties up to date (for example if you run increment('votes', 10) on a an Eloquent object the $votes property will be stale).
My solution (so far) is the below incrementalSave() method, which allows you to modify Eloquent's integer properties as normal but still have them safely saved to the database incrementally (if this is the behaviour you want).
So you may do something like this, where the book already has 4 votes:
$book = Book::find(1);
$book->votes += 1;
$book->vote_at = '2014-07-01 00:00:00';
$book->save();
$book = Book::find(1);
$book->votes += 1;
$book->vote_at = '2014-07-01 00:00:00';
$book->incrementalSave(['votes']);
and that would give you the following differing queries:
UPDATE `books` SET `votes` = 5, `vote_at` = '2014-07-01 00:00:00' WHERE `id` = 1;
UPDATE `books` SET `votes` = `votes` + 1, `vote_at` = '2014-07-01 00:00:00' WHERE `id` = 1;
Of course the second query increments the votes safely, whilst maintaining the local model representation.
What do people think about something like this being submitted as a PR? I'm sure this is not suitable at the moment but that's why I'm posting here, for feedback from those who are more skilled than me. The biggest problem I have currently is that at the time model events are fired, attributes are an instance of whatever DB::raw() is returning.
class BaseModel extends Eloquent
{
public function incrementalSave($incremental_columns)
{
$updated_values = [];
foreach($incremental_columns AS $column) {
if($this->isDirty($column)) {
// Get the original value and type check it.
$value_original = $this->getOriginal($column);
if(!is_int($value_original)) {
throw new Exception(sprintf('The column `%s` cannot be saved incrementally, expected INT for original value but received %s.', $column, gettype($value_original)));
}
// Get the value now and type check it.
$value_now = $this->getAttribute($column);
if(!is_int($value_now)) {
throw new Exception(sprintf('The column `%s` cannot be saved incrementally, expected INT for updated value but received %s.', $column, gettype($value_now)));
}
// Set the attribute to be a raw incremental query.
$difference = $value_now - $value_original;
$this->setAttribute($column, DB::raw(sprintf('`%s` %s %d', $column, $difference < 0 ? '-' : '+', abs($difference))));
$updated_values[$column] = $value_now;
}
}
// Try to save, re-throwing any exception but making sure that all
// attributes are set back to their proper values again.
try {
$this->save();
} catch (Exception $e) {
throw $e;
} finally {
foreach($updated_values AS $column => $amount) {
$this->setAttribute($column, $amount);
}
}
}
}
Let me know what you think.. I think something like this needs to be part of Laravel because over-writing values is a bad practise in many applications and the existing ->increment() method is not great (limiting to one column, doesn't update the model representation, etc).
Cheers
It looks like my main concern of the model's value not being updated has actually already been fixed by Taylor here: https://github.com/laravel/framework/commit/b18c7258f7ae8ba3a01a41c272cbf2f7e0b75005
Sadly the increment() and decrement() methods are still more limiting IMO. For reasons such as being unable to increment/decrement multiple columns at once, and the fact the model gets saved at the time of the increment/decrement call.
Sign in to participate in this thread!
The Laravel portal for problem solving, knowledge sharing and community building.
The community