From e3d226932d50a5357c6d0bf6b2bc4e7bb007d54e Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Thu, 28 Jul 2022 11:28:07 +0200 Subject: [PATCH 1/2] [Serializer] Deprecate using datetime construct as fallback on default format mismatch --- src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../Normalizer/DateTimeNormalizer.php | 2 ++ .../Normalizer/DateTimeNormalizerTest.php | 35 +++++++++++++++++-- .../Tests/Normalizer/ObjectNormalizerTest.php | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 7c2dd31143551..8fc9ba1a9433a 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -22,6 +22,7 @@ CHANGELOG * `JsonSerializableNormalizer` * `ObjectNormalizer` * `PropertyNormalizer` + * Deprecate datetime constructor as a fallback whenever the `DateTimeNormalizer` default format mismatches 6.2 --- diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index 43faf9ad15248..84e876928d114 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -120,6 +120,8 @@ public function denormalize(mixed $data, string $type, string $format = null, ar if (false !== $object) { return $object; } + + 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); } try { diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 8f368deca68b3..5af8ee201ea4c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -21,6 +22,8 @@ */ class DateTimeNormalizerTest extends TestCase { + use ExpectDeprecationTrait; + /** * @var DateTimeNormalizer */ @@ -177,7 +180,13 @@ public function testDenormalize() $this->assertEquals(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeInterface::class)); $this->assertEquals(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeImmutable::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTime::class)); - $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', \DateTime::class)); + } + + public function testDenormalizeWithoutFormat() + { + $normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => null]); + + $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', \DateTime::class)); } public function testDenormalizeUsingTimezonePassedInConstructor() @@ -203,7 +212,9 @@ public function testDenormalizeUsingFormatPassedInContext() */ public function testDenormalizeUsingTimezonePassedInContext($input, $expected, $timezone, $format = null) { - $actual = $this->normalizer->denormalize($input, \DateTimeInterface::class, null, [ + $normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'Y/m/d H:i:s']); + + $actual = $normalizer->denormalize($input, \DateTimeInterface::class, null, [ DateTimeNormalizer::TIMEZONE_KEY => $timezone, DateTimeNormalizer::FORMAT_KEY => $format, ]); @@ -236,13 +247,26 @@ public static function denormalizeUsingTimezonePassedInContextProvider() \DateTime::RFC3339, ]; } - + /** + * Deprecation will be removed as of 7.0, but this test case is still legit + * TODO: remove the @group legacy and expectDeprecation in Symfony 7.0 + * + * @group legacy + */ public function testDenormalizeInvalidDataThrowsException() { + $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.'); + $this->expectException(UnexpectedValueException::class); $this->normalizer->denormalize('invalid date', \DateTimeInterface::class); } + public function testDenormalizeWithFormatAndInvalidDataThrowsException() + { + $this->expectException(UnexpectedValueException::class); + $this->normalizer->denormalize('invalid date', \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y.m.d']); + } + public function testDenormalizeNullThrowsException() { $this->expectException(UnexpectedValueException::class); @@ -282,8 +306,13 @@ public function testDenormalizeDateTimeStringWithDefaultContextFormat() $this->assertSame('01/10/2018', $denormalizedDate->format($format)); } + /** + * @group legacy + */ public function testDenormalizeDateTimeStringWithDefaultContextAllowsErrorFormat() { + $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.'); + $format = 'd/m/Y'; // the default format $string = '2020-01-01'; // the value which is in the wrong format, but is accepted because of `new \DateTime` in DateTimeNormalizer::denormalize diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 5eba0707c67ea..bb53b190f963e 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -707,7 +707,7 @@ public function testDenomalizeRecursive() $obj = $serializer->denormalize([ 'inner' => ['foo' => 'foo', 'bar' => 'bar'], - 'date' => '1988/01/21', + 'date' => '1988-01-21T00:00:00+00:00', 'inners' => [['foo' => 1], ['foo' => 2]], ], ObjectOuter::class); From 41a1d5676ac376c3d5bc0db9c74a6712d5faaf7e Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Tue, 27 Dec 2022 10:34:07 +0100 Subject: [PATCH 2/2] [Serializer] Add the auto format to explicitly use PHP datetime constructor --- UPGRADE-6.3.md | 7 +++++ src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../DateTimeNormalizerContextBuilder.php | 10 ++++++ .../Normalizer/DateTimeNormalizer.php | 31 ++++++++++++++++++- .../DateTimeNormalizerContextBuilderTest.php | 5 +++ .../Normalizer/DateTimeNormalizerTest.php | 25 +++++++++++++-- 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/UPGRADE-6.3.md b/UPGRADE-6.3.md index 06928dde31914..dc77c18c333ab 100644 --- a/UPGRADE-6.3.md +++ b/UPGRADE-6.3.md @@ -107,6 +107,13 @@ SecurityBundle * Deprecate enabling bundle and not configuring it * Deprecate the `security.firewalls.logout.csrf_token_generator` config option, use `security.firewalls.logout.csrf_token_manager` instead +Serializer +---------- + +* Deprecate datetime constructor as a fallback whenever the `DateTimeNormalizer` + default format mismatches. Use the `DateTimeNormalizer::FORMAT_AUTO` when + denormalizing to explicitly rely on the PHP datetime constructor instead. + Validator --------- diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 8fc9ba1a9433a..831b62b271aa8 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -23,6 +23,7 @@ CHANGELOG * `ObjectNormalizer` * `PropertyNormalizer` * Deprecate datetime constructor as a fallback whenever the `DateTimeNormalizer` default format mismatches + * Add `DateTimeNormalizer::FORMAT_AUTO` to denormalize datetime objects using the PHP datetime constructor 6.2 --- diff --git a/src/Symfony/Component/Serializer/Context/Normalizer/DateTimeNormalizerContextBuilder.php b/src/Symfony/Component/Serializer/Context/Normalizer/DateTimeNormalizerContextBuilder.php index 99517afb1d8d4..d640d72377298 100644 --- a/src/Symfony/Component/Serializer/Context/Normalizer/DateTimeNormalizerContextBuilder.php +++ b/src/Symfony/Component/Serializer/Context/Normalizer/DateTimeNormalizerContextBuilder.php @@ -35,6 +35,16 @@ public function withFormat(?string $format): static return $this->with(DateTimeNormalizer::FORMAT_KEY, $format); } + /** + * Configures the denormalization format of the date to use PHP datetime construct. + * + * @see https://www.php.net/manual/en/datetime.construct.php + */ + public function withAutoDenormalizationFormat(): static + { + return $this->with(DateTimeNormalizer::FORMAT_KEY, DateTimeNormalizer::FORMAT_AUTO); + } + /** * Configures the timezone of the date. * diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index 84e876928d114..fdc0647895d8c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; /** @@ -26,6 +27,10 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface { public const FORMAT_KEY = 'datetime_format'; + /** + * Use this value as the `datetime_format` to use the datetime constructor when denormalizing. + */ + public const FORMAT_AUTO = 'auto'; public const TIMEZONE_KEY = 'datetime_timezone'; private $defaultContext = [ @@ -46,6 +51,10 @@ public function __construct(array $defaultContext = []) public function setDefaultContext(array $defaultContext): void { + if (($defaultContext[self::FORMAT_KEY] ?? null) === self::FORMAT_AUTO) { + 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)); + } + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } @@ -70,6 +79,11 @@ public function normalize(mixed $object, string $format = null, array $context = } $dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]; + + if (self::FORMAT_AUTO === $dateTimeFormat) { + 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)); + } + $timezone = $this->getTimezone($context); if (null !== $timezone) { @@ -101,6 +115,17 @@ public function denormalize(mixed $data, string $type, string $format = null, ar } if (null !== $dateTimeFormat) { + // If we specifically asked for the auto format on denormalization context: + if (self::FORMAT_AUTO === $dateTimeFormat) { + try { + // use the constructor to create the DateTime object: + return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone); + } catch (\Exception $e) { + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, false, $e->getCode(), $e); + } + } + + // Otherwise, use the provided format: $object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone); if (false !== $object) { @@ -121,7 +146,11 @@ public function denormalize(mixed $data, string $type, string $format = null, ar return $object; } - 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); + // TODO: Throw a NotNormalizableValueException exception in Symfony 7.0+ instead of the deprecation: + // $dateTimeErrors = \DateTime::class === $type ? \DateTime::getLastErrors() : \DateTimeImmutable::getLastErrors(); + // 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); + + 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); } try { diff --git a/src/Symfony/Component/Serializer/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php b/src/Symfony/Component/Serializer/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php index 8ab41f949c3cc..f917df0a7ebec 100644 --- a/src/Symfony/Component/Serializer/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Context/Normalizer/DateTimeNormalizerContextBuilderTest.php @@ -57,6 +57,11 @@ public static function withersDataProvider(): iterable DateTimeNormalizer::FORMAT_KEY => null, DateTimeNormalizer::TIMEZONE_KEY => null, ]]; + + yield 'With auto format' => [[ + DateTimeNormalizer::FORMAT_KEY => DateTimeNormalizer::FORMAT_KEY, + DateTimeNormalizer::TIMEZONE_KEY => null, + ]]; } public function testCastTimezoneStringToTimezone() diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 5af8ee201ea4c..f302017ac0bef 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -58,6 +59,15 @@ public function testNormalizeUsingFormatPassedInConstructor() $this->assertEquals('16', $normalizer->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC')))); } + public function testCannotUseAutoFormatWhileNormalizing() + { + $this->expectException(LogicException::class); + $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.'); + + $normalizer = new DateTimeNormalizer(); + $normalizer->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), null, [DateTimeNormalizer::FORMAT_KEY => DateTimeNormalizer::FORMAT_AUTO]); + } + public function testNormalizeUsingTimeZonePassedInConstructor() { $normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan')]); @@ -247,15 +257,16 @@ public static function denormalizeUsingTimezonePassedInContextProvider() \DateTime::RFC3339, ]; } + /** * Deprecation will be removed as of 7.0, but this test case is still legit - * TODO: remove the @group legacy and expectDeprecation in Symfony 7.0 + * TODO: remove the @group legacy and expectDeprecation in Symfony 7.0. * * @group legacy */ public function testDenormalizeInvalidDataThrowsException() { - $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.'); + $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.'); $this->expectException(UnexpectedValueException::class); $this->normalizer->denormalize('invalid date', \DateTimeInterface::class); @@ -295,6 +306,14 @@ public function testDenormalizeDateTimeStringWithSpacesUsingFormatPassedInContex $this->normalizer->denormalize(' 2016.01.01 ', \DateTime::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|']); } + public function testCannotUseAutoFormatInDefaultContext() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "auto" format cannot is not supported in the default "datetime_format" context key. Use this format on specific context when denormalizing.'); + + new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => DateTimeNormalizer::FORMAT_AUTO]); + } + public function testDenormalizeDateTimeStringWithDefaultContextFormat() { $format = 'd/m/Y'; @@ -311,7 +330,7 @@ public function testDenormalizeDateTimeStringWithDefaultContextFormat() */ public function testDenormalizeDateTimeStringWithDefaultContextAllowsErrorFormat() { - $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.'); + $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.'); $format = 'd/m/Y'; // the default format $string = '2020-01-01'; // the value which is in the wrong format, but is accepted because of `new \DateTime` in DateTimeNormalizer::denormalize 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