From 39fca5a9acb20395aca6534f09bdc02f792be291 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 30 Dec 2022 12:29:22 +1100 Subject: [PATCH] [9.x] Fix decimal cast precision issue (#45456) * add failing test for decimal precision * fix decimal cast precision with manual string manipulation --- .../Eloquent/Concerns/HasAttributes.php | 11 +++- .../EloquentModelDecimalCastingTest.php | 62 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 4b787352cfc8..e0e532e6fdba 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -31,6 +31,7 @@ use ReflectionClass; use ReflectionMethod; use ReflectionNamedType; +use TypeError; trait HasAttributes { @@ -1318,7 +1319,15 @@ public function fromFloat($value) */ protected function asDecimal($value, $decimals) { - return number_format($value, $decimals, '.', ''); + $value = (string) $value; + + if (! is_numeric($value)) { + throw new TypeError('$value must be numeric.'); + } + + [$int, $fraction] = explode('.', $value) + [1 => '']; + + return $int.'.'.Str::of($fraction)->limit($decimals, '')->padLeft($decimals, '0'); } /** diff --git a/tests/Integration/Database/EloquentModelDecimalCastingTest.php b/tests/Integration/Database/EloquentModelDecimalCastingTest.php index fb7e35d49001..33e9c5ded67f 100644 --- a/tests/Integration/Database/EloquentModelDecimalCastingTest.php +++ b/tests/Integration/Database/EloquentModelDecimalCastingTest.php @@ -6,6 +6,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use Illuminate\Tests\Integration\Database\DatabaseTestCase; +use TypeError; class EloquentModelDecimalCastingTest extends DatabaseTestCase { @@ -18,6 +19,67 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() }); } + public function testItThrowsOnNonNumericValues() + { + $model = new class extends Model { + public $timestamps = false; + + protected $casts = [ + 'amount' => 'decimal:20', + ]; + }; + $model->amount = 'foo'; + + $this->expectException(TypeError::class); + + $model->amount; + } + + public function testItHandlesLargeNumbers() + { + $model = new class extends Model { + public $timestamps = false; + + protected $casts = [ + 'amount' => 'decimal:20', + ]; + }; + + $model->amount = '0.89898989898989898989'; + $this->assertSame('0.89898989898989898989', $model->amount); + + $model->amount = '89898989898989898989'; + $this->assertSame('89898989898989898989.00000000000000000000', $model->amount); + } + + public function testItTrimsLongValues() + { + $model = new class extends Model { + public $timestamps = false; + + protected $casts = [ + 'amount' => 'decimal:20', + ]; + }; + + $model->amount = '0.89898989898989898989898989898989898989898989'; + $this->assertSame('0.89898989898989898989', $model->amount); + } + + public function testItDoesntRoundNumbers() + { + $model = new class extends Model { + public $timestamps = false; + + protected $casts = [ + 'amount' => 'decimal:1', + ]; + }; + + $model->amount = '0.99'; + $this->assertSame('0.9', $model->amount); + } + public function testDecimalsAreCastable() { $user = TestModel1::create([