Skip to content

[9.x] Improve decimal cast fix#45492

Merged
taylorotwell merged 1 commit into
laravel:9.xfrom
timacdonald:decimal
Jan 3, 2023
Merged

[9.x] Improve decimal cast fix#45492
taylorotwell merged 1 commit into
laravel:9.xfrom
timacdonald:decimal

Conversation

@timacdonald

@timacdonald timacdonald commented Jan 3, 2023

Copy link
Copy Markdown
Member

This is option 1 of 2 for potential further fixes to the decimal model cast.

If you want to read up on what PHP considers a numeric string, check the docs.

Solution 1 (this PR)

A more comprehensive fix than the initial fix, taking the same approach.

This PR is important for two reasons:

  1. The first PR I submitted uses padLeft instead of padRight for the precision (🤦‍♂️).
  2. I believe that we should also handle string values when it comes to floats, as floats are often passed around and stored as strings, to not exceed overflows / lose precision of very large values.

Regarding 2 (handling strings), the additional improvements included in this PR are:

  • If an exponential is passed through as a string, we will throw (this matches the behaviour of bcmath).

Note The original number_format approach was able to handle these as values would be cast to a float internally. We are unable to cast to a float as doing so may lose precision, which was the issue in the first place that we are trying to address here.

So the decimal cast no longer supports string based numbers using exponents, whether bcmath is installed or not.

If we do not throw an exception, we will end up with weird values, as "1e3" is considered a numeric string by PHP:

$model = new class extends Model
{
    protected $casts = [
        'amount' => 'decimal:20',
    ];
};
$model->amount = '1e3';

$model->amount;
// 1e3.00000000000000000000
  • Correctly handle missing integer values, e.g. ".1"

Pros

  • We get the full fix without requiring bcmath in a patch release.
  • The return values are the same for both branches.

Cons

  • I think the code wrangling we are doing speaks for itself in that regards 🤢
  • Different exception types are thrown in the different code branches.

Solution 2 (#45494)

This solution introduces all the fixes and changes that this PR introduces, but only when bcmath is installed.

Otherwise it falls back to number_format.

Pros

  • A much cleaner solution.

Cons

Laravel 10.x

As mentioned in the original PR, in Laravel 10.x I would like to reduce this to just use bcmath and no other implementations.

@joshhanley

Copy link
Copy Markdown
Contributor

@timacdonald not sure yet if this is an issue for us or an issue with this PR, but we're now experiencing failing tests in Livewire's core, which were passing in Laravel 9.45.1, but failing when running against 9.46.0.

image

Have a look at the failing test results here https://github.com/livewire/livewire/actions/runs/3852570505/jobs/6564814421

And these are the two tests that are failing https://github.com/livewire/livewire/blob/4cc5dedaab1e9512efb4d528fde67df98e9b465a/tests/Unit/ModelAttributesCanBeCastTest.php#L157-L195

I believe our tests are correct with the rounding, but would appreciate if you can have a look if you get the chance.

Thanks! 🙂

@timacdonald

Copy link
Copy Markdown
Member Author

Hey Josh,

Unfortunately this was a "fix" that was is also potentially breaking.

You can see the initial PR here: #45456 which outlines that the new solution, and the solution improved on here, no longer rounds values.

I'll re-flag this with Taylor so he is aware, but i'm not sure there is much we can do when dealing with unknown sized floats as we are just delegating to BCMath, which does not provide rounding.

@joshhanley

Copy link
Copy Markdown
Contributor

@timacdonald ah right, ok that makes sense why it breaks our tests as they assume rounding!

Thanks I will see if I can wrap my head around the change, I think it will just mean updating our tests to not assume rounding.

We have tests matrix that tests multiple Laravel versions though, so we will probably need to add Laravel version checks to make sure our assertions assert for rounding/non-rounding depending on which version we're testing. Unless you've got any better ideas for handling it? 😅

Thanks for the quick response 🙂

@timacdonald

Copy link
Copy Markdown
Member Author

No worries. A version check makes sense here I think.

@barryvdh

barryvdh commented Jan 9, 2023

Copy link
Copy Markdown
Contributor

I see that my initial implementation in #26173 might have been a bit simplistic, but I'm not sure if rounding is a problem? I guess I would expect rounding when losing precision?

The use-case was mainly to prevent inconsistencies when inserting/loading data from a decimal field in the database vs data that changed in that request. If I save 8.55 in a decimal 6,1 field (1 digit precision), it is store as 9, not as 8. It would be weird to changed that, right?

@timacdonald

Copy link
Copy Markdown
Member Author

@barryvdh your initial implementation was fine. It is just floats being floats.

The issue is more complicated due to large floats, where need to rely on bcmath, which does not support rounding.

I personally feel supporting large numbers is more important than rounding.

@barryvdh

Copy link
Copy Markdown
Contributor

Yeah maybe, but changing the rounding after 5 year could potentially have more effect for existing code then rounding handling large numbers which were broken already? Maybe we can handle both?

@barryvdh

Copy link
Copy Markdown
Contributor

@timacdonald something like this? #45580
That restores rounding and throws an error when precision is to high. Or would it be better to just fail silently in 9.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants