diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index 5958aab067..960782bc46 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -23,18 +23,22 @@ use Symfony\Component\Serializer\Exception\UnexpectedValueException; 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; } /** @@ -49,6 +53,11 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface } $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); } @@ -69,9 +78,15 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface 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; @@ -89,7 +104,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface } 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); } @@ -126,4 +141,15 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface 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); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 6d622bbcc0..43cb67c968 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -52,6 +52,32 @@ class DateTimeNormalizerTest extends TestCase $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". @@ -76,6 +102,17 @@ class DateTimeNormalizerTest extends TestCase $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|'))); @@ -83,6 +120,45 @@ class DateTimeNormalizerTest extends TestCase $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 */