Skip to content

Commit

Permalink
feature #22444 [Serializer] DateTimeNormalizer: allow to provide time…
Browse files Browse the repository at this point in the history
…zone (ogizanagi)

This PR was merged into the 3.4 branch.

Discussion
----------

[Serializer] DateTimeNormalizer: allow to provide timezone

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | N/A
| License       | MIT
| Doc PR        | N/A

My own use-case was for denormalization of a csv file provided by a third-party. The datetime format inside does not contain any timezone information, and won't change, but it's established to be UTC (or at least consistent).

So by providing the new `datetime_timezone` option, the returned instance of `\DateTime(Interface)` will properly be set with the expected timezone. (In case the format already supports the time offset, the provided timezone is ignored in favor of the one parsed by the `\DateTime` object)

Regarding normalization, the expected behavior of this feature is to consistently return the same time offset.

Commits
-------

c10a780 [Serializer] DateTimeNormalizer: allow to provide timezone
  • Loading branch information
fabpot committed Jun 14, 2017
2 parents a03e194 + c10a780 commit 1ed41b5
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 4 deletions.
34 changes: 30 additions & 4 deletions src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php
Expand Up @@ -23,18 +23,22 @@
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
{
const FORMAT_KEY = 'datetime_format';
const TIMEZONE_KEY = 'datetime_timezone';

/**
* @var string
*/
private $format;
private $timezone;

/**
* @param string $format
* @param string $format
* @param \DateTimeZone|null $timezone
*/
public function __construct($format = \DateTime::RFC3339)
public function __construct($format = \DateTime::RFC3339, \DateTimeZone $timezone = null)
{
$this->format = $format;
$this->timezone = $timezone;
}

/**
Expand All @@ -49,6 +53,11 @@ public function normalize($object, $format = null, array $context = array())
}

$format = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
$timezone = $this->getTimezone($context);

if (null !== $timezone) {
$object = (new \DateTimeImmutable('@'.$object->getTimestamp()))->setTimezone($timezone);
}

return $object->format($format);
}
Expand All @@ -69,9 +78,15 @@ public function supportsNormalization($data, $format = null)
public function denormalize($data, $class, $format = null, array $context = array())
{
$dateTimeFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : null;
$timezone = $this->getTimezone($context);

if (null !== $dateTimeFormat) {
$object = \DateTime::class === $class ? \DateTime::createFromFormat($dateTimeFormat, $data) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data);
if (null === $timezone && PHP_VERSION_ID < 50600) {
// https://bugs.php.net/bug.php?id=68669
$object = \DateTime::class === $class ? \DateTime::createFromFormat($dateTimeFormat, $data) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data);
} else {
$object = \DateTime::class === $class ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone);
}

if (false !== $object) {
return $object;
Expand All @@ -89,7 +104,7 @@ public function denormalize($data, $class, $format = null, array $context = arra
}

try {
return \DateTime::class === $class ? new \DateTime($data) : new \DateTimeImmutable($data);
return \DateTime::class === $class ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
} catch (\Exception $e) {
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
}
Expand Down Expand Up @@ -126,4 +141,15 @@ private function formatDateTimeErrors(array $errors)

return $formattedErrors;
}

private function getTimezone(array $context)
{
$dateTimeZone = array_key_exists(self::TIMEZONE_KEY, $context) ? $context[self::TIMEZONE_KEY] : $this->timezone;

if (null === $dateTimeZone) {
return null;
}

return $dateTimeZone instanceof \DateTimeZone ? $dateTimeZone : new \DateTimeZone($dateTimeZone);
}
}
Expand Up @@ -52,6 +52,32 @@ public function testNormalizeUsingFormatPassedInConstructor()
$this->assertEquals('16', (new DateTimeNormalizer('y'))->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC'))));
}

public function testNormalizeUsingTimeZonePassedInConstructor()
{
$normalizer = new DateTimeNormalizer(\DateTime::RFC3339, new \DateTimeZone('Japan'));

$this->assertSame('2016-12-01T00:00:00+09:00', $normalizer->normalize(new \DateTime('2016/12/01', new \DateTimeZone('Japan'))));
$this->assertSame('2016-12-01T09:00:00+09:00', $normalizer->normalize(new \DateTime('2016/12/01', new \DateTimeZone('UTC'))));
}

/**
* @dataProvider normalizeUsingTimeZonePassedInContextProvider
*/
public function testNormalizeUsingTimeZonePassedInContext($expected, $input, $timezone)
{
$this->assertSame($expected, $this->normalizer->normalize($input, null, array(
DateTimeNormalizer::TIMEZONE_KEY => $timezone,
)));
}

public function normalizeUsingTimeZonePassedInContextProvider()
{
yield array('2016-12-01T00:00:00+00:00', new \DateTime('2016/12/01', new \DateTimeZone('UTC')), null);
yield array('2016-12-01T00:00:00+09:00', new \DateTime('2016/12/01', new \DateTimeZone('Japan')), new \DateTimeZone('Japan'));
yield array('2016-12-01T09:00:00+09:00', new \DateTime('2016/12/01', new \DateTimeZone('UTC')), new \DateTimeZone('Japan'));
yield array('2016-12-01T09:00:00+09:00', new \DateTimeImmutable('2016/12/01', new \DateTimeZone('UTC')), new \DateTimeZone('Japan'));
}

/**
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
* @expectedExceptionMessage The object must implement the "\DateTimeInterface".
Expand All @@ -76,13 +102,63 @@ public function testDenormalize()
$this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTime::class));
}

public function testDenormalizeUsingTimezonePassedInConstructor()
{
$timezone = new \DateTimeZone('Japan');
$expected = new \DateTime('2016/12/01 17:35:00', $timezone);
$normalizer = new DateTimeNormalizer(null, $timezone);

$this->assertEquals($expected, $normalizer->denormalize('2016.12.01 17:35:00', \DateTime::class, null, array(
DateTimeNormalizer::FORMAT_KEY => 'Y.m.d H:i:s',
)));
}

public function testDenormalizeUsingFormatPassedInContext()
{
$this->assertEquals(new \DateTimeImmutable('2016/01/01'), $this->normalizer->denormalize('2016.01.01', \DateTimeInterface::class, null, array(DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|')));
$this->assertEquals(new \DateTimeImmutable('2016/01/01'), $this->normalizer->denormalize('2016.01.01', \DateTimeImmutable::class, null, array(DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|')));
$this->assertEquals(new \DateTime('2016/01/01'), $this->normalizer->denormalize('2016.01.01', \DateTime::class, null, array(DateTimeNormalizer::FORMAT_KEY => 'Y.m.d|')));
}

/**
* @dataProvider denormalizeUsingTimezonePassedInContextProvider
*/
public function testDenormalizeUsingTimezonePassedInContext($input, $expected, $timezone, $format = null)
{
$actual = $this->normalizer->denormalize($input, \DateTimeInterface::class, null, array(
DateTimeNormalizer::TIMEZONE_KEY => $timezone,
DateTimeNormalizer::FORMAT_KEY => $format,
));

$this->assertEquals($expected, $actual);
}

public function denormalizeUsingTimezonePassedInContextProvider()
{
yield 'with timezone' => array(
'2016/12/01 17:35:00',
new \DateTimeImmutable('2016/12/01 17:35:00', new \DateTimeZone('Japan')),
new \DateTimeZone('Japan'),
);
yield 'with timezone as string' => array(
'2016/12/01 17:35:00',
new \DateTimeImmutable('2016/12/01 17:35:00', new \DateTimeZone('Japan')),
'Japan',
);
yield 'with format without timezone information' => array(
'2016.12.01 17:35:00',
new \DateTimeImmutable('2016/12/01 17:35:00', new \DateTimeZone('Japan')),
new \DateTimeZone('Japan'),
'Y.m.d H:i:s',
);
yield 'ignored with format with timezone information' => array(
'2016-12-01T17:35:00Z',
new \DateTimeImmutable('2016/12/01 17:35:00', new \DateTimeZone('UTC')),
'Europe/Paris',
\DateTime::RFC3339,
);
}

/**
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
*/
Expand Down

0 comments on commit 1ed41b5

Please sign in to comment.