Skip to content

Commit 8bcd45b

Browse files
committed
[Serializer] Add the auto format to explicitly use PHP datetime constructor
1 parent e16f867 commit 8bcd45b

File tree

6 files changed

+75
-4
lines changed

6 files changed

+75
-4
lines changed

UPGRADE-6.3.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ SecurityBundle
5757
* Deprecate enabling bundle and not configuring it
5858
* Deprecate the `security.firewalls.logout.csrf_token_generator` config option, use `security.firewalls.logout.csrf_token_manager` instead
5959

60+
Serializer
61+
----------
62+
63+
* Deprecate datetime constructor as a fallback whenever the `DateTimeNormalizer`
64+
default format mismatches. Use the `DateTimeNormalizer::FORMAT_AUTO` when
65+
denormalizing to explicitly rely on the PHP datetime constructor instead.
66+
6067
Validator
6168
---------
6269

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `XmlEncoder::SAVE_OPTIONS` context option
88
* Deprecate datetime constructor as a fallback whenever the `DateTimeNormalizer` default format mismatches
9+
* Add `DateTimeNormalizer::FORMAT_AUTO` to denormalize datetime objects using the PHP datetime constructor
910

1011
6.2
1112
---

src/Symfony/Component/Serializer/Context/Normalizer/DateTimeNormalizerContextBuilder.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ public function withFormat(?string $format): static
3535
return $this->with(DateTimeNormalizer::FORMAT_KEY, $format);
3636
}
3737

38+
/**
39+
* Configures the denormalization format of the date to use PHP datetime construct.
40+
*
41+
* @see https://www.php.net/manual/en/datetime.construct.php
42+
*/
43+
public function withAutoDenormalizationFormat(): static
44+
{
45+
return $this->with(DateTimeNormalizer::FORMAT_KEY, DateTimeNormalizer::FORMAT_AUTO);
46+
}
47+
3848
/**
3949
* Configures the timezone of the date.
4050
*

src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\PropertyInfo\Type;
1515
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
16+
use Symfony\Component\Serializer\Exception\LogicException;
1617
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1718

1819
/**
@@ -24,6 +25,10 @@
2425
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
2526
{
2627
public const FORMAT_KEY = 'datetime_format';
28+
/**
29+
* Use this value as the `datetime_format` to use the datetime constructor when denormalizing.
30+
*/
31+
public const FORMAT_AUTO = 'auto';
2732
public const TIMEZONE_KEY = 'datetime_timezone';
2833

2934
private $defaultContext = [
@@ -44,6 +49,10 @@ public function __construct(array $defaultContext = [])
4449

4550
public function setDefaultContext(array $defaultContext): void
4651
{
52+
if (($defaultContext[self::FORMAT_KEY] ?? null) === self::FORMAT_AUTO) {
53+
throw new LogicException(sprintf('The "%s" format cannot is not supported in the default "%s" context key. Use this format on specific context when denormalizing.', self::FORMAT_AUTO, self::FORMAT_KEY));
54+
}
55+
4756
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
4857
}
4958

@@ -57,6 +66,11 @@ public function normalize(mixed $object, string $format = null, array $context =
5766
}
5867

5968
$dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
69+
70+
if (self::FORMAT_AUTO === $dateTimeFormat) {
71+
throw new LogicException(sprintf('The "%s" format cannot is not supported in the "%s" context key when normalizing. Use this format on specific context when denormalizing.', self::FORMAT_AUTO, self::FORMAT_KEY));
72+
}
73+
6074
$timezone = $this->getTimezone($context);
6175

6276
if (null !== $timezone) {
@@ -88,6 +102,17 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
88102
}
89103

90104
if (null !== $dateTimeFormat) {
105+
// If we specifically asked for the auto format on denormalization context:
106+
if (self::FORMAT_AUTO === $dateTimeFormat) {
107+
try {
108+
// use the constructor to create the DateTime object:
109+
return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
110+
} catch (\Exception $e) {
111+
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, false, $e->getCode(), $e);
112+
}
113+
}
114+
115+
// Otherwise, use the provided format:
91116
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone);
92117

93118
if (false !== $object) {
@@ -108,7 +133,11 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
108133
return $object;
109134
}
110135

111-
trigger_deprecation('symfony/serializer', '6.2', 'Relying on a datetime constructor as a fallback when using a specific default date format (`datetime_format`) for the DateTimeNormalizer is deprecated. Respect the "%s" default format.', $defaultDateTimeFormat);
136+
// TODO: Throw a NotNormalizableValueException exception in Symfony 7.0+ instead of the deprecation:
137+
// $dateTimeErrors = \DateTime::class === $type ? \DateTime::getLastErrors() : \DateTimeImmutable::getLastErrors();
138+
// throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $defaultDateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
139+
140+
trigger_deprecation('symfony/serializer', '6.3', 'Relying on a datetime constructor as a fallback when using a specific default date format (`datetime_format`) for the DateTimeNormalizer is deprecated. Respect the "%s" default format or use the "auto" format in denormalization context.', $defaultDateTimeFormat);
112141
}
113142

