While working on a delivery order processing flow, I encountered an issue where two consecutive operations produced an unexpected quantity.
The flow itself was straightforward:
public function handle(SalesOrder $sales_order): void
{
$this->processingService->attachToDeliveryOrder(
$sales_order,
$delivery_order
);
// another process
$this->processingService->completeDeliveryOrder(
$sales_order,
$delivery_order
);
}
The first operation prepares the remaining sales order quantity for a delivery order. The second operation completes the delivery and moves that prepared quantity into the processed quantity.
At first glance, both operations are working with the same SalesOrder.
The problem was that they weren’t necessarily working with the same Eloquent model instances.
Querying a Relationship Can Create a Second Instance
Inside attachToDeliveryOrder(), the sales order details were retrieved
through the relationship query:
protected function attachToDeliveryOrder(
SalesOrder $sales_order,
DeliveryOrder $delivery_order
): void {
$sales_order_details = $sales_order->sales_order_details()
->get();
foreach ($sales_order_details as $detail) {
$detail->increment(
'prepared_do_quantity',
$remaining_quantity
);
}
}
Later, completeDeliveryOrder() accessed the already-loaded
relationship:
protected function completeDeliveryOrder(
SalesOrder $sales_order,
DeliveryOrder $delivery_order
): void {
foreach ($sales_order->sales_order_details as $detail) {
$detail->increment(
'processed_do_quantity',
$detail->prepared_do_quantity
);
}
}
These two expressions look similar:
$sales_order->sales_order_details()
$sales_order->sales_order_details
But they have an important difference.
The first returns a relationship query. Calling get() executes another
database query and hydrates the results into new Eloquent model
instances.
The second accesses the relationship collection already loaded on
$sales_order.
As a result, the application could end up with two objects representing the exact same database row.
One Database Row, Two PHP Objects
The execution flow effectively looked like this:
SalesOrderDetail #123
│
┌────────────┴────────────┐
│ │
Loaded Instance New Instance
│ │
│ attachToDeliveryOrder()
│ │
│ prepared += quantity
│
│
completeDeliveryOrder()
│
└── reads the old in-memory state
Both instances have the same primary key and represent the same database record, but they are different PHP objects.
This can easily be overlooked during debugging because:
$loaded_detail->id === $queried_detail->id;
// true
while:
$loaded_detail === $queried_detail;
// false
Updating one instance does not synchronize every other instance of that model currently living in memory.
For example, assume the database row starts with a prepared quantity of
0, and the remaining quantity is 5:
// New instance retrieved by attachToDeliveryOrder()
$queried_detail->increment('prepared_do_quantity', 5);
// The database value is now 5, but this is a different instance.
$loaded_detail->prepared_do_quantity;
// 0
$loaded_detail->increment(
'processed_do_quantity',
$loaded_detail->prepared_do_quantity
);
// processed_do_quantity += 0
The second operation reads the stale value from $loaded_detail, even
though the database already contains the updated prepared quantity.
That distinction was the root cause of the incorrect quantity calculation.
Reuse the Loaded Relationship Collection
In this particular flow, I didn’t need another representation of the
sales order details. I needed to continue working with the instances
already attached to the SalesOrder.
Instead of querying the relationship again:
$sales_order_details = $sales_order->sales_order_details()->get();
I reused the already-loaded relationship collection:
$sales_order_details = $sales_order->sales_order_details;
Now both operations work with the same model instances throughout the process:
SalesOrderDetail #123
│
Instance A
│
┌─────────┴─────────┐
│ │
attachToDeliveryOrder completeDeliveryOrder
│ │
prepared += qty processed += prepared
│ │
└─────────┬─────────┘
│
Same Instance
Why Object Identity Matters
The interesting part of this bug wasn’t increment(), the relationship
itself, or even the quantity calculation.
It was object identity.
In Eloquent, the same database record can be represented by multiple model instances during a single request. Querying a relationship again does not guarantee that you’ll continue working with an instance that already exists elsewhere in your application flow.
This distinction matters when multiple operations depend on in-memory changes made earlier in the same process.
A useful mental model is:
Same primary key means the same database record. It does not mean the same PHP object.
Relationship queries and loaded relationships are therefore not always interchangeable:
$model->relation() // relationship query
$model->relation // loaded relationship value
Most of the time, that distinction is harmless. But when several services mutate the same models sequentially, it can become a surprisingly subtle source of bugs.
