From c6147c954803508ff74107de77635ad0e3e47676 Mon Sep 17 00:00:00 2001 From: Guram Vashakidze Date: Thu, 24 Jul 2025 18:39:22 +0300 Subject: [PATCH 1/6] Fix overwriting `updated_at` when `$set` is used --- src/Eloquent/Builder.php | 4 ++++ tests/ModelTest.php | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index f3ffd7012..1b13de314 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -310,6 +310,10 @@ protected function addUpdatedAtColumn(array $values) } $column = $this->model->getUpdatedAtColumn(); + if (isset($values['$set'][$column])) { + return $values; + } + $values = array_replace( [$column => $this->model->freshTimestampString()], $values, diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 88bd27e44..8fcd0b272 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -169,6 +169,21 @@ public function testUpdate(): void $this->assertEquals('Hans Thomas', $check->fullname); } + public function testUpdateTroughSetUpdatedAt(): void + { + $user = new User(); + $user->name = 'John Doe'; + $user->title = 'admin'; + $user->age = 35; + $user->save(); + + $updatedAt = Carbon::yesterday(); + User::query()->update(['$set' => ['updated_at' => new UTCDateTime($updatedAt)]]); + + $user->refresh(); + $this->assertEquals($updatedAt, $user->updated_at); + } + public function testUpsert() { $result = User::upsert([ From f0270891b561eeb0a2542a23fb0ac32b2ecea95c Mon Sep 17 00:00:00 2001 From: Guram Vashakidze Date: Thu, 7 Aug 2025 17:40:21 +0300 Subject: [PATCH 2/6] fix incorrect behaviour when comparing castable attributes in `originalIsEquivalent` method --- src/Eloquent/DocumentModel.php | 12 ++++++++++ tests/ModelTest.php | 11 ++++++++++ tests/Models/Options.php | 37 +++++++++++++++++++++++++++++++ tests/Models/OptionsCast.php | 40 ++++++++++++++++++++++++++++++++++ tests/Models/User.php | 2 ++ 5 files changed, 102 insertions(+) create mode 100644 tests/Models/Options.php create mode 100755 tests/Models/OptionsCast.php diff --git a/src/Eloquent/DocumentModel.php b/src/Eloquent/DocumentModel.php index f8d399e62..7d7ef6674 100644 --- a/src/Eloquent/DocumentModel.php +++ b/src/Eloquent/DocumentModel.php @@ -43,6 +43,7 @@ use function is_string; use function ltrim; use function method_exists; +use function serialize; use function sprintf; use function str_contains; use function str_starts_with; @@ -377,6 +378,17 @@ public function originalIsEquivalent($key) $this->castAttribute($key, $original); } + if ($this->isClassCastable($key)) { + $attribute = $this->castAttribute($key, $attribute); + $original = $this->castAttribute($key, $original); + + if ($attribute === $original) { + return true; + } + + return serialize($attribute) === serialize($original); + } + return is_numeric($attribute) && is_numeric($original) && strcmp((string) $attribute, (string) $original) === 0; } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 8fcd0b272..9cefd8049 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -26,6 +26,7 @@ use MongoDB\Laravel\Tests\Models\Item; use MongoDB\Laravel\Tests\Models\MemberStatus; use MongoDB\Laravel\Tests\Models\NonIncrementing; +use MongoDB\Laravel\Tests\Models\Options; use MongoDB\Laravel\Tests\Models\Soft; use MongoDB\Laravel\Tests\Models\SqlUser; use MongoDB\Laravel\Tests\Models\User; @@ -1075,6 +1076,16 @@ public function testGetDirtyDates(): void $this->assertEmpty($user->getDirty()); } + public function testGetDirtyObjects(): void + { + $user = new User(); + $user->options = new Options(); + $this->assertNotEmpty($user->getDirty()); + + $user->save(); + $this->assertEmpty($user->getDirty()); + } + public function testChunkById(): void { User::create(['name' => 'fork', 'tags' => ['sharp', 'pointy']]); diff --git a/tests/Models/Options.php b/tests/Models/Options.php new file mode 100644 index 000000000..ce9a2c424 --- /dev/null +++ b/tests/Models/Options.php @@ -0,0 +1,37 @@ +option1 = $option1; + return $this; + } + + public function setOption2(string $option2): self + { + $this->option2 = $option2; + return $this; + } + + public function serialize(): object + { + $result = []; + if (isset($this->option1)) { + $result['option1'] = $this->option1; + } + + if (isset($this->option2)) { + $result['option2'] = $this->option2; + } + + return (object) $result; + } +} diff --git a/tests/Models/OptionsCast.php b/tests/Models/OptionsCast.php new file mode 100755 index 000000000..db8045794 --- /dev/null +++ b/tests/Models/OptionsCast.php @@ -0,0 +1,40 @@ +setOption1($value['option1']); + } + + if (! empty($value['option2'])) { + $attributes->setOption2($value['option2']); + } + + return $attributes; + } + + /** + * @param Model $model + * @param string $key + * @param Options|null $value + * @param array $attributes + * + * @return null[] + */ + public function set(Model $model, string $key, mixed $value, array $attributes): array + { + return [ + $key => $value?->serialize(), + ]; + } +} diff --git a/tests/Models/User.php b/tests/Models/User.php index 5b8ac983a..257a2dd34 100644 --- a/tests/Models/User.php +++ b/tests/Models/User.php @@ -29,6 +29,7 @@ * @property Carbon $updated_at * @property string $username * @property MemberStatus member_status + * @property Options $options */ class User extends Model implements AuthenticatableContract, CanResetPasswordContract { @@ -44,6 +45,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon 'birthday' => 'datetime', 'entry.date' => 'datetime', 'member_status' => MemberStatus::class, + 'options' => OptionsCast::class, ]; protected $fillable = [ From 40ecfd01296e09a32690a9e91ff18f4056ab4be6 Mon Sep 17 00:00:00 2001 From: Guram Vashakidze Date: Thu, 7 Aug 2025 17:52:53 +0300 Subject: [PATCH 3/6] fix phpDoc return type --- tests/Models/OptionsCast.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Models/OptionsCast.php b/tests/Models/OptionsCast.php index db8045794..9a427ad1d 100755 --- a/tests/Models/OptionsCast.php +++ b/tests/Models/OptionsCast.php @@ -29,7 +29,7 @@ public function get(Model $model, string $key, mixed $value, array $attributes): * @param Options|null $value * @param array $attributes * - * @return null[] + * @return null[]|object[] */ public function set(Model $model, string $key, mixed $value, array $attributes): array { From 8e808a372b74ea4a45d270bac16f041c6d36a1d1 Mon Sep 17 00:00:00 2001 From: Guram Vashakidze Date: Fri, 8 Aug 2025 17:29:57 +0300 Subject: [PATCH 4/6] comparing castable attributes just by serialization --- src/Eloquent/DocumentModel.php | 7 ------- tests/ModelTest.php | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Eloquent/DocumentModel.php b/src/Eloquent/DocumentModel.php index 7d7ef6674..8051858e0 100644 --- a/src/Eloquent/DocumentModel.php +++ b/src/Eloquent/DocumentModel.php @@ -379,13 +379,6 @@ public function originalIsEquivalent($key) } if ($this->isClassCastable($key)) { - $attribute = $this->castAttribute($key, $attribute); - $original = $this->castAttribute($key, $original); - - if ($attribute === $original) { - return true; - } - return serialize($attribute) === serialize($original); } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 9cefd8049..e67f742e9 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -1084,6 +1084,9 @@ public function testGetDirtyObjects(): void $user->save(); $this->assertEmpty($user->getDirty()); + + $user->options = (new Options())->setOption1('Value1'); + $this->assertNotEmpty($user->getDirty()); } public function testChunkById(): void From 13d48d40bc3e4007a91a141a0b8c6f5e5b924180 Mon Sep 17 00:00:00 2001 From: Guram Vashakidze Date: Fri, 8 Aug 2025 17:36:47 +0300 Subject: [PATCH 5/6] add one more "assert" into tests --- tests/ModelTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ModelTest.php b/tests/ModelTest.php index e67f742e9..7e3445492 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -1087,6 +1087,9 @@ public function testGetDirtyObjects(): void $user->options = (new Options())->setOption1('Value1'); $this->assertNotEmpty($user->getDirty()); + + $user->save(); + $this->assertEmpty($user->getDirty()); } public function testChunkById(): void From 7358dcc0864e509c7058b9e63439fe5da3692059 Mon Sep 17 00:00:00 2001 From: Guram Vashakidze Date: Wed, 20 Aug 2025 12:43:40 +0300 Subject: [PATCH 6/6] change comparing by serialization to different comparing type (related to variable type) --- src/Eloquent/DocumentModel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Eloquent/DocumentModel.php b/src/Eloquent/DocumentModel.php index 8051858e0..9f928f061 100644 --- a/src/Eloquent/DocumentModel.php +++ b/src/Eloquent/DocumentModel.php @@ -40,10 +40,10 @@ use function in_array; use function is_array; use function is_numeric; +use function is_object; use function is_string; use function ltrim; use function method_exists; -use function serialize; use function sprintf; use function str_contains; use function str_starts_with; @@ -379,7 +379,7 @@ public function originalIsEquivalent($key) } if ($this->isClassCastable($key)) { - return serialize($attribute) === serialize($original); + return ! is_object($attribute) ? $attribute === $original : $attribute == $original; } return is_numeric($attribute) && is_numeric($original)