10000 [Serializer] Allow to cast certain DateTime formats to int/float · symfony/symfony@b3d55df · GitHub
[go: up one dir, main page]

Skip to content

Commit b3d55df

Browse files
committed
[Serializer] Allow to cast certain DateTime formats to int/float
1 parent 0f46e33 commit b3d55df

File tree

10 files changed

+173
-26
lines changed

10 files changed

+173
-26
lines changed

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.1
5+
---
6+
7+
* Allow to cast certain DateTime formats to int/float
8+
49
7.0
510
---
611

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,9 @@ public function withTimezone(\DateTimeZone|string|null $timezone): static
6161

6262
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, $timezone);
6363
}
64+
65+
public function withTimestampCast(bool $cast = true): static
66+
{
67+
return $this->with(DateTimeNormalizer::TIMESTAMP_CAST_KEY, $cast);
68+
}
6469
}

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ final class DateTimeNormalizer implements NormalizerInterface, DenormalizerInter
2525
{
2626
public const FORMAT_KEY = 'datetime_format';
2727
public const TIMEZONE_KEY = 'datetime_timezone';
28+
public const TIMESTAMP_CAST_KEY = 'datetime_timestamp_cast';
2829

2930
private array $defaultContext = [
3031
self::FORMAT_KEY => \DateTimeInterface::RFC3339,
3132
self::TIMEZONE_KEY => null,
33+
// self::TIMESTAMP_CAST_KEY => true,
3234
];
3335

3436
private const SUPPORTED_TYPES = [
@@ -59,12 +61,16 @@ public function getSupportedTypes(?string $format): array
5961
/**
6062
* @throws InvalidArgumentException
6163
*/
62-
public function normalize(mixed $object, string $format = null, array $context = []): string
64+
public function normalize(mixed $object, string $format = null, array $context = []): int|float|string
6365
{
6466
if (!$object instanceof \DateTimeInterface) {
6567
throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".');
6668
}
6769

70+
if (!isset($this->defaultContext[self::TIMESTAMP_CAST_KEY])) {
71+
trigger_deprecation('symfony/serializer', '7.1', 'Not setting the "%s" in defaultContext for "%s" is deprecated. It will default to "true" in 8.0.', self::TIMESTAMP_CAST_KEY, self::class);
72+
}
73+
6874
$dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
6975
$timezone = $this->getTimezone($context);
7076

@@ -73,6 +79,16 @@ public function normalize(mixed $object, string $format = null, array $context =
7379
$object = $object->setTimezone($timezone);
7480
}
7581

82+
if ($context[self::TIMESTAMP_CAST_KEY] ?? $this->defaultContext[self::TIMESTAMP_CAST_KEY] ?? false) {
83+
return match ($dateTimeFormat) {
84+
'U' => $object->getTimestamp(),
85+
'U.u' => (float) $object->format('U.u'),
86+
'Uv' => (int) $object->format('Uv'),
87+
'Uu' => (int) $object->format('Uu'),
88+
default => $object->format($dateTimeFormat),
89+
};
90+
}
91+
7692
return $object->format($dateTimeFormat);
7793
}
7894

@@ -88,8 +104,12 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
88104
{
89105
if (\is_int($data) || \is_float($data)) {
90106
switch ($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY] ?? null) {
91-
case 'U': $data = sprintf('%d', $data); break;
92-
case 'U.u': $data = sprintf('%.6F', $data); break;
107+
case 'U': $data = sprintf('%d', $data);
108+
break;
109+
case 'U.u': $data = sprintf('%.6F', $data);
110+
break;
111+
case 'U.v': $data = sprintf('%.3F', $data);
112+
break;
93113
}
94114
}
95115

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ public static function withersDataProvider(): iterable
5959
]];
6060
}
6161

62+
/**
63+
* @testWith [false]
64+
* [true]
65+
*/
66+
public function testWithTimestampCast(bool $cast)
67+
{
68+
$context = $this->contextBuilder
69+
->withTimestampCast($cast)
70+
->toArray();
71+
72+
$this->assertEquals([DateTimeNormalizer::TIMESTAMP_CAST_KEY => $cast], $context);
73+
}
74+
6275
public function testCastTimezoneStringToTimezone()
6376
{
6477
$this->assertEquals([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('GMT')], $this->contextBuilder->withTimezone('GMT')->toArray());

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ public function testNormalizeUsesContextAttributeForPropertiesInConstructorWithS
743743

744744
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
745745
$normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory), null, $extractor);
746-
$serializer = new Serializer([new DateTimeNormalizer(), $normalizer]);
746+
$serializer = new Serializer([new DateTimeNormalizer([DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]), $normalizer]);
747747

