Skip to content

Commit cc9d7d3

Browse files
frankdekkernicolas-grekas
authored andcommitted
[Serializer] Allow forcing timezone in DateTimeNormalizer during denormalization
1 parent a9bb338 commit cc9d7d3

File tree

6 files changed

+87
-8
lines changed

6 files changed

+87
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
1414
* Deprecate getters in attribute classes in favor of public properties
1515
* Deprecate `ClassMetadataFactoryCompiler`
16+
* Add `FORCE_TIMEZONE_KEY` to `DateTimeNormalizer` to force the timezone during denormalization
1617

1718
7.3
1819
---

Context/Normalizer/DateTimeNormalizerContextBuilder.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ public function withFormat(?string $format): static
4343
*
4444
* @see https://secure.php.net/manual/en/class.datetimezone.php
4545
*
46+
* @param ?bool $force Whether to enforce the timezone during denormalization
47+
*
4648
* @throws InvalidArgumentException
4749
*/
48-
public function withTimezone(\DateTimeZone|string|null $timezone): static
50+
public function withTimezone(\DateTimeZone|string|null $timezone, ?bool $force = null): static
4951
{
5052
if (null === $timezone) {
51-
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, null);
53+
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, null)->with(DateTimeNormalizer::FORCE_TIMEZONE_KEY, $force);
5254
}
5355

5456
if (\is_string($timezone)) {
@@ -59,7 +61,7 @@ public function withTimezone(\DateTimeZone|string|null $timezone): static
5961
}
6062
}
6163

62-
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, $timezone);
64+
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, $timezone)->with(DateTimeNormalizer::FORCE_TIMEZONE_KEY, $force);
6365
}
6466

