Skip to content

Commit a5cf5cb

Browse files
authored
Fix casting issues (issue: #2703) (#2705)
1 parent 634ea50 commit a5cf5cb

13 files changed

+352
-37
lines changed

src/Eloquent/Model.php

+52-31
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,11 @@
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;
107
use Carbon\CarbonInterface;
118
use DateTimeInterface;
129
use Illuminate\Contracts\Queue\QueueableCollection;
1310
use Illuminate\Contracts\Queue\QueueableEntity;
1411
use Illuminate\Contracts\Support\Arrayable;
15-
use Illuminate\Database\Eloquent\Casts\Json;
1612
use Illuminate\Database\Eloquent\Model as BaseModel;
1713
use Illuminate\Database\Eloquent\Relations\Relation;
1814
use Illuminate\Support\Arr;
@@ -22,10 +18,11 @@
2218
use MongoDB\BSON\Binary;
2319
use MongoDB\BSON\Decimal128;
2420
use MongoDB\BSON\ObjectID;
21+
use MongoDB\BSON\Type;
2522
use MongoDB\BSON\UTCDateTime;
2623
use MongoDB\Laravel\Query\Builder as QueryBuilder;
24+
use Stringable;
2725

28-
use function abs;
2926
use function array_key_exists;
3027
use function array_keys;
3128
use function array_merge;
@@ -41,7 +38,6 @@
4138
use function is_string;
4239
use function ltrim;
4340
use function method_exists;
44-
use function sprintf;
4541
use function str_contains;
4642
use function str_starts_with;
4743
use function strcmp;
@@ -139,15 +135,9 @@ public function fromDateTime($value)
139135
/** @inheritdoc */
140136
protected function asDateTime($value)
141137
{
142-
// Convert UTCDateTime instances.
138+
// Convert UTCDateTime instances to Carbon.
143139
if ($value instanceof UTCDateTime) {
144-
$date = $value->toDateTime();
145-
146-
$seconds = $date->format('U');
147-
$milliseconds = abs((int) $date->format('v'));
148-
$timestampMs = sprintf('%d%03d', $seconds, $milliseconds);
149-
150-
return Date::createFromTimestampMs($timestampMs);
140+
return Date::instance($value->toDateTime());
151141
}
152142

153143
return parent::asDateTime($value);
@@ -250,9 +240,16 @@ public function setAttribute($key, $value)
250240
{
251241
$key = (string) $key;
252242

253-
// Add casts
254-
if ($this->hasCast($key)) {
255-
$value = $this->castAttribute($key, $value);
243+
$casts = $this->getCasts();
244+
if (array_key_exists($key, $casts)) {
245+
$castType = $this->getCastType($key);
246+
$castOptions = Str::after($casts[$key], ':');
247+
248+
// Can add more native mongo type casts here.
249+
$value = match ($castType) {
250+
'decimal' => $this->fromDecimal($value, $castOptions),
251+
default => $value,
252+
};
256253
}
257254

258255
// Convert _id to ObjectID.
@@ -281,26 +278,38 @@ public function setAttribute($key, $value)
281278
return parent::setAttribute($key, $value);
282279
}
283280

284-
/** @inheritdoc */
281+
/**
282+
* @param mixed $value
283+
*
284+
* @inheritdoc
285+
*/
285286
protected function asDecimal($value, $decimals)
286287
{
287-
try {
288-
$value = (string) BigDecimal::of((string) $value)->toScale((int) $decimals, RoundingMode::HALF_UP);
289-
290-
return new Decimal128($value);
291-
} catch (BrickMathException $e) {
292-
throw new MathException('Unable to cast value to a decimal.', previous: $e);
288+
// Convert BSON to string.
289+
if ($this->isBSON($value)) {
290+
if ($value instanceof Binary) {
291+
$value = $value->getData();
292+
} elseif ($value instanceof Stringable) {
293+
$value = (string) $value;
294+
} else {
295+
throw new MathException('BSON type ' . $value::class . ' cannot be converted to string');
296+
}
293297
}
298+
299+
return parent::asDecimal($value, $decimals);
294300
}
295301

296-
/** @inheritdoc */
297-
public function fromJson($value, $asObject = false)
302+
/**
303+
* Change to mongo native for decimal cast.
304+
*
305+
* @param mixed $value
306+
* @param int $decimals
307+
*
308+
* @return Decimal128
309+
*/
310+
protected function fromDecimal($value, $decimals)
298311
{
299-
if (! is_string($value)) {
300-
$value = Json::encode($value);
301-
}
302-
303-
return Json::decode($value, ! $asObject);
312+
return new Decimal128($this->asDecimal($value, $decimals));
304313
}
305314

306315
/** @inheritdoc */
@@ -707,4 +716,16 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
707716

708717
return $attributes;
709718
}
719+
720+
/**
721+
* Is a value a BSON type?
722+
*
723+
* @param mixed $value
724+
*
725+
* @return bool
726+
*/
727+
protected function isBSON(mixed $value): bool
728+
{
729+
return $value instanceof Type;
730+
}
710731
}

tests/Casts/BooleanTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,42 @@ public function testBoolAsString(): void
5050

5151
self::assertIsBool($model->booleanValue);
5252
self::assertSame(false, $model->booleanValue);
53+
54+
$model->update(['booleanValue' => 'false']);
55+
56+
self::assertIsBool($model->booleanValue);
57+
self::assertSame(true, $model->booleanValue);
58+
59+
$model->update(['booleanValue' => '0.0']);
60+
61+
self::assertIsBool($model->booleanValue);
62+
self::assertSame(true, $model->booleanValue);
63+
64+
$model->update(['booleanValue' => 'true']);
65+
66+
self::assertIsBool($model->booleanValue);
67+
self::assertSame(true, $model->booleanValue);
68+
}
69+
70+
public function testBoolAsNumber(): void
71+
{
72+
$model = Casting::query()->create(['booleanValue' => 1]);
73+
74+
self::assertIsBool($model->booleanValue);
75+
self::assertSame(true, $model->booleanValue);
76+
77+
$model->update(['booleanValue' => 0]);
78+
79+
self::assertIsBool($model->booleanValue);
80+
self::assertSame(false, $model->booleanValue);
81+
82+
$model->update(['booleanValue' => 1.79]);
83+
84+
self::assertIsBool($model->booleanValue);
85+
self::assertSame(true, $model->booleanValue);
86+
87+
$model->update(['booleanValue' => 0.0]);
88+
self::assertIsBool($model->booleanValue);
89+
self::assertSame(false, $model->booleanValue);
5390
}
5491
}