748748
$obj = new ObjectDummyWithContextAttributeAndSerializedPath(new \DateTimeImmutable('22-02-2023'));
749749

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

Lines changed: 117 additions & 14 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,11 +22,15 @@
2122
*/
2223
class DateTimeNormalizerTest extends TestCase
2324
{
25+
use ExpectDeprecationTrait;
26+
2427
private DateTimeNormalizer $normalizer;
2528

2629
protected function setUp(): void
2730
{
28-
$this->normalizer = new DateTimeNormalizer();
31+
$this->normalizer = new DateTimeNormalizer([
32+
DateTimeNormalizer::TIMESTAMP_CAST_KEY => true,
33+
]);
2934
}
3035

3136
public function testSupportsNormalization()
@@ -48,13 +53,13 @@ public function testNormalizeUsingFormatPassedInContext()
4853

4954
public function testNormalizeUsingFormatPassedInConstructor()
5055
{
51-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'y']);
56+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'y', DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
5257
$this->assertEquals('16', $normalizer->normalize(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC'))));
5358
}
5459

5560
public function testNormalizeUsingTimeZonePassedInConstructor()
5661
{
57-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan')]);
62+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'), DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
5863

5964
$this->assertSame('2016-12-01T00:00:00+09:00', $normalizer->normalize(new \DateTimeImmutable('2016/12/01', new \DateTimeZone('Japan'))));
6065
$this->assertSame('2016-12-01T09:00:00+09:00', $normalizer->normalize(new \DateTimeImmutable('2016/12/01', new \DateTimeZone('UTC'))));
@@ -154,6 +159,60 @@ public static function normalizeUsingTimeZonePassedInContextAndExpectedFormatWit
154159
];
155160
}
156161

162+
/**
163+
* @dataProvider provideNormalizeUsingTimestampCastCases
164+
*/
165+
public function testNormalizeUsingTimestampCast(\DateTimeInterface $date, string $format, int|float $expectedResult)
166+
{
167+
self::assertSame($expectedResult, $this->normalizer->normalize($date, null, [
168+
DateTimeNormalizer::TIMESTAMP_CAST_KEY => true,
169+
DateTimeNormalizer::FORMAT_KEY => $format,
170+
]));
171+
}
172+
173+
/**
174+
* @dataProvider provideNormalizeUsingTimestampCastCases
175+
*/
176+
public function testNormalizeUsingTimestampCastFromDefaultContext(\DateTimeInterface $date, string $format, int|float $expectedResult)
177+
{
178+
$normalizer = new DateTimeNormalizer([
179+
DateTimeNormalizer::TIMESTAMP_CAST_KEY => true,
180+
DateTimeNormalizer::FORMAT_KEY => $format,
181+
]);
182+
183+
self::assertSame($expectedResult, $normalizer->normalize($date));
184+
}
185+
186+
/**
187+
* @return iterable<array{0: \DateTimeInterface, 1: non-empty-string, 2: int|float}>
188+
*/
189+
public static function provideNormalizeUsingTimestampCastCases(): iterable
190+
{
191+
yield [
192+
new \DateTimeImmutable('2016-01-01T00:00:00+00:00'),
193+
'U',
194+
1451606400,
195+
];
196+
197+
yield [
198+
new \DateTimeImmutable('2016-01-01T00:00:00.123456+00:00'),
199+
'U.u',
200+
1451606400.123456,
201+
];
202+
203+
yield [
204+
new \DateTimeImmutable('2016-01-01T00:00:00.123456+00:00'),
205+
'Uv',
206+
1451606400123,
207+
];
208+
209+
yield [
210+
new \DateTimeImmutable('2016-01-01T00:00:00.123456+00:00'),
211+
'Uu',
212+
1451606400123456,
213+
];
214+
}
215+
157216
public function testNormalizeInvalidObjectThrowsException()
158217
{
159218
$this->expectException(InvalidArgumentException::class);
@@ -183,7 +242,7 @@ public function testDenormalizeUsingTimezonePassedInConstructor()
183242
{
184243
$timezone = new \DateTimeZone('Japan');
185244
$expected = new \DateTimeImmutable('2016/12/01 17:35:00', $timezone);
186-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => $timezone]);
245+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMEZONE_KEY => $timezone, DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
187246

