skip to Main Content

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


  1. Chosen as BEST ANSWER

    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:

    protected function refundedAmount(): Attribute
    {
        return new Attribute(
            set: function (Money $value, array $attributes) {
                if (
                    $attributes['currency'] instanceof Currency &&
                    $value->getCurrency()->getCode() !== $attributes['currency']->getCode()
                ) {
                    throw new InvalidMoneyException(
                        "{$value->getCurrency()->getCode()} does not equal existing currency {$attributes['currency']->getCode()}"
                    );
                }
    
                return [
                    'currency'        => $value->getCurrency()->getCode(),
                    'refunded_amount' => $value->getAmount(),
                ];
            },
        );
    }
    

    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.


  2. 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 the attributes 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 a refunded_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;

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search