Skip to content

Commit 28e6f4a

Browse files
improvement(DateTime): handle multiple deserialization formats and timezones (#39)
1 parent 1ee50dd commit 28e6f4a

File tree

7 files changed

+87
-16
lines changed

7 files changed

+87
-16
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
# 2.6.0
4+
* (De)serialization now accepts timezones, and lists of deserialization formats
5+
36
# 2.5.1
47

58
* Generalized the improvement on arrays with primitive types to generate more efficient code.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"require": {
1717
"php": "^8.0",
1818
"ext-json": "*",
19-
"liip/metadata-parser": "^1.1",
19+
"liip/metadata-parser": "^1.2",
2020
"pnz/json-exception": "^1.0",
2121
"symfony/filesystem": "^4.4 || ^5.0 || ^6.0",
2222
"symfony/finder": "^4.4 || ^5.0 || ^6.0",

src/DeserializerGenerator.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -205,19 +205,16 @@ private function generateInnerCodeForFieldType(
205205

206206
switch ($type) {
207207
case $type instanceof PropertyTypeArray:
208-
if ($type->isCollection()) {
208+
if ($type->isTraversable()) {
209209
return $this->generateCodeForArrayCollection($propertyMetadata, $type, $arrayPath, $modelPropertyPath, $stack);
210210
}
211211

212212
return $this->generateCodeForArray($type, $arrayPath, $modelPropertyPath, $stack);
213213

214214
case $type instanceof PropertyTypeDateTime:
215-
if (null !== $type->getZone()) {
216-
throw new \RuntimeException('Timezone support is not implemented');
217-
}
218-
$format = $type->getDeserializeFormat() ?: $type->getFormat();
219-
if (null !== $format) {
220-
return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $format);
215+
$formats = $type->getDeserializeFormats() ?: (\is_string($type->getFormat()) ? [$type->getFormat()] : $type->getFormat());
216+
if (null !== $formats) {
217+
return $this->templating->renderAssignDateTimeFromFormat($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath, $formats, $type->getZone());
221218
}
222219

223220
return $this->templating->renderAssignDateTimeToField($type->isImmutable(), (string) $modelPropertyPath, (string) $arrayPath);

src/SerializerGenerator.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,7 @@ private function generateCodeForFieldType(
181181
): string {
182182
switch ($type) {
183183
case $type instanceof PropertyTypeDateTime:
184-
if (null !== $type->getZone()) {
185-
throw new \RuntimeException('Timezone support is not implemented');
186-
}
187-
$dateFormat = $type->getFormat() ?: \DateTime::ISO8601;
184+
$dateFormat = $type->getFormat() ?: \DateTimeInterface::ISO8601;
188185

189186
return $this->templating->renderAssign(
190187
$fieldPath,

src/Template/Deserialization.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,18 @@ function {{functionName}}(array {{jsonPath}}): {{className}}
6262
EOT;
6363

6464
private const TMPL_ASSIGN_DATETIME_FROM_FORMAT = <<<'EOT'
65-
{{modelPath}} = \DateTime::createFromFormat('{{format}}', {{jsonPath}});
65+
{{date}} = false;
66+
foreach([{{formats|join(', ')}}] as {{format}}) {
67+
if (({{date}} = \DateTime::createFromFormat({{format}}, {{jsonPath}}, {{timezone}}))) {
68+
{{modelPath}} = {{date}};
69+
break;
70+
}
71+
}
72+
73+
if (false === {{date}}) {
74+
throw new \Exception('Invalid datetime string '.({{jsonPath}}).' matches none of the deserialization formats: '.{{formatsError}});
75+
}
76+
unset({{format}}, {{date}});
6677

6778
EOT;
6879

@@ -72,7 +83,18 @@ function {{functionName}}(array {{jsonPath}}): {{className}}
7283
EOT;
7384

7485
private const TMPL_ASSIGN_DATETIME_IMMUTABLE_FROM_FORMAT = <<<'EOT'
75-
{{modelPath}} = \DateTimeImmutable::createFromFormat('{{format}}', {{jsonPath}});
86+
{{date}} = false;
87+
foreach([{{formats|join(', ')}}] as {{format}}) {
88+
if (({{date}} = \DateTimeImmutable::createFromFormat({{format}}, {{jsonPath}}, {{timezone}}))) {
89+
{{modelPath}} = {{date}};
90+
break;
91+
}
92+
}
93+
94+
if (false === {{date}}) {
95+
throw new \Exception('Invalid datetime string '.({{jsonPath}}).' matches none of the deserialization formats: '.{{formatsError}});
96+
}
97+
unset({{format}}, {{date}});
7698

7799
EOT;
78100

@@ -190,14 +212,36 @@ public function renderAssignDateTimeToField(bool $immutable, string $modelPath,
190212
]);
191213
}
192214

193-
public function renderAssignDateTimeFromFormat(bool $immutable, string $modelPath, string $jsonPath, string $format): string
215+
/**
216+
* @param list<string>|string $formats
217+
*/
218+
public function renderAssignDateTimeFromFormat(bool $immutable, string $modelPath, string $jsonPath, array|string $formats, string $timezone = null): string
194219
{
220+
if (\is_string($formats)) {
221+
@trigger_error('Passing a string for argument $formats is deprecated, please pass an array of strings instead', \E_USER_DEPRECATED);
222+
$formats = [$formats];
223+
}
224+
195225
$template = $immutable ? self::TMPL_ASSIGN_DATETIME_IMMUTABLE_FROM_FORMAT : self::TMPL_ASSIGN_DATETIME_FROM_FORMAT;
226+
$formats = array_map(
227+
static fn (string $f): string => var_export($f, true),
228+
$formats
229+
);
230+
$formatsError = var_export(implode(',', $formats), true);
231+
$dateVariable = preg_replace_callback(
232+
'/([^a-zA-Z]+|\d+)([a-zA-Z])/',
233+
static fn ($match): string => (ctype_digit($match[1]) ? $match[1] : null).mb_strtoupper($match[2]),
234+
$modelPath
235+
);
196236

197237
return $this->render($template, [
198238
'modelPath' => $modelPath,
199239
'jsonPath' => $jsonPath,
200-
'format' => $format,
240+
'formats' => $formats,
241+
'formatsError' => $formatsError,
242+
'format' => '$'.lcfirst($dateVariable).'Format',
243+
'date' => '$'.lcfirst($dateVariable),
244+
'timezone' => $timezone ? 'new \DateTimeZone('.var_export($timezone, true).')' : 'null',
201245
]);
202246
}
203247

tests/Fixtures/Model.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ class Model
4545
*/
4646
public $dateWithFormat;
4747

48+
/**
49+
* @Serializer\Type("DateTime<'Y-m-d', '', 'd/m/Y'>")
50+
*
51+
* @var \DateTime
52+
*/
53+
public $dateWithOneDeserializationFormat;
54+
55+
/**
56+
* @Serializer\Type("DateTime<'Y-m-d', '', ['m/d/Y', 'Y-m-d']>")
57+
*
58+
* @var \DateTime
59+
*/
60+
public $dateWithMultipleDeserializationFormats;
61+
62+
/**
63+
* @Serializer\Type("DateTime<'Y-m-d', '+0600', '!d/m/Y'>")
64+
*
65+
* @var \DateTime
66+
*/
67+
public $dateWithTimezone;
68+
4869
/**
4970
* @Serializer\Type("DateTimeImmutable")
5071
*

tests/Unit/DeserializerGeneratorTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public function testNested(): void
5252
'nested_field' => ['nested_string' => 'nested'],
5353
'date' => '2018-08-03T00:00:00+02:00',
5454
'date_with_format' => '2018-08-04',
55+
'date_with_one_deserialization_format' => '15/05/2019',
56+
'date_with_multiple_deserialization_formats' => '05/16/2019',
57+
'date_with_timezone' => '04/08/2018', // Defined timezone offset is +6 hours, so bringing it back to UTC removes a day
5558
'date_immutable' => '2016-06-01T00:00:00+02:00',
5659
];
5760

@@ -67,6 +70,12 @@ public function testNested(): void
6770
self::assertSame('2018-08-03', $model->date->format('Y-m-d'));
6871
self::assertInstanceOf(\DateTime::class, $model->dateWithFormat);
6972
self::assertSame('2018-08-04', $model->dateWithFormat->format('Y-m-d'));
73+
self::assertInstanceOf(\DateTime::class, $model->dateWithOneDeserializationFormat);
74+
self::assertSame('2019-05-15', $model->dateWithOneDeserializationFormat->format('Y-m-d'));
75+
self::assertInstanceOf(\DateTime::class, $model->dateWithMultipleDeserializationFormats);
76+
self::assertSame('2019-05-16', $model->dateWithMultipleDeserializationFormats->format('Y-m-d'));
77+
self::assertInstanceOf(\DateTime::class, $model->dateWithTimezone);
78+
self::assertSame('2018-08-03', $model->dateWithTimezone->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d'));
7079
self::assertInstanceOf(\DateTimeImmutable::class, $model->dateImmutable);
7180
self::assertSame('2016-06-01', $model->dateImmutable->format('Y-m-d'));
7281
}

0 commit comments

Comments
 (0)