From 38b58b8dc38c237aeb676d42e63469a41306500b Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 13:39:42 +0000 Subject: [PATCH 01/12] Add Encryption cast test --- tests/Casts/EncryptionTest.php | 29 +++++++++++++++++++++++++++++ tests/Models/Casting.php | 2 ++ 2 files changed, 31 insertions(+) create mode 100644 tests/Casts/EncryptionTest.php diff --git a/tests/Casts/EncryptionTest.php b/tests/Casts/EncryptionTest.php new file mode 100644 index 000000000..6aff994fc --- /dev/null +++ b/tests/Casts/EncryptionTest.php @@ -0,0 +1,29 @@ +create(['encryptedString' => 'encrypted']); + + self::assertIsString($model->encryptedString); + self::assertEquals('encrypted', $model->encryptedString); + self::assertNotEquals('encrypted', $model->getRawOriginal('encryptedString')); + self::assertEquals('encrypted', app()->make(Encrypter::class)->decryptString($model->getRawOriginal('encryptedString'))); + } +} diff --git a/tests/Models/Casting.php b/tests/Models/Casting.php index 9e232cf15..9ec5a6c8c 100644 --- a/tests/Models/Casting.php +++ b/tests/Models/Casting.php @@ -32,6 +32,7 @@ class Casting extends Eloquent 'datetimeWithFormatField', 'immutableDatetimeField', 'immutableDatetimeWithFormatField', + 'encryptedString', ]; protected $casts = [ @@ -52,5 +53,6 @@ class Casting extends Eloquent 'datetimeWithFormatField' => 'datetime:j.n.Y H:i', 'immutableDatetimeField' => 'immutable_datetime', 'immutableDatetimeWithFormatField' => 'immutable_datetime:j.n.Y H:i', + 'encryptedString' => 'encrypted', ]; } From 4d12cba619941b21b40d2911f6262c0903aceca2 Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 13:57:49 +0000 Subject: [PATCH 02/12] Add Encryption casts tests for array, object, collection --- tests/Casts/EncryptionTest.php | 72 +++++++++++++++++++++++++++++++++- tests/Models/Casting.php | 6 +++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/tests/Casts/EncryptionTest.php b/tests/Casts/EncryptionTest.php index 6aff994fc..a82a50639 100644 --- a/tests/Casts/EncryptionTest.php +++ b/tests/Casts/EncryptionTest.php @@ -4,7 +4,9 @@ namespace Casts; +use Illuminate\Database\Eloquent\Casts\Json; use Illuminate\Encryption\Encrypter; +use Illuminate\Support\Collection; use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; @@ -17,6 +19,14 @@ protected function setUp(): void Casting::truncate(); } + protected function decryptRaw(Casting $model, $key) + { + return app()->make(Encrypter::class) + ->decryptString( + $model->getRawOriginal($key) + ); + } + public function testEncryptedString(): void { $model = Casting::query()->create(['encryptedString' => 'encrypted']); @@ -24,6 +34,66 @@ public function testEncryptedString(): void self::assertIsString($model->encryptedString); self::assertEquals('encrypted', $model->encryptedString); self::assertNotEquals('encrypted', $model->getRawOriginal('encryptedString')); - self::assertEquals('encrypted', app()->make(Encrypter::class)->decryptString($model->getRawOriginal('encryptedString'))); + self::assertEquals('encrypted', $this->decryptRaw($model, 'encryptedString')); + + $model->update(['encryptedString' => 'updated']); + self::assertIsString($model->encryptedString); + self::assertEquals('updated', $model->encryptedString); + self::assertNotEquals('updated', $model->getRawOriginal('encryptedString')); + self::assertEquals('updated', $this->decryptRaw($model, 'encryptedString')); + } + + public function testEncryptedArray(): void + { + $expected = ['foo' => 'bar']; + $model = Casting::query()->create(['encryptedArray' => $expected]); + + self::assertIsArray($model->encryptedArray); + self::assertEquals($expected, $model->encryptedArray); + self::assertNotEquals($expected, $model->getRawOriginal('encryptedArray')); + self::assertEquals($expected, Json::decode($this->decryptRaw($model, 'encryptedArray'))); + + $updated = ['updated' => 'array']; + $model->update(['encryptedArray' => $updated]); + self::assertIsArray($model->encryptedArray); + self::assertEquals($updated, $model->encryptedArray); + self::assertNotEquals($updated, $model->getRawOriginal('encryptedArray')); + self::assertEquals($updated, Json::decode($this->decryptRaw($model, 'encryptedArray'))); + } + + public function testEncryptedObject(): void + { + $expected = (object) ['foo' => 'bar']; + $model = Casting::query()->create(['encryptedObject' => $expected]); + + self::assertIsObject($model->encryptedObject); + self::assertEquals($expected, $model->encryptedObject); + self::assertNotEquals($expected, $model->getRawOriginal('encryptedObject')); + self::assertEquals($expected, Json::decode($this->decryptRaw($model, 'encryptedObject'), false)); + + $updated = (object) ['updated' => 'object']; + $model->update(['encryptedObject' => $updated]); + self::assertIsObject($model->encryptedObject); + self::assertEquals($updated, $model->encryptedObject); + self::assertNotEquals($updated, $model->getRawOriginal('encryptedObject')); + self::assertEquals($updated, Json::decode($this->decryptRaw($model, 'encryptedObject'), false)); + } + + public function testEncryptedCollection(): void + { + $expected = collect(['foo' => 'bar']); + $model = Casting::query()->create(['encryptedCollection' => $expected]); + + self::assertInstanceOf(Collection::class, $model->encryptedCollection); + self::assertEquals($expected, $model->encryptedCollection); + self::assertNotEquals($expected, $model->getRawOriginal('encryptedCollection')); + self::assertEquals($expected, collect(Json::decode($this->decryptRaw($model, 'encryptedCollection'), false))); + + $updated = collect(['updated' => 'object']); + $model->update(['encryptedCollection' => $updated]); + self::assertIsObject($model->encryptedCollection); + self::assertEquals($updated, $model->encryptedCollection); + self::assertNotEquals($updated, $model->getRawOriginal('encryptedCollection')); + self::assertEquals($updated, collect(Json::decode($this->decryptRaw($model, 'encryptedCollection'), false))); } } diff --git a/tests/Models/Casting.php b/tests/Models/Casting.php index 9ec5a6c8c..c67d43117 100644 --- a/tests/Models/Casting.php +++ b/tests/Models/Casting.php @@ -33,6 +33,9 @@ class Casting extends Eloquent 'immutableDatetimeField', 'immutableDatetimeWithFormatField', 'encryptedString', + 'encryptedArray', + 'encryptedObject', + 'encryptedCollection' ]; protected $casts = [ @@ -54,5 +57,8 @@ class Casting extends Eloquent 'immutableDatetimeField' => 'immutable_datetime', 'immutableDatetimeWithFormatField' => 'immutable_datetime:j.n.Y H:i', 'encryptedString' => 'encrypted', + 'encryptedArray' => 'encrypted:array', + 'encryptedObject' => 'encrypted:object', + 'encryptedCollection' => 'encrypted:collection', ]; } From 36dc1a850ce8f2d773c6b5435e3806029741346b Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 22:44:58 +0000 Subject: [PATCH 03/12] Clean up asDateTime method on model This will not change how it functions, just makes it simpler --- src/Eloquent/Model.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index bcb672a3c..1e788a039 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -25,7 +25,6 @@ use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Query\Builder as QueryBuilder; -use function abs; use function array_key_exists; use function array_keys; use function array_merge; @@ -41,7 +40,6 @@ use function is_string; use function ltrim; use function method_exists; -use function sprintf; use function str_contains; use function str_starts_with; use function strcmp; @@ -139,15 +137,9 @@ public function fromDateTime($value) /** @inheritdoc */ protected function asDateTime($value) { - // Convert UTCDateTime instances. + // Convert UTCDateTime instances to Carbon. if ($value instanceof UTCDateTime) { - $date = $value->toDateTime(); - - $seconds = $date->format('U'); - $milliseconds = abs((int) $date->format('v')); - $timestampMs = sprintf('%d%03d', $seconds, $milliseconds); - - return Date::createFromTimestampMs($timestampMs); + return Date::instance($value->toDateTime()); } return parent::asDateTime($value); From e2d293180694ca7690311a63ee21acf2d13abc71 Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 22:52:59 +0000 Subject: [PATCH 04/12] Removed janky fromJson override --- src/Eloquent/Model.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 1e788a039..98615d2fd 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -12,7 +12,6 @@ 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; @@ -285,16 +284,6 @@ protected function asDecimal($value, $decimals) } } - /** @inheritdoc */ - public function fromJson($value, $asObject = false) - { - if (! is_string($value)) { - $value = Json::encode($value); - } - - return Json::decode($value, ! $asObject); - } - /** @inheritdoc */ protected function castAttribute($key, $value) { From 3d11dd24a59969beae291987db71abfd52762c7b Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 22:57:55 +0000 Subject: [PATCH 05/12] Reduce asDecimal override by using parent::asDecimal --- src/Eloquent/Model.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 98615d2fd..745051551 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -4,9 +4,6 @@ namespace MongoDB\Laravel\Eloquent; -use Brick\Math\BigDecimal; -use Brick\Math\Exception\MathException as BrickMathException; -use Brick\Math\RoundingMode; use Carbon\CarbonInterface; use DateTimeInterface; use Illuminate\Contracts\Queue\QueueableCollection; @@ -15,7 +12,6 @@ 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; @@ -275,13 +271,12 @@ public function 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); + if ($value instanceof Decimal128) { + // Convert it to a string to round, want to make it act exactly like we expect. + $value = (string) $value; } + + return parent::asDecimal($value, $decimals); } /** @inheritdoc */ From b76f5754548885347f3407e019e6da7fd7bcc94d Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 22:59:53 +0000 Subject: [PATCH 06/12] Cast to native mongo Decimal128 --- src/Eloquent/Model.php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 745051551..e40542b31 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -237,9 +237,16 @@ public function setAttribute($key, $value) { $key = (string) $key; - // Add casts - if ($this->hasCast($key)) { - $value = $this->castAttribute($key, $value); + $casts = $this->getCasts(); + if (array_key_exists($key, $casts)) { + $castType = $this->getCastType($key); + $castOptions = Str::after($casts[$key], ':'); + + // Can add more native mongo type casts here. + $value = match (true) { + $castType === 'decimal' => $this->fromDecimal($value, $castOptions), + default => $value, + }; } // Convert _id to ObjectID. @@ -279,6 +286,19 @@ protected function asDecimal($value, $decimals) return parent::asDecimal($value, $decimals); } + /** + * Change to mongo native for decimal cast. + * + * @param mixed $value + * @param int $decimals + * + * @return Decimal128 + */ + protected function fromDecimal($value, $decimals) + { + return new Decimal128($this->asDecimal($value, $decimals)); + } + /** @inheritdoc */ protected function castAttribute($key, $value) { From 5a7f718c169de35f958b00e61f842aee64ad55f8 Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 23:02:04 +0000 Subject: [PATCH 07/12] Add more cast tests --- tests/Casts/BooleanTest.php | 37 ++++++++++++++++++++++++++++++++ tests/Casts/CollectionTest.php | 8 +++++++ tests/Casts/DateTest.php | 10 +++++++++ tests/Casts/DatetimeTest.php | 6 ++++++ tests/Casts/DecimalTest.php | 39 ++++++++++++++++++++++++++++++---- tests/Casts/IntegerTest.php | 18 ++++++++++++++++ tests/Casts/JsonTest.php | 10 ++++++++- tests/Casts/ObjectTest.php | 2 ++ tests/Casts/StringTest.php | 6 ++++++ 9 files changed, 131 insertions(+), 5 deletions(-) diff --git a/tests/Casts/BooleanTest.php b/tests/Casts/BooleanTest.php index 8be2a4def..a4812ddec 100644 --- a/tests/Casts/BooleanTest.php +++ b/tests/Casts/BooleanTest.php @@ -50,5 +50,42 @@ public function testBoolAsString(): void self::assertIsBool($model->booleanValue); self::assertSame(false, $model->booleanValue); + + $model->update(['booleanValue' => 'false']); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => '0.0']); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => 'true']); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + } + + public function testBoolAsNumber(): void + { + $model = Casting::query()->create(['booleanValue' => 1]); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => 0]); + + self::assertIsBool($model->booleanValue); + self::assertSame(false, $model->booleanValue); + + $model->update(['booleanValue' => 1.79]); + + self::assertIsBool($model->booleanValue); + self::assertSame(true, $model->booleanValue); + + $model->update(['booleanValue' => 0.0]); + self::assertIsBool($model->booleanValue); + self::assertSame(false, $model->booleanValue); } } diff --git a/tests/Casts/CollectionTest.php b/tests/Casts/CollectionTest.php index 67498c092..4c2400ecb 100644 --- a/tests/Casts/CollectionTest.php +++ b/tests/Casts/CollectionTest.php @@ -24,11 +24,19 @@ public function testCollection(): void $model = Casting::query()->create(['collectionValue' => ['g' => 'G-Eazy']]); self::assertInstanceOf(Collection::class, $model->collectionValue); + self::assertIsString($model->getRawOriginal('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::assertIsString($model->getRawOriginal('collectionValue')); self::assertEquals(collect(['Dont let me go' => 'Even the longest of nights turn days']), $model->collectionValue); + + $model->update(['collectionValue' => [['Dont let me go' => 'Even the longest of nights turn days']]]); + + self::assertInstanceOf(Collection::class, $model->collectionValue); + self::assertIsString($model->getRawOriginal('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 index bd4b76424..20ce5dd9a 100644 --- a/tests/Casts/DateTest.php +++ b/tests/Casts/DateTest.php @@ -7,6 +7,7 @@ use Carbon\CarbonImmutable; use DateTime; use Illuminate\Support\Carbon; +use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; @@ -31,17 +32,26 @@ public function testDate(): void $model->update(['dateField' => now()->subDay()]); self::assertInstanceOf(Carbon::class, $model->dateField); + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('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::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('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::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField')); self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->dateField); + + $refetchedModel = Casting::query()->find($model->getKey()); + + self::assertInstanceOf(Carbon::class, $refetchedModel->dateField); + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('dateField')); + self::assertEquals(now()->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $refetchedModel->dateField); } public function testDateAsString(): void diff --git a/tests/Casts/DatetimeTest.php b/tests/Casts/DatetimeTest.php index dc2bdd877..022ed3535 100644 --- a/tests/Casts/DatetimeTest.php +++ b/tests/Casts/DatetimeTest.php @@ -7,6 +7,7 @@ use Carbon\CarbonImmutable; use DateTime; use Illuminate\Support\Carbon; +use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; @@ -27,11 +28,13 @@ public function testDatetime(): void $model = Casting::query()->create(['datetimeField' => now()]); self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('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::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField')); self::assertEquals(now()->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField); } @@ -40,6 +43,7 @@ public function testDatetimeAsString(): void $model = Casting::query()->create(['datetimeField' => '2023-10-29']); self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField')); self::assertEquals( Carbon::createFromTimestamp(1698577443)->startOfDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField, @@ -48,6 +52,7 @@ public function testDatetimeAsString(): void $model->update(['datetimeField' => '2023-10-28 11:04:03']); self::assertInstanceOf(Carbon::class, $model->datetimeField); + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('datetimeField')); self::assertEquals( Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'), (string) $model->datetimeField, @@ -82,6 +87,7 @@ public function testImmutableDatetime(): void $model->update(['immutableDatetimeField' => '2023-10-28 11:04:03']); self::assertInstanceOf(CarbonImmutable::class, $model->immutableDatetimeField); + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('immutableDatetimeField')); self::assertEquals( Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'), (string) $model->immutableDatetimeField, diff --git a/tests/Casts/DecimalTest.php b/tests/Casts/DecimalTest.php index 535328fe4..0c5500fef 100644 --- a/tests/Casts/DecimalTest.php +++ b/tests/Casts/DecimalTest.php @@ -21,25 +21,56 @@ public function testDecimal(): void { $model = Casting::query()->create(['decimalNumber' => 100.99]); - self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); self::assertEquals('100.99', $model->decimalNumber); $model->update(['decimalNumber' => 9999.9]); - self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); self::assertEquals('9999.90', $model->decimalNumber); + + $model->update(['decimalNumber' => 9999.00000009]); + + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); + self::assertEquals('9999.00', $model->decimalNumber); } public function testDecimalAsString(): void { $model = Casting::query()->create(['decimalNumber' => '120.79']); - self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); self::assertEquals('120.79', $model->decimalNumber); $model->update(['decimalNumber' => '795']); - self::assertInstanceOf(Decimal128::class, $model->decimalNumber); + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); self::assertEquals('795.00', $model->decimalNumber); + + $model->update(['decimalNumber' => '1234.99999999999']); + + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); + self::assertEquals('1235.00', $model->decimalNumber); + } + + public function testDecimalAsDecimal128(): void + { + $model = Casting::query()->create(['decimalNumber' => new Decimal128('100.99')]); + + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); + self::assertEquals('100.99', $model->decimalNumber); + + $model->update(['decimalNumber' => new Decimal128('9999.9')]); + + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); + self::assertEquals('9999.90', $model->decimalNumber); } } diff --git a/tests/Casts/IntegerTest.php b/tests/Casts/IntegerTest.php index f1a11dba5..99cb0cd14 100644 --- a/tests/Casts/IntegerTest.php +++ b/tests/Casts/IntegerTest.php @@ -51,4 +51,22 @@ public function testIntAsString(): void self::assertIsInt($model->intNumber); self::assertEquals(9, $model->intNumber); } + + public function testIntAsFloat(): void + { + $model = Casting::query()->create(['intNumber' => 1.0]); + + self::assertIsInt($model->intNumber); + self::assertEquals(1, $model->intNumber); + + $model->update(['intNumber' => 2.0]); + + 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 index 99473c5d8..2b8759dd6 100644 --- a/tests/Casts/JsonTest.php +++ b/tests/Casts/JsonTest.php @@ -25,9 +25,17 @@ public function testJson(): void 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'])]); + $model->update(['jsonValue' => ['Dont let me go' => 'Even the longest of nights turn days']]); self::assertIsArray($model->jsonValue); + self::assertIsString($model->getRawOriginal('jsonValue')); self::assertEquals(['Dont let me go' => 'Even the longest of nights turn days'], $model->jsonValue); + + $json = json_encode(['it will encode json' => 'even if it is already json']); + $model->update(['jsonValue' => $json]); + + self::assertIsString($model->jsonValue); + self::assertIsString($model->getRawOriginal('jsonValue')); + self::assertEquals($json, $model->jsonValue); } } diff --git a/tests/Casts/ObjectTest.php b/tests/Casts/ObjectTest.php index 3217b23fc..e45b736e0 100644 --- a/tests/Casts/ObjectTest.php +++ b/tests/Casts/ObjectTest.php @@ -21,11 +21,13 @@ public function testObject(): void $model = Casting::query()->create(['objectValue' => ['g' => 'G-Eazy']]); self::assertIsObject($model->objectValue); + self::assertIsString($model->getRawOriginal('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::assertIsString($model->getRawOriginal('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 index 120fb9b19..11067222c 100644 --- a/tests/Casts/StringTest.php +++ b/tests/Casts/StringTest.php @@ -27,5 +27,11 @@ public function testString(): void self::assertIsString($model->stringContent); self::assertEquals("Losing hope, don't mean I'm hopeless And maybe all I need is time", $model->stringContent); + + $now = now(); + $model->update(['stringContent' => now()]); + + self::assertIsString($model->stringContent); + self::assertEquals((string) $now, $model->stringContent); } } From 9acd088dbbb52ffd810c6a9fbf7d507be6248946 Mon Sep 17 00:00:00 2001 From: stubbo Date: Tue, 9 Jan 2024 23:04:20 +0000 Subject: [PATCH 08/12] apply phpcbf formatting --- tests/Casts/EncryptionTest.php | 5 ++++- tests/Casts/StringTest.php | 2 ++ tests/Models/Casting.php | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Casts/EncryptionTest.php b/tests/Casts/EncryptionTest.php index a82a50639..0c40254f1 100644 --- a/tests/Casts/EncryptionTest.php +++ b/tests/Casts/EncryptionTest.php @@ -10,6 +10,9 @@ use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; +use function app; +use function collect; + class EncryptionTest extends TestCase { protected function setUp(): void @@ -23,7 +26,7 @@ protected function decryptRaw(Casting $model, $key) { return app()->make(Encrypter::class) ->decryptString( - $model->getRawOriginal($key) + $model->getRawOriginal($key), ); } diff --git a/tests/Casts/StringTest.php b/tests/Casts/StringTest.php index 11067222c..755bfd879 100644 --- a/tests/Casts/StringTest.php +++ b/tests/Casts/StringTest.php @@ -7,6 +7,8 @@ use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; +use function now; + class StringTest extends TestCase { protected function setUp(): void diff --git a/tests/Models/Casting.php b/tests/Models/Casting.php index c67d43117..f44f08a62 100644 --- a/tests/Models/Casting.php +++ b/tests/Models/Casting.php @@ -35,7 +35,7 @@ class Casting extends Eloquent 'encryptedString', 'encryptedArray', 'encryptedObject', - 'encryptedCollection' + 'encryptedCollection', ]; protected $casts = [ From 2a9d836cb712ace34b3d8731610341da4ec8458f Mon Sep 17 00:00:00 2001 From: George Stubbs Date: Wed, 10 Jan 2024 13:43:02 +0000 Subject: [PATCH 09/12] Cleanup casting in Model::setAttribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérôme Tamarelle --- src/Eloquent/Model.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index e40542b31..69ccb84dd 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -243,9 +243,9 @@ public function setAttribute($key, $value) $castOptions = Str::after($casts[$key], ':'); // Can add more native mongo type casts here. - $value = match (true) { - $castType === 'decimal' => $this->fromDecimal($value, $castOptions), - default => $value, + $value = match ($castType) { + 'decimal' => $this->fromDecimal($value, $castOptions), + default => $value, }; } From c55b2afef02905737a1b9f2a63042a234a6d21b3 Mon Sep 17 00:00:00 2001 From: George Stubbs Date: Wed, 10 Jan 2024 13:43:28 +0000 Subject: [PATCH 10/12] Use $now insead of a now() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérôme Tamarelle --- tests/Casts/StringTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Casts/StringTest.php b/tests/Casts/StringTest.php index 755bfd879..67ed7227d 100644 --- a/tests/Casts/StringTest.php +++ b/tests/Casts/StringTest.php @@ -31,7 +31,7 @@ public function testString(): void self::assertEquals("Losing hope, don't mean I'm hopeless And maybe all I need is time", $model->stringContent); $now = now(); - $model->update(['stringContent' => now()]); + $model->update(['stringContent' => $now]); self::assertIsString($model->stringContent); self::assertEquals((string) $now, $model->stringContent); From b0fdb1f3e75a00afbb53ea2797cd66ee0a925279 Mon Sep 17 00:00:00 2001 From: stubbo Date: Thu, 11 Jan 2024 17:15:16 +0000 Subject: [PATCH 11/12] Improve casting from BSON to decimal --- src/Eloquent/Model.php | 33 +++++++++++++++++++--- tests/Casts/DecimalTest.php | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 69ccb84dd..8928c78e1 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -12,13 +12,16 @@ 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\Type; use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Query\Builder as QueryBuilder; +use Stringable; use function array_key_exists; use function array_keys; @@ -275,12 +278,22 @@ public function setAttribute($key, $value) return parent::setAttribute($key, $value); } - /** @inheritdoc */ + /** + * @param mixed $value + * + * @inheritdoc + */ protected function asDecimal($value, $decimals) { - if ($value instanceof Decimal128) { - // Convert it to a string to round, want to make it act exactly like we expect. - $value = (string) $value; + // Convert BSON to string. + if ($this->isBSON($value)) { + if ($value instanceof Binary) { + $value = $value->getData(); + } elseif ($value instanceof Stringable) { + $value = (string) $value; + } else { + throw new MathException('BSON type ' . $value::class . ' cannot be converted to string'); + } } return parent::asDecimal($value, $decimals); @@ -703,4 +716,16 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt return $attributes; } + + /** + * Is a value a BSON type? + * + * @param mixed $value + * + * @return bool + */ + protected function isBSON(mixed $value): bool + { + return $value instanceof Type; + } } diff --git a/tests/Casts/DecimalTest.php b/tests/Casts/DecimalTest.php index 0c5500fef..f69d24d62 100644 --- a/tests/Casts/DecimalTest.php +++ b/tests/Casts/DecimalTest.php @@ -4,10 +4,18 @@ namespace MongoDB\Laravel\Tests\Casts; +use Illuminate\Support\Exceptions\MathException; +use MongoDB\BSON\Binary; use MongoDB\BSON\Decimal128; +use MongoDB\BSON\Int64; +use MongoDB\BSON\Javascript; +use MongoDB\BSON\UTCDateTime; +use MongoDB\Laravel\Collection; use MongoDB\Laravel\Tests\Models\Casting; use MongoDB\Laravel\Tests\TestCase; +use function now; + class DecimalTest extends TestCase { protected function setUp(): void @@ -73,4 +81,51 @@ public function testDecimalAsDecimal128(): void self::assertInstanceOf(Decimal128::class, $model->getRawOriginal('decimalNumber')); self::assertEquals('9999.90', $model->decimalNumber); } + + public function testOtherBSONTypes(): void + { + $modelId = $this->setBSONType(new Int64(100)); + $model = Casting::query()->find($modelId); + + self::assertIsString($model->decimalNumber); + self::assertIsInt($model->getRawOriginal('decimalNumber')); + self::assertEquals('100.00', $model->decimalNumber); + + // Update decimalNumber to a Binary type + $this->setBSONType(new Binary('100.1234', Binary::TYPE_GENERIC), $modelId); + $model->refresh(); + + self::assertIsString($model->decimalNumber); + self::assertInstanceOf(Binary::class, $model->getRawOriginal('decimalNumber')); + self::assertEquals('100.12', $model->decimalNumber); + + $this->setBSONType(new Javascript('function() { return 100; }'), $modelId); + $model->refresh(); + self::expectException(MathException::class); + self::expectExceptionMessage('Unable to cast value to a decimal.'); + $model->decimalNumber; + self::assertInstanceOf(Javascript::class, $model->getRawOriginal('decimalNumber')); + + $this->setBSONType(new UTCDateTime(now()), $modelId); + $model->refresh(); + self::expectException(MathException::class); + self::expectExceptionMessage('Unable to cast value to a decimal.'); + $model->decimalNumber; + self::assertInstanceOf(UTCDateTime::class, $model->getRawOriginal('decimalNumber')); + } + + private function setBSONType($value, $id = null) + { + // Do a raw insert/update, so we can enforce the type we want + return Casting::raw(function (Collection $collection) use ($id, $value) { + if (! empty($id)) { + return $collection->updateOne( + ['_id' => $id], + ['$set' => ['decimalNumber' => $value]], + ); + } + + return $collection->insertOne(['decimalNumber' => $value])->getInsertedId(); + }); + } } From 0dd845a9778c560e8ea84cb2e2d05c6e0beb1a97 Mon Sep 17 00:00:00 2001 From: stubbo Date: Fri, 12 Jan 2024 11:08:43 +0000 Subject: [PATCH 12/12] Fix queue tests which have delay --- tests/QueueTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/QueueTest.php b/tests/QueueTest.php index c23e711ab..2236fba1b 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -27,6 +27,8 @@ public function setUp(): void // Always start with a clean slate Queue::getDatabase()->table(Config::get('queue.connections.database.table'))->truncate(); Queue::getDatabase()->table(Config::get('queue.failed.table'))->truncate(); + + Carbon::setTestNow(Carbon::now()); } public function testQueueJobLifeCycle(): void @@ -147,7 +149,6 @@ public function testQueueDeleteReserved(): void public function testQueueRelease(): void { - Carbon::setTestNow(); $queue = 'test'; $delay = 123; Queue::push($queue, ['action' => 'QueueRelease'], 'test');