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);
"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);
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)
That is a hell of a gotcha.
This is one of the reasons I'm not that found on using the
firstOrmethods.This is more intentional which makes it less prone to errors.