.../src/Normalizer/DateTimeIso8601Normalizer.php | 46 +++++++++++---------- .../src/Normalizer/DateTimeNormalizer.php | 16 +++----- .../Normalizer/DateTimeIso8601NormalizerTest.php | 48 +++++++++++++++++----- 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php b/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php index 4515e52..a3ec201 100644 --- a/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php +++ b/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php @@ -4,18 +4,12 @@ use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; /** * Converts values for the DateTimeIso8601 data type to RFC3339. * * @internal - * - * Overrides DateTimeNormalizer in only one case: datetime fields that are - * configured to be "date only". - * - * @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATE - * - * @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416. */ class DateTimeIso8601Normalizer extends DateTimeNormalizer { @@ -23,6 +17,9 @@ class DateTimeIso8601Normalizer extends DateTimeNormalizer { * {@inheritdoc} */ protected $allowedFormats = [ + 'RFC 3339' => \DateTime::RFC3339, + 'ISO 8601' => \DateTime::ISO8601, + // @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416. // RFC3339 only covers combined date and time representations. For date-only // representations, we need to use ISO 8601. There isn't a constant on the // \DateTime class that we can use, so we have to hardcode the format. @@ -41,6 +38,7 @@ class DateTimeIso8601Normalizer extends DateTimeNormalizer { */ public function normalize($datetime, $format = NULL, array $context = []) { $field_item = $datetime->getParent(); + // @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416. if ($field_item instanceof DateTimeItem && $field_item->getFieldDefinition()->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) { $drupal_date_time = $datetime->getDateTime(); if ($drupal_date_time === NULL) { @@ -55,20 +53,26 @@ public function normalize($datetime, $format = NULL, array $context = []) { * {@inheritdoc} */ public function denormalize($data, $class, $format = NULL, array $context = []) { - $date = parent::denormalize($data, $class, $format, $context); - // Extract the year, month, and day from the object. - $ymd = $date->format('Y-m-d'); - // Rebuild the date object using the extracted year, month, and day, but - // for consistency set the time to 12:00:00 UTC upon creation for date-only - // fields. Rebuilding, instead of using the object methods, is done to - // avoid the initial date object picking up the local time and time zone - // from an input value with a missing or partial time string, and then - // rolling over to a different day when changing the object to UTC. - // @see \Drupal\Component\Datetime\DateTimePlus::setDefaultDateTime() - // @see \Drupal\datetime\Plugin\views\filter\Date::getOffset() - // @see \Drupal\datetime\DateTimeComputed::getValue() - // @see http://php.net/manual/en/datetime.createfromformat.php - return \DateTime::createFromFormat('Y-m-d\TH:i:s e', $ymd . 'T12:00:00 UTC'); + // @todo Move the date-only handling out of here in https://www.drupal.org/project/drupal/issues/2958416. + $field_definition = isset($context['target_instance']) + ? $context['target_instance']->getFieldDefinition() + : (isset($context['field_definition']) ? $context['field_definition'] : NULL); + $datetime_type = $field_definition->getSetting('datetime_type'); + $is_date_only = $datetime_type === DateTimeItem::DATETIME_TYPE_DATE; + + if ($is_date_only) { + $context['datetime_allowed_formats'] = array_intersect_key($this->allowedFormats, ['date-only' => TRUE]); + $datetime = parent::denormalize($data, $class, $format, $context); + unset($context['datetime_allowed_formats']); + return $datetime->format(DateTimeItemInterface::DATE_STORAGE_FORMAT); + } + else { + $context['datetime_allowed_formats'] = array_diff_key($this->allowedFormats, ['date-only' => TRUE]); + $datetime = parent::denormalize($data, $class, $format, $context); + unset($context['datetime_allowed_formats']); + $datetime->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE)); + return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT); + } } } diff --git a/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php b/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php index 89e4e86..e84571a 100644 --- a/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php +++ b/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php @@ -10,15 +10,6 @@ * Converts values for datetime objects to RFC3339 and from common formats. * * @internal - * - * Note that this is class can become the 'serializer.normalizer.datetime' - * service in Drupal 9.0.0 and allow the 'serializer.normalizer.datetimeiso8601' - * service to be removed in Drupal 9.0.0. That is not possible today, because - * this class also works for \Drupal\Core\TypedData\Plugin\DataType\Timestamp - * objects, but those must be ignored while the 'bc_timestamp_normalizer_unix' - * BC flag is enabled. If this class were already an active service, it'd cause - * 'timestamp' fields to not have (numeric) UNIX timestamps as normalized values - * anymore, which would break BC. */ class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface { @@ -87,7 +78,10 @@ public function denormalize($data, $class, $format = NULL, array $context = []) // input data if it matches the defined pattern. Since the formats are // unambiguous (i.e., they reference an absolute time with a defined time // zone), only one will ever match. - foreach ($this->allowedFormats as $format) { + $allowed_formats = isset($context['datetime_allowed_formats']) + ? $context['datetime_allowed_formats'] + : $this->allowedFormats; + foreach ($allowed_formats as $format) { $date = \DateTime::createFromFormat($format, $data); if ($date !== FALSE) { return $date; @@ -96,7 +90,7 @@ public function denormalize($data, $class, $format = NULL, array $context = []) $format_strings = []; - foreach ($this->allowedFormats as $label => $format) { + foreach ($allowed_formats as $label => $format) { $format_strings[] = "\"$format\" ($label)"; } diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php index b3cf2ae..ffd3f23 100644 --- a/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php +++ b/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php @@ -11,6 +11,7 @@ use Drupal\Core\TypedData\Plugin\DataType\IntegerData; use Drupal\Core\TypedData\Type\DateTimeInterface; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem; +use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; use Drupal\serialization\Normalizer\DateTimeIso8601Normalizer; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; @@ -187,12 +188,13 @@ public function providerTestNormalize() { * @covers ::denormalize * @dataProvider providerTestDenormalizeValidFormats */ - public function testDenormalizeValidFormats($normalized, $expected) { - $denormalized = $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, []); - $this->assertInstanceOf(\DateTime::class, $denormalized); - $this->assertEquals('UTC', $denormalized->getTimezone()->getName()); - $this->assertEquals('12:00:00', $denormalized->format('H:i:s')); - $this->assertEquals($expected->format(\DateTime::RFC3339), $denormalized->format(\DateTime::RFC3339)); + public function testDenormalizeValidFormats($type, $normalized, $expected) { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getSetting('datetime_type')->willReturn($type === 'date-only' ? DateTimeItem::DATETIME_TYPE_DATE : DateTimeItem::DATETIME_TYPE_DATETIME); + $denormalized = $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, [ + 'field_definition' => $field_definition->reveal(), + ]); + $this->assertSame($expected, $denormalized); } /** @@ -202,21 +204,47 @@ public function testDenormalizeValidFormats($normalized, $expected) { */ public function providerTestDenormalizeValidFormats() { $data = []; - $data['denormalized dates have the UTC timezone'] = ['2016-11-06', new \DateTimeImmutable('2016-11-06T12:00:00', new \DateTimeZone('UTC'))]; + $data['just a date'] = ['date-only', '2016-11-06', '2016-11-06']; + + $data['RFC3339'] = ['date+time', '2016-11-06T09:02:00+00:00', '2016-11-06T09:02:00']; + $data['RFC3339 +0100'] = ['date+time', '2016-11-06T09:02:00+01:00', '2016-11-06T08:02:00']; + $data['RFC3339 -0600'] = ['date+time', '2016-11-06T09:02:00-06:00', '2016-11-06T15:02:00']; + + $data['ISO8601'] = ['date+time', '2016-11-06T09:02:00+0000', '2016-11-06T09:02:00']; + $data['ISO8601 +0100'] = ['date+time', '2016-11-06T09:02:00+0100', '2016-11-06T08:02:00']; + $data['ISO8601 -0600'] = ['date+time', '2016-11-06T09:02:00-0600', '2016-11-06T15:02:00']; + return $data; } /** - * Tests the denormalize function with bad data. + * Tests the denormalize function with bad data for the date-only case. * * @covers ::denormalize */ - public function testDenormalizeException() { + public function testDenormalizeDateOnlyException() { $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06" is not in an accepted format: "Y-m-d" (date-only).'); $normalized = '2016/11/06'; - $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, []); + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getSetting('datetime_type')->willReturn(DateTimeItem::DATETIME_TYPE_DATE); + $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, ['field_definition' => $field_definition->reveal()]); + } + + /** + * Tests the denormalize function with bad data for the date+time case. + * + * @covers ::denormalize + */ + public function testDenormalizeDateAndTimeException() { + $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016-11-06T08:00:00" is not in an accepted format: "Y-m-d\TH:i:sP" (RFC 3339), "Y-m-d\TH:i:sO" (ISO 8601).'); + + $normalized = '2016-11-06T08:00:00'; + + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getSetting('datetime_type')->willReturn(DateTimeItem::DATETIME_TYPE_DATETIME); + $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, ['field_definition' => $field_definition->reveal()]); } }