114143
try {

src/Symfony/Component/Serializer/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public function withersDataProvider(): iterable
5757
DateTimeNormalizer::FORMAT_KEY => null,
5858
DateTimeNormalizer::TIMEZONE_KEY => null,
5959
]];
60+
61+
yield 'With auto format' => [[
62+
DateTimeNormalizer::FORMAT_KEY => DateTimeNormalizer::FORMAT_KEY,
63+
DateTimeNormalizer::TIMEZONE_KEY => null,
64+
]];
6065
}
6166

6267
public function testCastTimezoneStringToTimezone()

src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1616
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
17+
use Symfony\Component\Serializer\Exception\LogicException;
1718
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
1819
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
1920

@@ -58,6 +59,15 @@ public function testNormalizeUsingFormatPassedInConstructor()
5859
$this->assertEquals('16', $normalizer->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC'))));
5960
}
6061

62+
public function testCannotUseAutoFormatWhileNormalizing()
63+
{
64+
$this->expectException(LogicException::class);
65+
$this->expectExceptionMessage('The "auto" format cannot is not supported in the "datetime_format" context key when normalizing. Use this format on specific context when denormalizing.');
66+
67+
$normalizer = new DateTimeNormalizer();
68+
$normalizer->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), null, [DateTimeNormalizer::FORMAT_KEY => DateTimeNormalizer::FORMAT_AUTO]);
69+
}
70+
6171
public function testNormalizeUsingTimeZonePassedInConstructor()
6272
{
6373
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan')]);
@@ -247,15 +257,16 @@ public function denormalizeUsingTimezonePassedInContextProvider()
247257
\DateTime::RFC3339,
248258
];
249259
}
260+
250261
/**
251262
* Deprecation will be removed as of 7.0, but this test case is still legit
252-
* TODO: remove the @group legacy and expectDeprecation in Symfony 7.0
263+
* TODO: remove the @group legacy and expectDeprecation in Symfony 7.0.
253264
*
254265
* @group legacy
255266
*/
256267
public function testDenormalizeInvalidDataThrowsException()
257268
{
258-
$this->expectDeprecation('Since symfony/serializer 6.2: Relying on a datetime constructor as a fallback when using a specific default date format (`datetime_format`) for the DateTimeNormalizer is deprecated. Respect the "Y-m-d\TH:i:sP" default format.');
269+
$this->expectDeprecation('Since symfony/serializer 6.3: Relying on a datetime constructor as a fallback when using a specific default date format (`datetime_format`) for the DateTimeNormalizer is deprecated. Respect the "Y-m-d\TH:i:sP" default format or use the "auto" format in denormalization context.');
259270

260271
$this->expectException(UnexpectedValueException::class);
261272
$this->normalizer->denormalize('invalid date', \DateTimeInterface::class);
@@ -295,6 +306,14 @@ public function testDenormalizeDateTimeStringWithSpacesUsingFormatPassedInContex
295306
$this->normalizer->denormalize(' 2016.01.01 ', \DateTime::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|']);
296307
}
297308

309+
public function testCannotUseAutoFormatInDefaultContext()
310+
{
311+
$this->expectException(LogicException::class);
312+
$this->expectExceptionMessage('The "auto" format cannot is not supported in the default "datetime_format" context key. Use this format on specific context when denormalizing.');
313+
314+
new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => DateTimeNormalizer::FORMAT_AUTO]);
315+
}
316+
298317
public function testDenormalizeDateTimeStringWithDefaultContextFormat()
299318
{
300319
$format = 'd/m/Y';
@@ -311,7 +330,7 @@ public function testDenormalizeDateTimeStringWithDefaultContextFormat()
311330
*/
312331
public function testDenormalizeDateTimeStringWithDefaultContextAllowsErrorFormat()
313332
{
314-
$this->expectDeprecation('Since symfony/serializer 6.2: Relying on a datetime constructor as a fallback when using a specific default date format (`datetime_format`) for the DateTimeNormalizer is deprecated. Respect the "d/m/Y" default format.');
333+
$this->expectDeprecation('Since symfony/serializer 6.3: Relying on a datetime constructor as a fallback when using a specific default date format (`datetime_format`) for the DateTimeNormalizer is deprecated. Respect the "d/m/Y" default format or use the "auto" format in denormalization context.');
315334

316335
$format = 'd/m/Y'; // the default format
317336
$string = '2020-01-01'; // the value which is in the wrong format, but is accepted because of `new \DateTime` in DateTimeNormalizer::denormalize

0 commit comments

Comments
 (0)