From 028bac85dbb781951839866a75edb4ab284f171f Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Wed, 27 Mar 2024 23:12:58 +0100 Subject: [PATCH] feat: add datetime constructor with timezone handling It is now possible to use the following signature to map to a datetime: `array{datetime: non-empty-string|int, timezone: \DateTimeZone}`. ```php (new \CuyZ\Valinor\MapperBuilder()) ->registerConstructor( function (\DateTimeInterface $datetime, \DateTimeZone $timezone): \DateTimeInterface { return $datetime->setTimezone($timezone); }, ) ->mapper() ->map(\DateTimeInterface::class, [ 'datetime' => '2024-03-28T21:12:27+00:00', 'timezone' => 'America/New_York', ]); ``` --- .../Object/DateTimeFormatConstructor.php | 22 ++++++++++++--- .../Mapping/Object/DateTimeMappingTest.php | 27 ++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Mapper/Object/DateTimeFormatConstructor.php b/src/Mapper/Object/DateTimeFormatConstructor.php index 98298f22..1058c8b0 100644 --- a/src/Mapper/Object/DateTimeFormatConstructor.php +++ b/src/Mapper/Object/DateTimeFormatConstructor.php @@ -9,6 +9,10 @@ use DateTimeImmutable; use DateTimeInterface; +use DateTimeZone; + +use function is_array; + /** * Can be given to {@see MapperBuilder::registerConstructor()} to describe which * date formats should be allowed during mapping. @@ -44,15 +48,27 @@ public function __construct(string $format, string ...$formats) /** * @param class-string $className - * @param non-empty-string|int $value + * @param non-empty-string|int|array{datetime: non-empty-string|int, timezone: DateTimeZone} $value */ #[DynamicConstructor] - public function __invoke(string $className, string|int $value): DateTimeInterface + public function __invoke(string $className, string|int|array $value): DateTimeInterface { + if (is_array($value)) { + $datetime = $value['datetime']; + $timezone = $value['timezone']; + } else { + $datetime = $value; + $timezone = null; + } + foreach ($this->formats as $format) { - $date = $className::createFromFormat($format, (string)$value) ?: null; + $date = $className::createFromFormat($format, (string)$datetime) ?: null; if ($date) { + if ($timezone) { + $date = $date->setTimezone($timezone); + } + return $date; } } diff --git a/tests/Integration/Mapping/Object/DateTimeMappingTest.php b/tests/Integration/Mapping/Object/DateTimeMappingTest.php index 99d7113f..60c0e3e9 100644 --- a/tests/Integration/Mapping/Object/DateTimeMappingTest.php +++ b/tests/Integration/Mapping/Object/DateTimeMappingTest.php @@ -10,17 +10,32 @@ final class DateTimeMappingTest extends IntegrationTestCase { - public function test_default_datetime_constructor_cannot_be_used(): void + public function test_default_datetime_constructor_can_be_used_with_valid_rfc_3339(): void { try { - $this->mapperBuilder() + $result = $this->mapperBuilder() ->mapper() - ->map(DateTimeInterface::class, ['datetime' => '2022/08/05', 'timezone' => 'Europe/Paris']); - } catch (MappingError $exception) { - $error = $exception->node()->messages()[0]; + ->map(DateTimeInterface::class, ['datetime' => '2024-03-27T21:12:27+00:00', 'timezone' => 'Europe/Paris']); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('2024-03-27T22:12:27+01:00', $result->format(DATE_ATOM)); + self::assertSame('Europe/Paris', $result->getTimezone()->getName()); + } - self::assertSame('1607027306', $error->code()); + public function test_default_datetime_constructor_can_be_used_with_valid_timestamp(): void + { + try { + $result = $this->mapperBuilder() + ->mapper() + ->map(DateTimeInterface::class, ['datetime' => 1711573053, 'timezone' => 'Europe/Paris']); + } catch (MappingError $error) { + $this->mappingFail($error); } + + self::assertSame('2024-03-27T21:57:33+01:00', $result->format(DATE_ATOM)); + self::assertSame('Europe/Paris', $result->getTimezone()->getName()); } public function test_default_date_constructor_with_valid_rfc_3339_format_source_returns_datetime(): void