Skip to content

Commit bc209f7

Browse files
authored
Fix casting when setting an attribute (#2653)
1 parent f5ed7bf commit bc209f7

14 files changed

+525
-23
lines changed

src/Eloquent/Model.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@
44

55
namespace MongoDB\Laravel\Eloquent;
66

7+
use Brick\Math\BigDecimal;
8+
use Brick\Math\Exception\MathException as BrickMathException;
9+
use Brick\Math\RoundingMode;
710
use DateTimeInterface;
811
use Illuminate\Contracts\Queue\QueueableCollection;
912
use Illuminate\Contracts\Queue\QueueableEntity;
1013
use Illuminate\Contracts\Support\Arrayable;
14+
use Illuminate\Database\Eloquent\Casts\Json;
1115
use Illuminate\Database\Eloquent\Model as BaseModel;
1216
use Illuminate\Database\Eloquent\Relations\Relation;
1317
use Illuminate\Support\Arr;
18+
use Illuminate\Support\Exceptions\MathException;
1419
use Illuminate\Support\Facades\Date;
1520
use Illuminate\Support\Str;
1621
use MongoDB\BSON\Binary;
22+
use MongoDB\BSON\Decimal128;
1723
use MongoDB\BSON\ObjectID;
1824
use MongoDB\BSON\UTCDateTime;
1925
use MongoDB\Laravel\Query\Builder as QueryBuilder;
@@ -211,6 +217,11 @@ public function setAttribute($key, $value)
211217
{
212218
$key = (string) $key;
213219

220+
//Add casts
221+
if ($this->hasCast($key)) {
222+
$value = $this->castAttribute($key, $value);
223+
}
224+
214225
// Convert _id to ObjectID.
215226
if ($key === '_id' && is_string($value)) {
216227
$builder = $this->newBaseQueryBuilder();
@@ -237,6 +248,28 @@ public function setAttribute($key, $value)
237248
return parent::setAttribute($key, $value);
238249
}
239250

251+
/** @inheritdoc */
252+
protected function asDecimal($value, $decimals)
253+
{
254+
try {
255+
$value = (string) BigDecimal::of((string) $value)->toScale((int) $decimals, RoundingMode::HALF_UP);
256+
257+
return new Decimal128($value);
258+
} catch (BrickMathException $e) {
259+
throw new MathException('Unable to cast value to a decimal.', previous: $e);
260+
}
261+
}
262+
263+
/** @inheritdoc */
264+
public function fromJson($value, $asObject = false)
265+
{
266+
if (! is_string($value)) {
267+
$value = Json::encode($value ?? '');
268+
}
269+
270+
return Json::decode($value ?? '', ! $asObject);
271+
}
272+
240273
/** @inheritdoc */
241274
public function attributesToArray()
242275
{

tests/Casts/BinaryUuidTest.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use Generator;
88
use MongoDB\BSON\Binary;
9-
use MongoDB\Laravel\Tests\Models\CastBinaryUuid;
9+
use MongoDB\Laravel\Tests\Models\Casting;
1010
use MongoDB\Laravel\Tests\TestCase;
1111

1212
use function hex2bin;
@@ -17,15 +17,15 @@ protected function setUp(): void
1717
{
1818
parent::setUp();
1919

20-
CastBinaryUuid::truncate();
20+
Casting::truncate();
2121
}
2222

2323
/** @dataProvider provideBinaryUuidCast */
2424
public function testBinaryUuidCastModel(string $expectedUuid, string|Binary $saveUuid, Binary $queryUuid): void
2525
{
26-
CastBinaryUuid::create(['uuid' => $saveUuid]);
26+
Casting::create(['uuid' => $saveUuid]);
2727

28-
$model = CastBinaryUuid::firstWhere('uuid', $queryUuid);
28+
$model = Casting::firstWhere('uuid', $queryUuid);
2929
$this->assertNotNull($model);
3030
$this->assertSame($expectedUuid, $model->uuid);
3131
}
@@ -43,9 +43,9 @@ public function testQueryByStringDoesNotCast(): void
4343
{
4444
$uuid = '0c103357-3806-48c9-a84b-867dcb625cfb';
4545

46-
CastBinaryUuid::create(['uuid' => $uuid]);
46+
Casting::create(['uuid' => $uuid]);
4747

48-
$model = CastBinaryUuid::firstWhere('uuid', $uuid);
48+
$model = Casting::firstWhere('uuid', $uuid);
4949
$this->assertNull($model);
5050
}
5151
}

tests/Casts/BooleanTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Tests\Casts;
6+
7+
use MongoDB\Laravel\Tests\Models\Casting;
8+
use MongoDB\Laravel\Tests\TestCase;
9+
10+
class BooleanTest extends TestCase
11+
{
12+
protected function setUp(): void
13+
{
14+
parent::setUp();
15+
16+
Casting::truncate();
17+
}
18+
19+
public function testBool(): void
20+
{
21+
$model = Casting::query()->create(['booleanValue' => true]);
22+
23+
self::assertIsBool($model->booleanValue);
24+
self::assertSame(true, $model->booleanValue);
25+
26+
$model->update(['booleanValue' => false]);
27+
28+
self::assertIsBool($model->booleanValue);
29+
self::assertSame(false, $model->booleanValue);
30+
31+
$model->update(['booleanValue' => 1]);
32+
33+
self::assertIsBool($model->booleanValue);
34+
self::assertSame(true, $model->booleanValue);
35+
36+
$model->update(['booleanValue' => 0]);
37+
38+
self::assertIsBool($model->booleanValue);
39+
self::assertSame(false, $model->booleanValue);
40+
}
41+
42+
public function testBoolAsString(): void
43+
{
44+
$model = Casting::query()->create(['booleanValue' => '1.79']);
45+
46+
self::assertIsBool($model->booleanValue);
47+
self::assertSame(true, $model->booleanValue);
48+
49+
$model->update(['booleanValue' => '0']);
50+
51+
self::assertIsBool($model->booleanValue);
52+
self::assertSame(false, $model->booleanValue);
53+
}
54+
}

tests/Casts/CollectionTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Tests\Casts;
6+
7+
use Illuminate\Support\Collection;
8+
use MongoDB\Laravel\Tests\Models\Casting;
9+
use MongoDB\Laravel\Tests\TestCase;
10+
11+
use function collect;
12+
13+
class CollectionTest extends TestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
Casting::truncate();
20+
}
21+
22+
public function testCollection(): void
23+
{
24+
$model = Casting::query()->create(['collectionValue' => ['g' => 'G-Eazy']]);
25+
26+
self::assertInstanceOf(Collection::class, $model->collectionValue);
27+
self::assertEquals(collect(['g' => 'G-Eazy']), $model->collectionValue);
28+
29+
$model->update(['collectionValue' => ['Dont let me go' => 'Even the longest of nights turn days']]);
30+
31+
self::assertInstanceOf(Collection::class, $model->collectionValue);
32+
self::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue);
33+
}
34+
}