188247
$this->assertEquals($expected, $normalizer->denormalize('2016.12.01 17:35:00', \DateTimeImmutable::class, null, [
189248
DateTimeNormalizer::FORMAT_KEY => 'Y.m.d H:i:s',
@@ -277,28 +336,58 @@ public function testDenormalizeDateTimeStringWithSpacesUsingFormatPassedInContex
277336
$this->normalizer->denormalize(' 2016.01.01 ', \DateTime::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|']);
278337
}
279338

280-
public function testDenormalizeTimestampWithFormatInContext()
339+
/**
340+
* @dataProvider provideDenormalizeTimestampCases
341+
*/
342+
public function testDenormalizeTimestampWithFormatInContext(int|float $data, string $format, string $expectedResult)
343+
{
344+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
345+
$denormalizedDate = $normalizer->denormalize($data, \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => $format]);
346+
347+
$this->assertSame($expectedResult, $denormalizedDate->format('Y-m-d H:i:s.u'));
348+
}
349+
350+
/**
351+
* @dataProvider provideDenormalizeTimestampCases
352+
*/
353+
public function testDenormalizeTimestampWithFormatInDefaultContext(int|float $data, string $format, string $expectedResult)
281354
{
282-
$normalizer = new DateTimeNormalizer();
283-
$denormalizedDate = $normalizer->denormalize(1698202249, \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U']);
355+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => $format, DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
356+
$denormalizedDate = $normalizer->denormalize($data, \DateTimeInterface::class);
284357

285-
$this->assertSame('2023-10-25 02:50:49', $denormalizedDate->format('Y-m-d H:i:s'));
358+
$this->assertSame($expectedResult, $denormalizedDate->format('Y-m-d H:i:s.u'));
286359
}
287360

288-
public function testDenormalizeTimestampWithFormatInDefaultContext()
361+
/**
362+
* @return iterable<array{0: int|float, 1: non-empty-string, 2: non-empty-string}>
363+
*/
364+
public function provideDenormalizeTimestampCases(): iterable
289365
{
290-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'U']);
291-
$denormalizedDate = $normalizer->denormalize(1698202249, \DateTimeInterface::class);
366+
yield [
367+
1698202249,
368+
'U',
369+
'2023-10-25 02:50:49.000000',
370+
];
292371

293-
$this->assertSame('2023-10-25 02:50:49', $denormalizedDate->format('Y-m-d H:i:s'));
372+
yield [
373+
1698202249.666,
374+
'U.u',
375+
'2023-10-25 02:50:49.666000',
376+
];
377+
378+
yield [
379+
1698202249.123456,
380+
'U.u',
381+
'2023-10-25 02:50:49.123456',
382+
];
294383
}
295384

296385
public function testDenormalizeDateTimeStringWithDefaultContextFormat()
297386
{
298387
$format = 'd/m/Y';
299388
$string = '01/10/2018';
300389

301-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => $format]);
390+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => $format, DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
302391
$denormalizedDate = $normalizer->denormalize($string, \DateTimeInterface::class);
303392

304393
$this->assertSame('01/10/2018', $denormalizedDate->format($format));
@@ -309,7 +398,7 @@ public function testDenormalizeDateTimeStringWithDefaultContextAllowsErrorFormat
309398
$format = 'd/m/Y'; // the default format
310399
$string = '2020-01-01'; // the value which is in the wrong format, but is accepted because of `new \DateTimeImmutable` in DateTimeNormalizer::denormalize
311400

312-
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => $format]);
401+
$normalizer = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => $format, DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]);
313402
$denormalizedDate = $normalizer->denormalize($string, \DateTimeInterface::class);
314403

