diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 30497ad86..72c4d2a5f 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -4,16 +4,22 @@ namespace MongoDB\Laravel\Eloquent; +use Brick\Math\BigDecimal; +use Brick\Math\Exception\MathException as BrickMathException; +use Brick\Math\RoundingMode; use DateTimeInterface; use Illuminate\Contracts\Queue\QueueableCollection; use Illuminate\Contracts\Queue\QueueableEntity; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Database\Eloquent\Casts\Json; use Illuminate\Database\Eloquent\Model as BaseModel; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Arr; +use Illuminate\Support\Exceptions\MathException; use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; use MongoDB\BSON\Binary; +use MongoDB\BSON\Decimal128; use MongoDB\BSON\ObjectID; use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Query\Builder as QueryBuilder; @@ -211,6 +217,11 @@ public function setAttribute($key, $value) { $key = (string) $key; + //Add casts + if ($this->hasCast($key)) { + $value = $this->castAttribute($key, $value); + } + // Convert _id to ObjectID. if ($key === '_id' && is_string($value)) { $builder = $this->newBaseQueryBuilder(); @@ -237,6 +248,28 @@ public function setAttribute($key, $value) return parent::setAttribute($key, $value); } + /** @inheritdoc */ + protected function asDecimal($value, $decimals) + { + try { + $value = (string) BigDecimal::of((string) $value)->toScale((int) $decimals, RoundingMode::HALF_UP); + + return new Decimal128($value); + } catch (BrickMathException $e) { + throw new MathException('Unable to cast value to a decimal.', previous: $e); + } + } + + /** @inheritdoc */ + public function fromJson($value, $asObject = false) + { + if (! is_string($value)) { + $value = Json::encode($value ?? ''); + } + + return Json::decode($value ?? '', ! $asObject); + } + /** @inheritdoc */ public function attributesToArray() { diff --git a/tests/Casts/BinaryUuidTest.php b/tests/Casts/BinaryUuidTest.php index 8a79b1500..2183c12fa 100644 --- a/tests/Casts/BinaryUuidTest.php +++ b/tests/Casts/BinaryUuidTest.php @@ -6,7 +6,7 @@ use Generator; use MongoDB\BSON\Binary; -use MongoDB\Laravel\Tests\Models\CastBinaryUuid; +use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; use function hex2bin; @@ -17,15 +17,15 @@ protected function setUp(): void { parent::setUp(); - CastBinaryUuid::truncate(); + Casting::truncate(); } /** @dataProvider provideBinaryUuidCast */ public function testBinaryUuidCastModel(string $expectedUuid, string|Binary $saveUuid, Binary $queryUuid): void { - CastBinaryUuid::create(['uuid' => $saveUuid]); + Casting::create(['uuid' => $saveUuid]); - $model = CastBinaryUuid::firstWhere('uuid', $queryUuid); + $model = Casting::firstWhere('uuid', $queryUuid); $this->assertNotNull($model); $this->assertSame($expectedUuid, $model->uuid); } @@ -43,9 +43,9 @@ public function testQueryByStringDoesNotCast(): void { $uuid = '0c103357-3806-48c9-a84b-867dcb625cfb'; - CastBinaryUuid::create(['uuid' => $uuid]); + Casting::create(['uuid' => $uuid]); - $model = CastBinaryUuid::firstWhere('uuid', $uuid); + $model = Casting::firstWhere('uuid', $uuid); $this->assertNull($model); } } diff --git a/tests/Casts/BooleanTest.php b/tests/Casts/BooleanTest.php new file mode 100644 index 000000000..8be2a4def --- /dev/null +++ b/tests/Casts/BooleanTest.php @@ -0,0 +1,54 @@ +create(['booleanValue' => true]); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => false]); + + self::assertIsBool($model->booleanValue); + self::assertSame(false, $model->booleanValue); + + $model->update(['booleanValue' => 1]); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => 0]); + + self::assertIsBool($model->booleanValue); + self::assertSame(false, $model->booleanValue); + } + + public function testBoolAsString(): void + { + $model = Casting::query()->create(['booleanValue' => '1.79']); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => '0']); + + self::assertIsBool($model->booleanValue); + self::assertSame(false, $model->booleanValue); + } +} diff --git a/tests/Casts/CollectionTest.php b/tests/Casts/CollectionTest.php new file mode 100644 index 000000000..67498c092 --- /dev/null +++ b/tests/Casts/CollectionTest.php @@ -0,0 +1,34 @@ +create(['collectionValue' => ['g' => 'G-Eazy']]); + + self::assertInstanceOf(Collection::class, $model->collectionValue); + self::assertEquals(collect(['g' => 'G-Eazy']), $model->collectionValue); + + $model->update(['collectionValue' => ['Dont let me go' => 'Even the longest of nights turn days']]); + + self::assertInstanceOf(Collection::class, $model->collectionValue); + self::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue); + } +} diff --git a/tests/Casts/DateTest.php b/tests/Casts/DateTest.php new file mode 100644 index 000000000..e0c775503 --- /dev/null +++ b/tests/Casts/DateTest.php @@ -0,0 +1,64 @@ +create(['dateField' => now()]); + + self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField); + + $model->update(['dateField' => now()->subDay()]); + + self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField); + + $model->update(['dateField' => new DateTime()]); + + self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertEquals(now()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField); + + $model->update(['dateField' => (new DateTime())->modify('-1 day')]); + + self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField); + } + + public function testDateAsString(): void + { + $model = Casting::query()->create(['dateField' => '2023-10-29']); + + self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertEquals( + Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'), + (string) $model->dateField, + ); + + $model->update(['dateField' => '2023-10-28']); + + self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertEquals( + Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('Y-m-d H:i:s'), + (string) $model->dateField, + ); + } +} diff --git a/tests/Casts/DatetimeTest.php b/tests/Casts/DatetimeTest.php new file mode 100644 index 000000000..77a9cb4b6 --- /dev/null +++ b/tests/Casts/DatetimeTest.php @@ -0,0 +1,53 @@ +create(['datetimeField' => now()]); + + self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertEquals(now()->format('Y-m-d H:i:s'), (string) $model->datetimeField); + + $model->update(['datetimeField' => now()->subDay()]); + + self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField); + } + + public function testDateAsString(): void + { + $model = Casting::query()->create(['datetimeField' => '2023-10-29']); + + self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertEquals( + Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'), + (string) $model->datetimeField, + ); + + $model->update(['datetimeField' => '2023-10-28 11:04:03']); + + self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertEquals( + Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'), + (string) $model->datetimeField, + ); + } +} diff --git a/tests/Casts/DecimalTest.php b/tests/Casts/DecimalTest.php new file mode 100644 index 000000000..535328fe4 --- /dev/null +++ b/tests/Casts/DecimalTest.php @@ -0,0 +1,45 @@ +create(['decimalNumber' => 100.99]); + + self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertEquals('100.99', $model->decimalNumber); + + $model->update(['decimalNumber' => 9999.9]); + + self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertEquals('9999.90', $model->decimalNumber); + } + + public function testDecimalAsString(): void + { + $model = Casting::query()->create(['decimalNumber' => '120.79']); + + self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertEquals('120.79', $model->decimalNumber); + + $model->update(['decimalNumber' => '795']); + + self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertEquals('795.00', $model->decimalNumber); + } +} diff --git a/tests/Casts/FloatTest.php b/tests/Casts/FloatTest.php new file mode 100644 index 000000000..e4d90cae9 --- /dev/null +++ b/tests/Casts/FloatTest.php @@ -0,0 +1,44 @@ +create(['floatNumber' => 1.79]); + + self::assertIsFloat($model->floatNumber); + self::assertEquals(1.79, $model->floatNumber); + + $model->update(['floatNumber' => 7E-5]); + + self::assertIsFloat($model->floatNumber); + self::assertEquals(7E-5, $model->floatNumber); + } + + public function testFloatAsString(): void + { + $model = Casting::query()->create(['floatNumber' => '1.79']); + + self::assertIsFloat($model->floatNumber); + self::assertEquals(1.79, $model->floatNumber); + + $model->update(['floatNumber' => '7E-5']); + + self::assertIsFloat($model->floatNumber); + self::assertEquals(7E-5, $model->floatNumber); + } +} diff --git a/tests/Casts/IntegerTest.php b/tests/Casts/IntegerTest.php new file mode 100644 index 000000000..f1a11dba5 --- /dev/null +++ b/tests/Casts/IntegerTest.php @@ -0,0 +1,54 @@ +create(['intNumber' => 1]); + + self::assertIsInt($model->intNumber); + self::assertEquals(1, $model->intNumber); + + $model->update(['intNumber' => 2]); + + self::assertIsInt($model->intNumber); + self::assertEquals(2, $model->intNumber); + + $model->update(['intNumber' => 9.6]); + + self::assertIsInt($model->intNumber); + self::assertEquals(9, $model->intNumber); + } + + public function testIntAsString(): void + { + $model = Casting::query()->create(['intNumber' => '1']); + + self::assertIsInt($model->intNumber); + self::assertEquals(1, $model->intNumber); + + $model->update(['intNumber' => '2']); + + self::assertIsInt($model->intNumber); + self::assertEquals(2, $model->intNumber); + + $model->update(['intNumber' => '9.6']); + + self::assertIsInt($model->intNumber); + self::assertEquals(9, $model->intNumber); + } +} diff --git a/tests/Casts/JsonTest.php b/tests/Casts/JsonTest.php new file mode 100644 index 000000000..99473c5d8 --- /dev/null +++ b/tests/Casts/JsonTest.php @@ -0,0 +1,33 @@ +create(['jsonValue' => ['g' => 'G-Eazy']]); + + self::assertIsArray($model->jsonValue); + self::assertEquals(['g' => 'G-Eazy'], $model->jsonValue); + + $model->update(['jsonValue' => json_encode(['Dont let me go' => 'Even the longest of nights turn days'])]); + + self::assertIsArray($model->jsonValue); + self::assertEquals(['Dont let me go' => 'Even the longest of nights turn days'], $model->jsonValue); + } +} diff --git a/tests/Casts/ObjectTest.php b/tests/Casts/ObjectTest.php new file mode 100644 index 000000000..3217b23fc --- /dev/null +++ b/tests/Casts/ObjectTest.php @@ -0,0 +1,31 @@ +create(['objectValue' => ['g' => 'G-Eazy']]); + + self::assertIsObject($model->objectValue); + self::assertEquals((object) ['g' => 'G-Eazy'], $model->objectValue); + + $model->update(['objectValue' => ['Dont let me go' => 'Even the brightest of colors turn greys']]); + + self::assertIsObject($model->objectValue); + self::assertEquals((object) ['Dont let me go' => 'Even the brightest of colors turn greys'], $model->objectValue); + } +} diff --git a/tests/Casts/StringTest.php b/tests/Casts/StringTest.php new file mode 100644 index 000000000..120fb9b19 --- /dev/null +++ b/tests/Casts/StringTest.php @@ -0,0 +1,31 @@ +create(['stringContent' => 'Home is behind The world ahead And there are many paths to tread']); + + self::assertIsString($model->stringContent); + self::assertEquals('Home is behind The world ahead And there are many paths to tread', $model->stringContent); + + $model->update(['stringContent' => "Losing hope, don't mean I'm hopeless And maybe all I need is time"]); + + self::assertIsString($model->stringContent); + self::assertEquals("Losing hope, don't mean I'm hopeless And maybe all I need is time", $model->stringContent); + } +} diff --git a/tests/Models/CastBinaryUuid.php b/tests/Models/CastBinaryUuid.php deleted file mode 100644 index 3d8b82941..000000000 --- a/tests/Models/CastBinaryUuid.php +++ /dev/null @@ -1,17 +0,0 @@ - BinaryUuid::class, - ]; -} diff --git a/tests/Models/Casting.php b/tests/Models/Casting.php new file mode 100644 index 000000000..5f825f954 --- /dev/null +++ b/tests/Models/Casting.php @@ -0,0 +1,43 @@ + BinaryUuid::class, + 'intNumber' => 'int', + 'floatNumber' => 'float', + 'decimalNumber' => 'decimal:2', + 'stringContent' => 'string', + 'booleanValue' => 'boolean', + 'objectValue' => 'object', + 'jsonValue' => 'json', + 'collectionValue' => 'collection', + 'dateField' => 'date', + 'datetimeField' => 'datetime', + ]; +}