DEV Community

Cover image for What I Found Inside Eloquent's increment()
Bakoulis George
Bakoulis George

Posted on • Originally published at georgebakoulis.dev

What I Found Inside Eloquent's increment()

I was experimenting with firstOrNew() and increment() in my Laravel lab and ended up running a query I did not expect.

I was building a small inventory tracker to see how these two methods interact. The idea is that when a shipment comes in, find the product and bump its stock count. If it doesn't exist yet, a new record gets created first. firstOrNew() seemed like the right tool, one call, handles both cases.

If you've ever used these two together, this is worth knowing.

$incomingShipment = $inventoryItem::firstOrNew(['product_name' => 'Metallic Coffee Mug']);
$incomingShipment->increment('stock_count', 50);
Enter fullscreen mode Exit fullscreen mode

"Metallic Coffee Mug" wasn't in the database. So firstOrNew() returned a new unsaved model instance — no id, no primary key, and $model->exists = false.

Then I called increment().

What the query log showed

I had three items in the table going in:

product_name stock_count
macbook air M4 10
DELL monitor 5
Varmilo Keyboard 3

After increment('stock_count', 50), every single row got updated:

product_name stock_count
macbook air M4 60
DELL monitor 55
Varmilo Keyboard 53

No error. No warning. The Coffee Mug still doesn't exist — increment() doesn't insert.

How increment() actually works

Digging into the source, increment() internally calls incrementOrDecrement(), which checks $this->exists before building the query.

If exists = false the model hasn't been saved yet — it runs the UPDATE without a WHERE clause. The whole table gets hit.

If exists = true, it scopes the query to the model's primary key. That's what you'd normally expect.

firstOrNew() when it finds nothing returns an unsaved model. So exists is false, and increment() takes the unscoped path.

Using these two together correctly

Once you know what exists does, the behaviour makes complete sense. The fix is to make sure the model is persisted before you call increment().

$incomingShipment = $inventoryItem::firstOrNew(['product_name' => 'Metallic Coffee Mug']);

if (! $incomingShipment->exists) {
    $incomingShipment->stock_count = 0;
    $incomingShipment->save();
}

$incomingShipment->increment('stock_count', 50);
Enter fullscreen mode Exit fullscreen mode

Or use firstOrCreate() instead, it persists the record immediately, so increment() always has an id to scope to.

The more I dig into Eloquent's internals, the more I see how consistently it's designed around the exists flag. It's a small property that a lot of methods quietly depend on.

Top comments (1)

Collapse
 
xwero profile image
david duymelinck

That is a hell of a gotcha.

This is one of the reasons I'm not that found on using the firstOr methods.

$data = ['product_name' => 'Metallic Coffee Mug', 'stock_count' => 50]:

$result = Inventoryitem::where('product_name', $data['product_name'])->get()->first();

is_null($result) ?  Inventoryitem::create($data) : $result->increment('stock_count', $data['stock_count']);
Enter fullscreen mode Exit fullscreen mode

This is more intentional which makes it less prone to errors.