315404
$this->assertSame('2020-01-01', $denormalizedDate->format('Y-m-d'));
@@ -320,4 +409,18 @@ public function testDenormalizeFormatMismatchThrowsException()
320409
$this->expectException(UnexpectedValueException::class);
321410
$this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeInterface::class, null, [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d|']);
322411
}
412+
413+
/**
414+
* @testWith ["U"]
415+
* ["U.u"]
416+
* [null]
417+
*
418+
* @group legacy
419+
*/
420+
public function testDeprecationWithoutTimestampCast(?string $format)
421+
{
422+
$this->expectDeprecation('Since symfony/serializer 7.1: Not setting the "datetime_timestamp_cast" in defaultContext for "Symfony\Component\Serializer\Normalizer\DateTimeNormalizer" is deprecated. It will default to "true" in 8.0.');
423+
424+
(new DateTimeNormalizer())->normalize(new \DateTimeImmutable(), null, $format !== null ? [DateTimeNormalizer::FORMAT_KEY => $format] : []);
425+
}
323426
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function testContextMetadataNormalize(string $contextMetadataDummyClass)
3535
{
3636
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
3737
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
38-
new Serializer([new DateTimeNormalizer(), $normalizer]);
38+
new Serializer([new DateTimeNormalizer([DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]), $normalizer]);
3939

4040
$dummy = new $contextMetadataDummyClass();
4141
$dummy->date = new \DateTimeImmutable('2011-07-28T08:44:00.123+00:00');
@@ -58,7 +58,7 @@ public function testContextMetadataContextDenormalize(string $contextMetadataDum
5858
{
5959
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
6060
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
61-
new Serializer([new DateTimeNormalizer(), $normalizer]);
61+
new Serializer([new DateTimeNormalizer([DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]), $normalizer]);
6262

6363
/** @var ContextMetadataDummy|ContextChildMetadataDummy $dummy */
6464
$dummy = $normalizer->denormalize(['date' => '2011-07-28T08:44:00+00:00'], $contextMetadataDummyClass);
@@ -90,7 +90,7 @@ public function testContextDenormalizeWithNameConverter()
9090
{
9191
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
9292
$normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter(), null, new PhpDocExtractor());
93-
new Serializer([new DateTimeNormalizer(), $normalizer]);
93+
new Serializer([new DateTimeNormalizer([DateTimeNormalizer::TIMESTAMP_CAST_KEY => true]), $normalizer]);
9494

9595
/** @var ContextMetadataNamingDummy $dummy */
9696
$dummy = $normalizer->denormalize(['created_at' => '28/07/2011'], ContextMetadataNamingDummy::class);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ public function testDenomalizeRecursive()
685685
{
686686
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
687687
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
688-
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
688+
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer([]), $normalizer]);
689689

690690
$obj = $serializer->denormalize([
691691
'inner' => ['foo' => 'foo', 'bar' => 'bar'],
@@ -704,7 +704,7 @@ public function testAcceptJsonNumber()
704704
{
705705
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
706706
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
707-
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
707+
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer([]), $normalizer]);
708708

709709
$this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'json')->number);
710710
$this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'jsonld')->number);
@@ -718,7 +718,7 @@ public function testDoesntHaveIssuesWithUnionConstTypes()
718718

719719
$extractor = new PropertyInfoExtractor([], [new PhpStanExtractor(), new PhpDocExtractor(), new ReflectionExtractor()]);
720720
$normalizer = new ObjectNormalizer(null, null, null, $extractor);
721-
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]);
721+
$serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer([]), $normalizer]);
722722

723723
$this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class() {
724724
/** @var self::*|null */

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ public function testUnionTypeDeserializable()
773773
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
774774
$serializer = new Serializer(
775775
[
776-
new DateTimeNormalizer(),
776+
new DateTimeNormalizer([]),
777777
new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)),
778778
],
779779
['json' => new JsonEncoder()]
@@ -926,7 +926,7 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet
926926
$serializer = new Serializer(
927927
[
928928
new ArrayDenormalizer(),
929-
new DateTimeNormalizer(),
929+
new DateTimeNormalizer([]),
930930
new DateTimeZoneNormalizer(),
931931
new DataUriNormalizer(),
932932
new UidNormalizer(),

0 commit comments

Comments
 (0)
0