I am using the Cknow/Money
package to handle money values in models.
I have this model event that validates a value before saving to prevent wrong data from being entered due to a common scenario:
I have this laravel 10 saving event:
static::saving(function (self $order) {
if ($order->refunded_amount->greaterThan($order->total_amount)) {
// Can't refund more than the order total.
$order->refunded_amount = $order->total_amount;
dd($order->refunded_amount); // This is still the original refunded_amount value instead of the new total_amount value
}
});
If I check the value of $order->refunded_amount
after it is set to $order->total_amount
, the value is still the wrong refunded amount as if the assignment didn’t work somehow.
On the model, these are both cast:
protected $casts = [
'total_amount' => MoneyIntegerCast::class.':currency',
'refunded_amount' => MoneyIntegerCast::class.':currency',
];
I have a mutator which sets the value on the model:
public function setRefundedAmountAttribute(Money $value): void
{
if ($this->currency instanceof Currency && $value->getCurrency()->getCode() !== $this->currency->getCode()) {
throw new InvalidMoneyException(
"{$value->getCurrency()->getCode()} does not equal existing currency {$this->currency->getCode()}"
);
}
$this->attributes['currency'] = $value->getCurrency();
$this->attributes['refunded_amount'] = $value->getAmount();
}
I’ve tried removing the mutator to see if that is the problem, but that does not help.
I can assign a value outside of the saving
event and no issues:
$someMoney = Money::USD(10.00);
$order = new Order();
$order->refunded_amount = $someMoney;
$order->save();
This only happens with objects within the Eloquent models. If I were to have something like refund_count
and total_count
and both are integers, then the result would be that refund_count
would now equal total_count
as expected.
To be clear, this isn’t just happening with these money objects. It is also happening with dates that Laravel has cast to Carbon objects.
protected $casts = ['renewed_at' => 'datetime'];
static::saving(function (self $order) {
$order->renewed_at = now();
dd($order->renewed_at); // The renewed_at value will still be what it was originally set to instead of now()
});
If I refresh the model after saving, it will show the updated timestamp values from the saving event, but it won’t show them if I add a dd()
right after the value is set within that event. This implies there is some sort of caching within the model going on after the accessor is first called.
This isn’t the case with the money objects. They don’t change within the saving event and they don’t change after a refresh.
Is there some sort of model level caching introduced in Laravel after v8? I don’t remember this being an issue in earlier versions but it is happening in v10.
2
Answers
The issue is that Laravel 10 didn't work well with the legacy mutator definition in
setRefundedAmountAttribute()
.To fix this issue, I had to change it to the new L10 way of doing mutators:
It still won't show the value as changed right after updating it in the
saving
event, but after the order model is refreshed, I can now see that it is correct.However, if I also simply add an accessor
get: fn($value) => $value,
to the mutator, then it also updates the value the moment it is changes in the saving event. I also don't need to explicitly refresh the model to see the changes after saving for the money objects...but I still have to for the timestamp objects.I'm assuming if I create custom accessors for all of the model's timestamps to just return the value as is, then that will bust whatever caching is happening behind the scenes in the Eloquent core.
What is happening is that
refunded_amount
is not actually a property of the order object. Instead when you request this value, Laravel is pulling it from theattributes
list it populated when the model is hydrated from the database.Now when you do
$order->refunded_amount = $order->total_amount
you are actually dynamically creating arefunded_amount
property and that is where that value is being set.Instead you want to modify the existing
refunded_amount
on your model’s attributes list as that is what will be saved. So the correct way would be:$order->attributes['refunded_amount'] = $order->total_amount;