Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 364bd7f

Browse filesBrowse files
committed
[Serializer] Add support for denormalizing invalid datetime without throwing an exception
1 parent c1c973c commit 364bd7f
Copy full SHA for 364bd7f

File tree

8 files changed

+78
-10
lines changed
Filter options

8 files changed

+78
-10
lines changed

‎UPGRADE-5.4.md

Copy file name to clipboardExpand all lines: UPGRADE-5.4.md
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ Security
4545
* Deprecate `TokenInterface:isAuthenticated()` and `setAuthenticated()` methods without replacement.
4646
Security tokens won't have an "authenticated" flag anymore, so they will always be considered authenticated
4747
* Deprecate `DeauthenticatedEvent`, use `TokenDeauthenticatedEvent` instead
48+
49+
Serializer
50+
----------
51+
52+
* Deprecate not setting a value for the context key `DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY`

‎UPGRADE-6.0.md

Copy file name to clipboardExpand all lines: UPGRADE-6.0.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ Serializer
349349
* Removed `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead.
350350
* `ArrayDenormalizer` does not implement `SerializerAwareInterface` anymore.
351351
* The annotation classes cannot be constructed by passing an array of parameters as first argument anymore, use named arguments instead
352+
* The default context value for `DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY` becomes `false`.
352353

353354
TwigBundle
354355
----------

‎src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ framework:
1313

1414
services:
1515
logger: { class: Psr\Log\NullLogger }
16+
17+
serializer.normalizer.datetime:
18+
class: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
19+
arguments:
20+
$defaultContext:
21+
throw_exception_on_invalid_key: false

‎src/Symfony/Component/Serializer/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support of PHP backed enumerations
8+
* Add support for denormalizing invalid datetime without throwing an exception
89

910
5.3
1011
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php
+25-2Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,21 @@
1717
/**
1818
* Normalizes an object implementing the {@see \DateTimeInterface} to a date string.
1919
* Denormalizes a date string to an instance of {@see \DateTime} or {@see \DateTimeImmutable}.
20+
* The denormalization may return the raw data if invalid according to the value of $context[self::THROW_EXCEPTION_ON_INVALID_KEY].
2021
*
2122
* @author Kévin Dunglas <dunglas@gmail.com>
2223
*/
2324
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
2425
{
2526
public const FORMAT_KEY = 'datetime_format';
2627
public const TIMEZONE_KEY = 'datetime_timezone';
28+
public const THROW_EXCEPTION_ON_INVALID_KEY = 'throw_exception_on_invalid_key';
2729

2830
private $defaultContext = [
2931
self::FORMAT_KEY => \DateTime::RFC3339,
3032
self::TIMEZONE_KEY => null,
33+
// BC layer to be moved to "false" in 6.0
34+
self::THROW_EXCEPTION_ON_INVALID_KEY => null,
3135
];
3236

3337
private const SUPPORTED_TYPES = [
@@ -39,6 +43,10 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface,
3943
public function __construct(array $defaultContext = [])
4044
{
4145
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
46+
47+
if (null === $this->defaultContext[self::THROW_EXCEPTION_ON_INVALID_KEY]) {
48+
trigger_deprecation('symfony/serializer', '5.4', 'The key context "%s" of "%s" must be defined. The value will be "false" in Symfony 6.0.', self::THROW_EXCEPTION_ON_INVALID_KEY, __CLASS__);
49+
}
4250
}
4351

4452
/**
@@ -77,15 +85,22 @@ public function supportsNormalization($data, string $format = null)
7785
* {@inheritdoc}
7886
*
7987
* @throws NotNormalizableValueException
80-
*
81-
* @return \DateTimeInterface
8288
*/
8389
public function denormalize($data, string $type, string $format = null, array $context = [])
8490
{
8591
$dateTimeFormat = $context[self::FORMAT_KEY] ?? null;
92+
$throwExceptionOnInvalid = $context[self::THROW_EXCEPTION_ON_INVALID_KEY] ?? $this->defaultContext[self::THROW_EXCEPTION_ON_INVALID_KEY];
93+
// BC layer to be removed in 6.0
94+
if (null === $throwExceptionOnInvalid) {
95+
$throwExceptionOnInvalid = true;
96+
}
8697
$timezone = $this->getTimezone($context);
8798

8899
if (null === $data || (\is_string($data) && '' === trim($data))) {
100+
if (!$throwExceptionOnInvalid) {
101+
return $data;
102+
}
103+
89104
throw new NotNormalizableValueException('The data is either an empty string or null, you should pass a string that can be parsed with the passed format or a valid DateTime string.');
90105
}
91106

@@ -98,12 +113,20 @@ public function denormalize($data, string $type, string $format = null, array $c
98113

99114
$dateTimeErrors = \DateTime::class === $type ? \DateTime::getLastErrors() : \DateTimeImmutable::getLastErrors();
100115

116+
if (!$throwExceptionOnInvalid) {
117+
return $data;
118+
}
119+
101120
throw new NotNormalizableValueException(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])));
102121
}
103122

104123
try {
105124
return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
106125
} catch (\Exception $e) {
126+
if (!$throwExceptionOnInvalid) {
127+
return $data;
128+
}
129+
107130
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
108131
}
109132
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php
+36-4Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Serializer\Tests\Normalizer;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1516
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1617
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
1718
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
@@ -21,14 +22,18 @@
2122
*/
2223
class DateTimeNormalizerTest extends TestCase
2324
{
25+
use ExpectDeprecationTrait;
26+
2427
/**
2528
* @var DateTimeNormalizer
2629
*/
2730
private $normalizer;
2831

2932
protected function setUp(): void
3033
{
31-
$this->normalizer = new DateTimeNormalizer();
34+
$this->normalizer = new DateTimeNormalizer([
35+
DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => true,
36+
]);
3237
}
3338

3439
public function testSupportsNormalization()
@@ -51,13 +56,13 @@ public function testNormalizeUsingFormatPassedInContext()
5156

5257
public function testNormalizeUsingFormatPassedInConstructor()
5358
{
54-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'y']);
59+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'y', DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => true]);
5560
$this->assertEquals('16', $normalizer->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC'))));
5661
}
5762

5863
public function testNormalizeUsingTimeZonePassedInConstructor()
5964
{
60-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan')]);
65+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'), DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => true]);
6166

6267
$this->assertSame('2016-12-01T00:00:00+09:00', $normalizer->normalize(new \DateTime('2016/12/01', new \DateTimeZone('Japan'))));
6368
$this->assertSame('2016-12-01T09:00:00+09:00', $normalizer->normalize(new \DateTime('2016/12/01', new \DateTimeZone('UTC'))));
@@ -184,7 +189,7 @@ public function testDenormalizeUsingTimezonePassedInConstructor()
184189
{
185190
$timezone = new \DateTimeZone('Japan');
186191
$expected = new \DateTime('2016/12/01 17:35:00', $timezone);
187-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => $timezone]);
192+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => $timezone, DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => true]);
188193

189194
$this->assertEquals($expected, $normalizer->denormalize('2016.12.01 17:35:00', \DateTime::class, null, [
190195
DateTimeNormalizer::FORMAT_KEY => 'Y.m.d H:i:s',
@@ -276,4 +281,31 @@ public function testDenormalizeFormatMismatchThrowsException()
276281
$this->expectException(UnexpectedValueException::class);
277282
$this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d|']);
278283
}
284+
285+
public function provideDenormalizeInvalidDataDontThrowsExceptionTests()
286+
{
287+
yield ['invalid date'];
288+
yield [null];
289+
yield [''];
290+
yield [' '];
291+
yield [' 2016.01.01 ', [DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|']];
292+
yield ['2016-01-01T00:00:00+00:00', [DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|']];
293+
}
294+
295+
/** @dataProvider provideDenormalizeInvalidDataDontThrowsExceptionTests */
296+
public function testDenormalizeInvalidDataDontThrowsException($data, array $context = [])
297+
{
298+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => false]);
299+
$this->assertSame($data, $normalizer->denormalize($data, \DateTimeInterface::class, null, $context));
300+
}
301+
302+
/**
303+
* @group legacy
304+
*/
305+
public function testLegacyConstructor()
306+
{
307+
$this->expectDeprecation('Since symfony/serializer 5.4: The key context "throw_exception_on_invalid_key" of "Symfony\Component\Serializer\Normalizer\DateTimeNormalizer" must be defined. The value will be "false" in Symfony 6.0.');
308+
309+
new DateTimeNormalizer();
310+
}
279311
}

‎src/Symfony/Component/Serializer/Tests/Normalizer/Features/ContextMetadataTestTrait.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/Features/ContextMetadataTestTrait.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function testContextMetadataNormalize()
3232
{
3333
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
3434
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
35-
new Serializer([new DateTimeNormalizer(), $normalizer]);
35+
new Serializer([new DateTimeNormalizer([DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => false]), $normalizer]);
3636

3737
$dummy = new ContextMetadataDummy();
3838
$dummy->date = new \DateTime('2011-07-28T08:44:00.123+00:00');
@@ -52,7 +52,7 @@ public function testContextMetadataContextDenormalize()
5252
{
5353
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
5454
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
55-
new Serializer([new DateTimeNormalizer(), $normalizer]);
55+
new Serializer([new DateTimeNormalizer([DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => false]), $normalizer]);
5656

5757
/** @var ContextMetadataDummy $dummy */
5858
$dummy = $normalizer->denormalize(['date' => '2011-07-28T08:44:00+00:00'], ContextMetadataDummy::class);

‎src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ public function testDenomalizeRecursive()
619619
{
620620
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
621621
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
622-
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
622+
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer([DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => false]), $normalizer]);
623623

624624
$obj = $serializer->denormalize([
625625
'inner' => ['foo' => 'foo', 'bar' => 'bar'],
@@ -638,7 +638,7 @@ public function testAcceptJsonNumber()
638638
{
639639
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
640640
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
641-
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
641+
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer([DateTimeNormalizer::THROW_EXCEPTION_ON_INVALID_KEY => false]), $normalizer]);
642642

643643
$this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'json')->number);
644644
$this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'jsonld')->number);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.