Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 39809e2

Browse files
committed
[Serializer] Allow to cast certain DateTime formats to int/float
1 parent 0d9562f commit 39809e2

File tree

6 files changed

+160
-17
lines changed

6 files changed

+160
-17
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 = [
@@ -40,6 +42,10 @@ final class DateTimeNormalizer implements NormalizerInterface, DenormalizerInter
4042
public function __construct(array $defaultContext = [])
4143
{
4244
$this->setDefaultContext($defaultContext);
45+
46+
if (!isset($this->defaultContext[self::TIMESTAMP_CAST_KEY])) {
47+
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);
48+
}
4349
}
4450

4551
public function setDefaultContext(array $defaultContext): void
@@ -59,7 +65,7 @@ public function getSupportedTypes(?string $format): array
5965
/**
6066
* @throws InvalidArgumentException
6167
*/
62-
public function normalize(mixed $object, string $format = null, array $context = []): string
68+
public function normalize(mixed $object, string $format = null, array $context = []): int|float|string
6369
{
6470
if (!$object instanceof \DateTimeInterface) {
6571
throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".');
@@ -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/DateTimeNormalizerTest.php

Lines changed: 113 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,14 @@ 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+
* @group legacy
415+
*/
416+
public function testDeprecationWithoutTimestampCastInConstructor()
417+
{
418+
$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.');
419+
420+
new DateTimeNormalizer();
421+
}
323422
}

src/Symfony/Component/Serializer/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
],
1818
"require": {
1919
"php": ">=8.2",
20+
"symfony/deprecation-contracts": "^2.5|^3",
2021
"symfony/polyfill-ctype": "~1.8"
2122
},
2223
"require-dev": {

0 commit comments

Comments
 (0)