tests/Casts/CollectionTest.php

+8
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@ public function testCollection(): void
2424
$model = Casting::query()->create(['collectionValue' => ['g' => 'G-Eazy']]);
2525

2626
self::assertInstanceOf(Collection::class, $model->collectionValue);
27+
self::assertIsString($model->getRawOriginal('collectionValue'));
2728
self::assertEquals(collect(['g' => 'G-Eazy']), $model->collectionValue);
2829

2930
$model->update(['collectionValue' => ['Dont let me go' => 'Even the longest of nights turn days']]);
3031

3132
self::assertInstanceOf(Collection::class, $model->collectionValue);
33+
self::assertIsString($model->getRawOriginal('collectionValue'));
3234
self::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue);
35+
36+
$model->update(['collectionValue' => [['Dont let me go' => 'Even the longest of nights turn days']]]);
37+
38+
self::assertInstanceOf(Collection::class, $model->collectionValue);
39+
self::assertIsString($model->getRawOriginal('collectionValue'));
40+
self::assertEquals(collect([['Dont let me go' => 'Even the longest of nights turn days']]), $model->collectionValue);
3341
}
3442
}

tests/Casts/DateTest.php

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Carbon\CarbonImmutable;
88
use DateTime;
99
use Illuminate\Support\Carbon;
10+
use MongoDB\BSON\UTCDateTime;
1011
use MongoDB\Laravel\Tests\Models\Casting;
1112
use MongoDB\Laravel\Tests\TestCase;
1213

@@ -31,17 +32,26 @@ public function testDate(): void
3132
$model->update(['dateField' => now()->subDay()]);
3233

3334
self::assertInstanceOf(Carbon::class, $model->dateField);
35+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
3436
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
3537

3638
$model->update(['dateField' => new DateTime()]);
3739

3840
self::assertInstanceOf(Carbon::class, $model->dateField);
41+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
3942
self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
4043

4144
$model->update(['dateField' => (new DateTime())->modify('-1 day')]);
4245

4346
self::assertInstanceOf(Carbon::class, $model->dateField);
47+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
4448
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField);
49+
50+
$refetchedModel = Casting::query()->find($model->getKey());
51+
52+
self::assertInstanceOf(Carbon::class, $refetchedModel->dateField);
53+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField'));
54+
self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $refetchedModel->dateField);
4555
}
4656

4757
public function testDateAsString(): void

tests/Casts/DatetimeTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Carbon\CarbonImmutable;
88
use DateTime;
99
use Illuminate\Support\Carbon;
10+
use MongoDB\BSON\UTCDateTime;
1011
use MongoDB\Laravel\Tests\Models\Casting;
1112
use MongoDB\Laravel\Tests\TestCase;
1213

@@ -27,11 +28,13 @@ public function testDatetime(): void
2728
$model = Casting::query()->create(['datetimeField' => now()]);
2829

2930
self::assertInstanceOf(Carbon::class, $model->datetimeField);
31+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
3032
self::assertEquals(now()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
3133

3234
$model->update(['datetimeField' => now()->subDay()]);
3335

3436
self::assertInstanceOf(Carbon::class, $model->datetimeField);
37+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
3538
self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField);
3639
}
3740

@@ -40,6 +43,7 @@ public function testDatetimeAsString(): void
4043
$model = Casting::query()->create(['datetimeField' => '2023-10-29']);
4144

4245
self::assertInstanceOf(Carbon::class, $model->datetimeField);
46+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
4347
self::assertEquals(
4448
Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'),
4549
(string) $model->datetimeField,
@@ -48,6 +52,7 @@ public function testDatetimeAsString(): void
4852
$model->update(['datetimeField' => '2023-10-28 11:04:03']);
4953

5054
self::assertInstanceOf(Carbon::class, $model->datetimeField);
55+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField'));
5156
self::assertEquals(
5257
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
5358
(string) $model->datetimeField,
@@ -82,6 +87,7 @@ public function testImmutableDatetime(): void
8287
$model->update(['immutableDatetimeField' => '2023-10-28 11:04:03']);
8388

8489
self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeField);
90+
self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('immutableDatetimeField'));
8591
self::assertEquals(
8692
Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'),
8793
(string) $model->immutableDatetimeField,

0 commit comments

Comments
 (0)