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 7c2dd31143551..831b62b271aa8 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -22,6 +22,8 @@ CHANGELOG * `JsonSerializableNormalizer` * `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 43faf9ad15248..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) { @@ -120,6 +145,12 @@ public function denormalize(mixed $data, string $type, string $format = null, ar if (false !== $object) { return $object; } + + // 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 8f368deca68b3..f302017ac0bef 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -12,7 +12,9 @@ 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\LogicException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -21,6 +23,8 @@ */ class DateTimeNormalizerTest extends TestCase { + use ExpectDeprecationTrait; + /** * @var DateTimeNormalizer */ @@ -55,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')]); @@ -177,7 +190,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 +222,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, ]); @@ -237,12 +258,26 @@ public static function denormalizeUsingTimezonePassedInContextProvider() ]; } + /** + * 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.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); } + 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); @@ -271,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'; @@ -282,8 +325,13 @@ public function testDenormalizeDateTimeStringWithDefaultContextFormat() $this->assertSame('01/10/2018', $denormalizedDate->format($format)); } + /** + * @group legacy + */ public function testDenormalizeDateTimeStringWithDefaultContextAllowsErrorFormat() { + $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 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); 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