tests/Casts/DateTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Tests\Casts;
6+
7+
use DateTime;
8+
use Illuminate\Support\Carbon;
9+
use MongoDB\Laravel\Tests\Models\Casting;
10+
use MongoDB\Laravel\Tests\TestCase;
11+
12+
use function now;
13+
14+
class DateTest extends TestCase
15+
{
16+
protected function setUp(): void
17+
{
18+
parent::setUp();
19+
20+
Casting::truncate();
21+
}
22+
23+
public function testDate(): void
24+
{
25+
$model = Casting::query()->create(['dateField' => now()]);
26+
27+
self::assertInstanceOf(Carbon::class, $model->dateField);
28+
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
29+
30+
$model->update(['dateField' => now()->subDay()]);
31+
32+
self::assertInstanceOf(Carbon::class, $model->dateField);
33+
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
34+
35+
$model->update(['dateField' => new DateTime()]);
36+
37+
self::assertInstanceOf(Carbon::class, $model->dateField);
38+
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
39+
40+
$model->update(['dateField' => (new DateTime())->modify('-1 day')]);
41+
42+
self::assertInstanceOf(Carbon::class, $model->dateField);
43+
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
44+
}
45+
46+
public function testDateAsString(): void
47+
{
48+
$model = Casting::query()->create(['dateField' => '2023-10-29']);
49+
50+
self::assertInstanceOf(Carbon::class, $model->dateField);
51+
self::assertEquals(
52+
Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'),
53+
(string) $model->dateField,
54+
);
55+
56+
$model->update(['dateField' => '2023-10-28']);
57+
58+
self::assertInstanceOf(Carbon::class, $model->dateField);
59+
self::assertEquals(
60+
Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('Y-m-d H:i:s'),
61+
(string) $model->dateField,
62+
);
63+
}
64+
}

tests/Casts/DatetimeTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Tests\Casts;
6+
7+
use Illuminate\Support\Carbon;
8+
use MongoDB\Laravel\Tests\Models\Casting;
9+
use MongoDB\Laravel\Tests\TestCase;
10+
11+
use function now;
12+
13+
class DatetimeTest extends TestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
Casting::truncate();
20+
}
21+
22+
public function testDate(): void
23+
{
24+
$model = Casting::query()->create(['datetimeField' => now()]);
25+
26+
self::assertInstanceOf(Carbon::class, $model->datetimeField);
27+
self::assertEquals(now()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
28+
29+
$model->update(['datetimeField' => now()->subDay()]);
30+
31+
self::assertInstanceOf(Carbon::class, $model->datetimeField);
32+
self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
33+
}
34+
35+
public function testDateAsString(): void
36+
{
37+
$model = Casting::query()->create(['datetimeField' => '2023-10-29']);
38+
39+
self::assertInstanceOf(Carbon::class, $model->datetimeField);
40+
self::assertEquals(
41+
Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'),
42+
(string) $model->datetimeField,
43+
);
44+
45+
$model->update(['datetimeField' => '2023-10-28 11:04:03']);
46+
47+
self::assertInstanceOf(Carbon::class, $model->datetimeField);
48+
self::assertEquals(
49+
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
50+
(string) $model->datetimeField,
51+
);
52+
}
53+
}

tests/Casts/DecimalTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MongoDB\Laravel\Tests\Casts;
6+
7+
use MongoDB\BSON\Decimal128;
8+
use MongoDB\Laravel\Tests\Models\Casting;
9+
use MongoDB\Laravel\Tests\TestCase;
10+
11+
class DecimalTest extends TestCase
12+
{
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
17+
Casting::truncate();
18+
}
19+
20+
public function testDecimal(): void
21+
{
22+
$model = Casting::query()->create(['decimalNumber' => 100.99]);
23+
24+
self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
25+
self::assertEquals('100.99', $model->decimalNumber);
26+
27+
$model->update(['decimalNumber' => 9999.9]);
28+
29+
self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
30+
self::assertEquals('9999.90', $model->decimalNumber);
31+
}
32+
33+
public function testDecimalAsString(): void
34+
{
35+
$model = Casting::query()->create(['decimalNumber' => '120.79']);
36+
37+
self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
38+
self::assertEquals('120.79', $model->decimalNumber);
39+
40+
$model->update(['decimalNumber' => '795']);
41+
42+
self::assertInstanceOf(Decimal128::class, $model->decimalNumber);
43+
self::assertEquals('795.00', $model->decimalNumber);
44+
}
45+
}

0 commit comments

Comments
 (0)