6567
/**

Mapping/Loader/schema/serialization.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@
235235
],
236236
"description": "Timezone of the date (DateTimeNormalizer)"
237237
},
238+
"forceTimezone": {
239+
"type": "boolean",
240+
"description": "Whether to enforce the timezone during denormalization (DateTimeNormalizer)"
241+
},
238242
"cast": {
239243
"enum": ["int", "float"],
240244
"description": "Cast type for DateTime (DateTimeNormalizer)"

Normalizer/DateTimeNormalizer.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ final class DateTimeNormalizer implements NormalizerInterface, DenormalizerInter
2525
public const FORMAT_KEY = 'datetime_format';
2626
public const TIMEZONE_KEY = 'datetime_timezone';
2727
public const CAST_KEY = 'datetime_cast';
28+
public const FORCE_TIMEZONE_KEY = 'datetime_force_timezone';
2829

2930
private array $defaultContext = [
3031
self::FORMAT_KEY => \DateTimeInterface::RFC3339,
3132
self::TIMEZONE_KEY => null,
3233
self::CAST_KEY => null,
34+
self::FORCE_TIMEZONE_KEY => false,
3335
];
3436

3537
private const SUPPORTED_TYPES = [
@@ -112,7 +114,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
112114

113115
if (null !== $dateTimeFormat) {
114116
if (false !== $object = $type::createFromFormat($dateTimeFormat, $data, $timezone)) {
115-
return $object;
117+
return $this->enforceTimezone($object, $context);
116118
}
117119

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

125127
if (null !== $defaultDateTimeFormat) {
126128
if (false !== $object = $type::createFromFormat($defaultDateTimeFormat, $data, $timezone)) {
127-
return $object;
129+
return $this->enforceTimezone($object, $context);
128130
}
129131
}
130132

131-
return new $type($data, $timezone);
133+
return $this->enforceTimezone(new $type($data, $timezone), $context);
132134
} catch (NotNormalizableValueException $e) {
133135
throw $e;
134136
} catch (\Exception $e) {
@@ -167,4 +169,16 @@ private function getTimezone(array $context): ?\DateTimeZone
167169

168170
return $dateTimeZone instanceof \DateTimeZone ? $dateTimeZone : new \DateTimeZone($dateTimeZone);
169171
}
172+
173+
private function enforceTimezone(\DateTime|\DateTimeImmutable $object, array $context): \DateTimeInterface
174+
{
175+
$timezone = $this->getTimezone($context);
176+
$forceTimezone = $context[self::FORCE_TIMEZONE_KEY] ?? $this->defaultContext[self::FORCE_TIMEZONE_KEY];
177+
178+
if (null === $timezone || !$forceTimezone) {
179+
return $object;
180+
}
181+
182+
return $object->setTimezone($timezone);
183+
}
170184
}

Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function testWithers(array $values)
3737
{
3838
$context = $this->contextBuilder
3939
->withFormat($values[DateTimeNormalizer::FORMAT_KEY])
40-
->withTimezone($values[DateTimeNormalizer::TIMEZONE_KEY])
40+
->withTimezone($values[DateTimeNormalizer::TIMEZONE_KEY], $values[DateTimeNormalizer::FORCE_TIMEZONE_KEY])
4141
->withCast($values[DateTimeNormalizer::CAST_KEY])
4242
->toArray();
4343

@@ -53,18 +53,23 @@ public static function withersDataProvider(): iterable
5353
DateTimeNormalizer::FORMAT_KEY => 'format',
5454
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('GMT'),
5555
DateTimeNormalizer::CAST_KEY => 'int',
56+
DateTimeNormalizer::FORCE_TIMEZONE_KEY => true,
5657
]];
5758

5859
yield 'With null values' => [[
5960
DateTimeNormalizer::FORMAT_KEY => null,
6061
DateTimeNormalizer::TIMEZONE_KEY => null,
6162
DateTimeNormalizer::CAST_KEY => null,
63+
DateTimeNormalizer::FORCE_TIMEZONE_KEY => null,
6264
]];
6365
}
6466

6567
public function testCastTimezoneStringToTimezone()
6668
{
67-
$this->assertEquals([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('GMT')], $this->contextBuilder->withTimezone('GMT')->toArray());
69+
$this->assertEquals(
70+
[DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('GMT'), DateTimeNormalizer::FORCE_TIMEZONE_KEY => null],
71+
$this->contextBuilder->withTimezone('GMT')->toArray()
72+
);
6873
}
6974

7075
public function testCannotSetInvalidTimezone()

Tests/Normalizer/DateTimeNormalizerTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,59 @@ public static function denormalizeUsingTimezonePassedInContextProvider()
299299
];
300300
}
301301

302+
public function testDenormalizeUsingPreserveContextTimezoneAndFormatPassedInConstructor()
303+
{
304+
$normalizer = new DateTimeNormalizer(
305+
[
306+
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'),
307+
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d\\TH:i:sO',
308+
DateTimeNormalizer::FORCE_TIMEZONE_KEY => true,
309+
]
310+
);
311+
$actual = $normalizer->denormalize('2016-12-01T12:34:56+0000', \DateTimeInterface::class);
312+
$this->assertEquals(new \DateTimeZone('Japan'), $actual->getTimezone());
313+
}
314+
315+
public function testDenormalizeUsingPreserveContextTimezoneAndFormatPassedInContext()
316+
{
317+
$actual = $this->normalizer->denormalize(
318+
'2016-12-01T12:34:56+0000',
319+
\DateTimeInterface::class,
320+
null,
321+
[
322+
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'),
323+
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d\\TH:i:sO',
324+
DateTimeNormalizer::FORCE_TIMEZONE_KEY => true,
325+
]
326+
);
327+
$this->assertEquals(new \DateTimeZone('Japan'), $actual->getTimezone());
328+
}
329+
330+
public function testDenormalizeUsingPreserveContextTimezoneWithoutFormat()
331+
{
332+
$actual = $this->normalizer->denormalize(
333+
'2016-12-01T12:34:56+0000',
334+
\DateTimeInterface::class,
335+
null,
336+
[
337+
DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'),
338+
DateTimeNormalizer::FORCE_TIMEZONE_KEY => true,
339+
]
340+
);
341+
$this->assertEquals(new \DateTimeZone('Japan'), $actual->getTimezone());
342+
}
343+
344+
public function testDenormalizeUsingPreserveContextShouldBeIgnoredWithoutTimezoneInContext()
345+
{
346+
$actual = $this->normalizer->denormalize(
347+
'2016-12-01T12:34:56+0000',
348+
\DateTimeInterface::class,
349+
null,
350+
[DateTimeNormalizer::FORCE_TIMEZONE_KEY => true]
351+
);
352+
$this->assertEquals(new \DateTimeZone('+00:00'), $actual->getTimezone());
353+
}
354+
302355
public function testDenormalizeInvalidDataThrowsException()
303356
{
304357
$this->expectException(UnexpectedValueException::class);

0 commit comments

Comments
 (0)