Skip to content

[Serializer] Allow custom timezone in DateTimeNormalizer during denormalization #60153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: 7.3
Choose a base branch
from
1 change: 1 addition & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
7.3
---

* Add `PRESERVE_CONTEXT_TIMEZONE_KEY` to `DateTimeNormalizer` to preserve the context timezone
* Deprecate the `CompiledClassMetadataFactory` and `CompiledClassMetadataCacheWarmer` classes
* Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers
* Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,13 @@ public function withCast(?string $cast): static
{
return $this->with(DateTimeNormalizer::CAST_KEY, $cast);
}

/**
* Configures if the timezone should be used from the context and ignore any timezone
* defined in date strings.
*/
public function withPreserveContextTimezone(?bool $preserveContextTimezone): static
{
return $this->with(DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY, $preserveContextTimezone);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ final class DateTimeNormalizer implements NormalizerInterface, DenormalizerInter
public const FORMAT_KEY = 'datetime_format';
public const TIMEZONE_KEY = 'datetime_timezone';
public const CAST_KEY = 'datetime_cast';
/* if set to true during denormalization DateTime(Immutable) objects will always receive the timezone set in the context
and the tz from date strings and timestamps will be ignored */
public const PRESERVE_CONTEXT_TIMEZONE_KEY = 'datetime_preserve_context_timezone';

private array $defaultContext = [
self::FORMAT_KEY => \DateTimeInterface::RFC3339,
self::TIMEZONE_KEY => null,
self::CAST_KEY => null,
self::PRESERVE_CONTEXT_TIMEZONE_KEY => false
];

private const SUPPORTED_TYPES = [
Expand Down Expand Up @@ -112,7 +116,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a

if (null !== $dateTimeFormat) {
if (false !== $object = $type::createFromFormat($dateTimeFormat, $data, $timezone)) {
return $object;
return $this->preserveContextTimezone($object, $context);
}

$dateTimeErrors = $type::getLastErrors();
Expand All @@ -124,11 +128,11 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a

if (null !== $defaultDateTimeFormat) {
if (false !== $object = $type::createFromFormat($defaultDateTimeFormat, $data, $timezone)) {
return $object;
return $this->preserveContextTimezone($object, $context);
}
}

return new $type($data, $timezone);
return $this->preserveContextTimezone(new $type($data, $timezone), $context);
} catch (NotNormalizableValueException $e) {
throw $e;
} catch (\Exception $e) {
Expand Down Expand Up @@ -167,4 +171,16 @@ private function getTimezone(array $context): ?\DateTimeZone

return $dateTimeZone instanceof \DateTimeZone ? $dateTimeZone : new \DateTimeZone($dateTimeZone);
}

private function preserveContextTimezone(\DateTime|\DateTimeImmutable $object, array $context): \DateTime|\DateTimeImmutable
{
$timezone = $this->getTimezone($context);
$preserveTimezone = $context[self::PRESERVE_CONTEXT_TIMEZONE_KEY] ?? $this->defaultContext[self::PRESERVE_CONTEXT_TIMEZONE_KEY];

if (null === $timezone || !$preserveTimezone) {
return $object;
}

return $object->setTimezone($timezone);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public function testWithers(array $values)
->withFormat($values[DateTimeNormalizer::FORMAT_KEY])
->withTimezone($values[DateTimeNormalizer::TIMEZONE_KEY])
->withCast($values[DateTimeNormalizer::CAST_KEY])
->withPreserveContextTimezone($values[DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY])
->toArray();

$this->assertEquals($values, $context);
Expand All @@ -53,12 +54,14 @@ public static function withersDataProvider(): iterable
DateTimeNormalizer::FORMAT_KEY => 'format',
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('GMT'),
DateTimeNormalizer::CAST_KEY => 'int',
DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY => true,
]];

yield 'With null values' => [[
DateTimeNormalizer::FORMAT_KEY => null,
DateTimeNormalizer::TIMEZONE_KEY => null,
DateTimeNormalizer::CAST_KEY => null,
DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY => null,
]];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,59 @@ public static function denormalizeUsingTimezonePassedInContextProvider()
];
}

public function testDenormalizeUsingPreserveContextTimezoneAndFormatPassedInConstructor()
{
$normalizer = new DateTimeNormalizer(
[
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'),
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d\\TH:i:sO',
DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY => true,
]
);
$actual = $normalizer->denormalize('2016-12-01T12:34:56+0000', \DateTimeInterface::class);
$this->assertEquals(new \DateTimeZone('Japan'), $actual->getTimezone());
}

public function testDenormalizeUsingPreserveContextTimezoneAndFormatPassedInContext()
{
$actual = $this->normalizer->denormalize(
'2016-12-01T12:34:56+0000',
\DateTimeInterface::class,
null,
[
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'),
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d\\TH:i:sO',
DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY => true,
]
);
$this->assertEquals(new \DateTimeZone('Japan'), $actual->getTimezone());
}

public function testDenormalizeUsingPreserveContextTimezoneWithoutFormat()
{
$actual = $this->normalizer->denormalize(
'2016-12-01T12:34:56+0000',
\DateTimeInterface::class,
null,
[
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'),
DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY => true,
]
);
$this->assertEquals(new \DateTimeZone('Japan'), $actual->getTimezone());
}

public function testDenormalizeUsingPreserveContextShouldBeIgnoredWithoutTimezoneInContext()
{
$actual = $this->normalizer->denormalize(
'2016-12-01T12:34:56+0000',
\DateTimeInterface::class,
null,
[DateTimeNormalizer::PRESERVE_CONTEXT_TIMEZONE_KEY => true]
);
$this->assertEquals(new \DateTimeZone('+00:00'), $actual->getTimezone());
}

public function testDenormalizeInvalidDataThrowsException()
{
$this->expectException(UnexpectedValueException::class);
Expand Down
Loading