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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy