From 730a5be6522c2b8968c40a04d04b46f6bd3d9efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 1 Mar 2024 19:00:54 +0100 Subject: [PATCH 1/5] PHPORM-148 Fix null value for datetime field --- CHANGELOG.md | 3 ++- src/Eloquent/Model.php | 19 +++++++++++++------ tests/Casts/DateTest.php | 12 ++++++++++++ tests/Casts/DatetimeTest.php | 24 ++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b536def07..82fbd7131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.1.3] - unreleased +## [4.1.3] - 2024-03-01 * Fix the timezone of `datetime` fields when they are read from the database by @GromNaN in [#2739](https://github.com/mongodb/laravel-mongodb/pull/2739) +* Fix support for null values in `datetime` fields by @GromNaN in [#2741](https://github.com/mongodb/laravel-mongodb/pull/2741) ## [4.1.2] - 2024-02-22 diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index dbf7579cd..de0ced4c2 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -320,12 +320,19 @@ protected function castAttribute($key, $value) { $castType = $this->getCastType($key); - return match ($castType) { - 'immutable_custom_datetime','immutable_datetime' => str_starts_with($this->getCasts()[$key], 'immutable_date:') ? - $this->asDate($value)->toImmutable() : - $this->asDateTime($value)->toImmutable(), - default => parent::castAttribute($key, $value) - }; + if ($castType === 'immutable_custom_datetime' || $castType === 'immutable_datetime') { + if ($value === null) { + return null; + } + + if (str_starts_with($this->getCasts()[$key], 'immutable_date:')) { + return $this->asDate($value)->toImmutable(); + } + + return $this->asDateTime($value)->toImmutable(); + } + + return parent::castAttribute($key, $value); } /** @inheritdoc */ diff --git a/tests/Casts/DateTest.php b/tests/Casts/DateTest.php index 20ce5dd9a..83811e8f5 100644 --- a/tests/Casts/DateTest.php +++ b/tests/Casts/DateTest.php @@ -52,6 +52,12 @@ public function testDate(): void 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); + + $model = Casting::query()->create(); + $this->assertNull($model->dateField); + + $model->update(['dateField' => null]); + $this->assertNull($model->dateField); } public function testDateAsString(): void @@ -126,5 +132,11 @@ public function testImmutableDateWithCustomFormat(): void Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField, ); + + $model = Casting::query()->create(); + $this->assertNull($model->immutableDateField); + + $model->update(['immutableDateField' => null]); + $this->assertNull($model->immutableDateField); } } diff --git a/tests/Casts/DatetimeTest.php b/tests/Casts/DatetimeTest.php index 022ed3535..49f1cd9c6 100644 --- a/tests/Casts/DatetimeTest.php +++ b/tests/Casts/DatetimeTest.php @@ -36,6 +36,12 @@ public function testDatetime(): void 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); + + $model = Casting::query()->create(); + $this->assertNull($model->datetimeField); + + $model->update(['datetimeField' => null]); + $this->assertNull($model->datetimeField); } public function testDatetimeAsString(): void @@ -70,6 +76,12 @@ public function testDatetimeWithCustomFormat(): void self::assertInstanceOf(Carbon::class, $model->datetimeWithFormatField); self::assertEquals(now()->subDay()->format('j.n.Y H:i'), (string) $model->datetimeWithFormatField); + + $model = Casting::query()->create(); + $this->assertNull($model->datetimeWithFormatField); + + $model->update(['datetimeWithFormatField' => null]); + $this->assertNull($model->datetimeWithFormatField); } public function testImmutableDatetime(): void @@ -92,6 +104,12 @@ public function testImmutableDatetime(): void Carbon::createFromTimestamp(1698577443)->subDay()->format('Y-m-d H:i:s'), (string) $model->immutableDatetimeField, ); + + $model = Casting::query()->create(); + $this->assertNull($model->immutableDatetimeField); + + $model->update(['immutableDatetimeField' => null]); + $this->assertNull($model->immutableDatetimeField); } public function testImmutableDatetimeWithCustomFormat(): void @@ -113,5 +131,11 @@ public function testImmutableDatetimeWithCustomFormat(): void Carbon::createFromTimestamp(1698577443)->subDay()->format('j.n.Y H:i'), (string) $model->immutableDatetimeWithFormatField, ); + + $model = Casting::query()->create(); + $this->assertNull($model->immutableDatetimeWithFormatField); + + $model->update(['immutableDatetimeWithFormatField' => null]); + $this->assertNull($model->immutableDatetimeWithFormatField); } } From 6c54739cbc6a169ba3d72a7ff7dd425d38fb04df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 1 Mar 2024 19:11:07 +0100 Subject: [PATCH 2/5] Update date with custom format to behave like Eloquent With a custom format, "date" cast behave like "datetime". The time is not reset to the start of the day. https://github.com/laravel/framework/blob/v10.46.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L866-L869 --- src/Eloquent/Model.php | 20 -------------------- tests/Casts/DateTest.php | 4 +++- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index de0ced4c2..e2d12036d 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -315,26 +315,6 @@ protected function fromDecimal($value, $decimals) return new Decimal128($this->asDecimal($value, $decimals)); } - /** @inheritdoc */ - protected function castAttribute($key, $value) - { - $castType = $this->getCastType($key); - - if ($castType === 'immutable_custom_datetime' || $castType === 'immutable_datetime') { - if ($value === null) { - return null; - } - - if (str_starts_with($this->getCasts()[$key], 'immutable_date:')) { - return $this->asDate($value)->toImmutable(); - } - - return $this->asDateTime($value)->toImmutable(); - } - - return parent::castAttribute($key, $value); - } - /** @inheritdoc */ public function attributesToArray() { diff --git a/tests/Casts/DateTest.php b/tests/Casts/DateTest.php index 83811e8f5..7a8f78fd8 100644 --- a/tests/Casts/DateTest.php +++ b/tests/Casts/DateTest.php @@ -115,10 +115,12 @@ public function testImmutableDate(): void public function testImmutableDateWithCustomFormat(): void { + // With a custom format, "date" cast behave like "datetime". The time is not reset to the start of the day. + // https://github.com/laravel/framework/blob/v10.46.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L866-L869 $model = Casting::query()->create(['immutableDateWithFormatField' => new DateTime()]); self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateWithFormatField); - self::assertEquals(now()->startOfDay()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField); + self::assertEquals(now()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField); $model->update(['immutableDateWithFormatField' => now()->startOfDay()->subDay()]); From b7852f5b0d5aa92967d1fd8f856c08c99cf62e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 1 Mar 2024 19:21:13 +0100 Subject: [PATCH 3/5] Reset the time for immutable date with custom format --- CHANGELOG.md | 6 +++--- src/Eloquent/Model.php | 5 +++-- tests/Casts/DateTest.php | 4 +--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fbd7131..f7446d2e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.1.3] - 2024-03-01 +## [4.1.3] - 2024-03-04 -* Fix the timezone of `datetime` fields when they are read from the database by @GromNaN in [#2739](https://github.com/mongodb/laravel-mongodb/pull/2739) -* Fix support for null values in `datetime` fields by @GromNaN in [#2741](https://github.com/mongodb/laravel-mongodb/pull/2741) +* Fix the timezone of `datetime` fields when they are read from the database. By @GromNaN in [#2739](https://github.com/mongodb/laravel-mongodb/pull/2739) +* Fix support for null values in `datetime` and reset `date` fields with custom format to the start of the day. By @GromNaN in [#2741](https://github.com/mongodb/laravel-mongodb/pull/2741) ## [4.1.2] - 2024-02-22 diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index e2d12036d..83239c8eb 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -205,9 +205,10 @@ protected function transformModelValue($key, $value) if ($this->hasCast($key) && $value instanceof CarbonInterface) { $value->settings(array_merge($value->getSettings(), ['toStringFormat' => $this->getDateFormat()])); + // "date" cast resets the time to 00:00:00. $castType = $this->getCasts()[$key]; - if ($this->isCustomDateTimeCast($castType) && str_starts_with($castType, 'date:')) { - $value->startOfDay(); + if (str_starts_with($castType, 'date:') || str_starts_with($castType, 'immutable_date:')) { + $value = $value->startOfDay(); } } diff --git a/tests/Casts/DateTest.php b/tests/Casts/DateTest.php index 7a8f78fd8..83811e8f5 100644 --- a/tests/Casts/DateTest.php +++ b/tests/Casts/DateTest.php @@ -115,12 +115,10 @@ public function testImmutableDate(): void public function testImmutableDateWithCustomFormat(): void { - // With a custom format, "date" cast behave like "datetime". The time is not reset to the start of the day. - // https://github.com/laravel/framework/blob/v10.46.0/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L866-L869 $model = Casting::query()->create(['immutableDateWithFormatField' => new DateTime()]); self::assertInstanceOf(CarbonImmutable::class, $model->immutableDateWithFormatField); - self::assertEquals(now()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField); + self::assertEquals(now()->startOfDay()->format('j.n.Y H:i'), (string) $model->immutableDateWithFormatField); $model->update(['immutableDateWithFormatField' => now()->startOfDay()->subDay()]); From dc7366121f4fc72ed05afb263d682a1ac5610895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 1 Mar 2024 19:43:29 +0100 Subject: [PATCH 4/5] Add missing tests --- tests/Casts/DateTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/Casts/DateTest.php b/tests/Casts/DateTest.php index 83811e8f5..64743bce0 100644 --- a/tests/Casts/DateTest.php +++ b/tests/Casts/DateTest.php @@ -90,6 +90,12 @@ public function testDateWithCustomFormat(): void self::assertInstanceOf(Carbon::class, $model->dateWithFormatField); self::assertEquals(now()->startOfDay()->subDay()->format('j.n.Y H:i'), (string) $model->dateWithFormatField); + + $model = Casting::query()->create(); + $this->assertNull($model->dateWithFormatField); + + $model->update(['dateWithFormatField' => null]); + $this->assertNull($model->dateWithFormatField); } public function testImmutableDate(): void @@ -111,6 +117,12 @@ public function testImmutableDate(): void Carbon::createFromTimestamp(1698577443)->subDay()->startOfDay()->format('Y-m-d H:i:s'), (string) $model->immutableDateField, ); + + $model = Casting::query()->create(); + $this->assertNull($model->immutableDateField); + + $model->update(['immutableDateField' => null]); + $this->assertNull($model->immutableDateField); } public function testImmutableDateWithCustomFormat(): void @@ -134,9 +146,9 @@ public function testImmutableDateWithCustomFormat(): void ); $model = Casting::query()->create(); - $this->assertNull($model->immutableDateField); + $this->assertNull($model->immutableDateWithFormatField); - $model->update(['immutableDateField' => null]); - $this->assertNull($model->immutableDateField); + $model->update(['immutableDateWithFormatField' => null]); + $this->assertNull($model->immutableDateWithFormatField); } } From 26e1b810621b8d6c4f609468de2fe14c0f2c05a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Tue, 5 Mar 2024 09:01:19 +0100 Subject: [PATCH 5/5] Update CHANGELOG.md Co-authored-by: Andreas Braun --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7446d2e7..56fe478d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog All notable changes to this project will be documented in this file. -## [4.1.3] - 2024-03-04 +## [4.1.3] - 2024-03-05 * Fix the timezone of `datetime` fields when they are read from the database. By @GromNaN in [#2739](https://github.com/mongodb/laravel-mongodb/pull/2739) * Fix support for null values in `datetime` and reset `date` fields with custom format to the start of the day. By @GromNaN in [#2741](https://github.com/mongodb/laravel-mongodb/pull/2741)