Issue #2926508 by Wim Leers, mpdonadio, joelstein, tacituseu, jhedstrom, effulgentsia, tedbow, mradcliffe, borisson_, dawehner, larowlan: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API
							parent
							
								
									c1f7ffae3c
								
							
						
					
					
						commit
						c8dc57eede
					
				| 
						 | 
				
			
			@ -26,7 +26,7 @@ class Timestamp extends IntegerData implements DateTimeInterface {
 | 
			
		|||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function getDateTime() {
 | 
			
		||||
    if ($this->value) {
 | 
			
		||||
    if (isset($this->value)) {
 | 
			
		||||
      return DrupalDateTime::createFromTimestamp($this->value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -136,7 +136,7 @@ class EntityTestDateonlyTest extends EntityTestResourceTestBase {
 | 
			
		|||
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' is invalid for the format 'Y-m-d'\n";
 | 
			
		||||
      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\" (date-only).";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when value is not a valid date.
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +146,7 @@ class EntityTestDateonlyTest extends EntityTestResourceTestBase {
 | 
			
		|||
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' did not parse properly for the format 'Y-m-d'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
 | 
			
		||||
      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\" (date-only).";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ class EntityTestDatetimeTest extends EntityTestResourceTestBase {
 | 
			
		|||
    return parent::getExpectedNormalizedEntity() + [
 | 
			
		||||
      static::$fieldName => [
 | 
			
		||||
        [
 | 
			
		||||
          'value' => $this->entity->get(static::$fieldName)->value,
 | 
			
		||||
          'value' => '2017-03-02T07:02:00+11:00',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +103,24 @@ class EntityTestDatetimeTest extends EntityTestResourceTestBase {
 | 
			
		|||
    return parent::getNormalizedPostEntity() + [
 | 
			
		||||
      static::$fieldName => [
 | 
			
		||||
        [
 | 
			
		||||
          'value' => static::$dateString . '+00:00',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getNormalizedPatchEntity() {
 | 
			
		||||
    return parent::getNormalizedPostEntity() + [
 | 
			
		||||
      static::$fieldName => [
 | 
			
		||||
        [
 | 
			
		||||
          // Omitting the timezone is allowed, this should result in the site's
 | 
			
		||||
          // timezone being used automatically. This does not make sense, but
 | 
			
		||||
          // it's how it functioned in the past, so we explicitly test this to
 | 
			
		||||
          // guarantee backward compatibility. ::getNormalizedPostEntity() tests
 | 
			
		||||
          // the recommended case, this tests backward compatibility.
 | 
			
		||||
          'value' => static::$dateString,
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +154,7 @@ class EntityTestDatetimeTest extends EntityTestResourceTestBase {
 | 
			
		|||
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' is invalid for the format 'Y-m-d\\TH:i:s'\n";
 | 
			
		||||
      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when date format is incorrect.
 | 
			
		||||
| 
						 | 
				
			
			@ -146,9 +164,29 @@ class EntityTestDatetimeTest extends EntityTestResourceTestBase {
 | 
			
		|||
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' did not parse properly for the format 'Y-m-d\\TH:i:s'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
 | 
			
		||||
      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when date value is invalid.
 | 
			
		||||
      $normalization = $this->getNormalizedPostEntity();
 | 
			
		||||
      $value = '2017-13-55T20:02:00+00:00';
 | 
			
		||||
      $normalization[static::$fieldName][0]['value'] = $value;
 | 
			
		||||
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   *
 | 
			
		||||
   * @group legacy
 | 
			
		||||
   * @expectedDeprecation The provided datetime string format (Y-m-d\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\TH:i:sP).
 | 
			
		||||
   */
 | 
			
		||||
  public function testPatch() {
 | 
			
		||||
    return parent::testPatch();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,164 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\datetime_range\Functional\EntityResource\EntityTest;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\entity_test\Functional\Rest\EntityTestResourceTestBase;
 | 
			
		||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
 | 
			
		||||
use GuzzleHttp\RequestOptions;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the 'daterange' field's normalization.
 | 
			
		||||
 *
 | 
			
		||||
 * @group datetime_range
 | 
			
		||||
 */
 | 
			
		||||
class EntityTestDateRangeTest extends EntityTestResourceTestBase {
 | 
			
		||||
 | 
			
		||||
  use AnonResourceTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The ISO date string to use throughout the test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected static $dateString = '2017-03-01T20:02:00';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Datetime Range test field name.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected static $fieldName = 'field_daterange';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static $modules = ['datetime_range', 'entity_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function setUp() {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Add datetime_range field.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => static::$fieldName,
 | 
			
		||||
      'type' => 'daterange',
 | 
			
		||||
      'entity_type' => static::$entityTypeId,
 | 
			
		||||
      'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_ALLDAY],
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => static::$fieldName,
 | 
			
		||||
      'entity_type' => static::$entityTypeId,
 | 
			
		||||
      'bundle' => $this->entity->bundle(),
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    // Reload entity so that it has the new field.
 | 
			
		||||
    $this->entity = $this->entityStorage->load($this->entity->id());
 | 
			
		||||
    $this->entity->set(static::$fieldName, [
 | 
			
		||||
      'value' => static::$dateString,
 | 
			
		||||
      'end_value' => static::$dateString,
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->entity->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function createEntity() {
 | 
			
		||||
    $entity_test = EntityTest::create([
 | 
			
		||||
      'name' => 'Llama',
 | 
			
		||||
      'type' => static::$entityTypeId,
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity_test->setOwnerId(0);
 | 
			
		||||
    $entity_test->save();
 | 
			
		||||
 | 
			
		||||
    return $entity_test;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getExpectedNormalizedEntity() {
 | 
			
		||||
    return parent::getExpectedNormalizedEntity() + [
 | 
			
		||||
      static::$fieldName => [
 | 
			
		||||
        [
 | 
			
		||||
          'value' => '2017-03-02T07:02:00+11:00',
 | 
			
		||||
          'end_value' => '2017-03-02T07:02:00+11:00',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getNormalizedPostEntity() {
 | 
			
		||||
    return parent::getNormalizedPostEntity() + [
 | 
			
		||||
      static::$fieldName => [
 | 
			
		||||
        [
 | 
			
		||||
          'value' => '2017-03-01T20:02:00+00:00',
 | 
			
		||||
          'end_value' => '2017-03-01T20:02:00+00:00',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
 | 
			
		||||
    parent::assertNormalizationEdgeCases($method, $url, $request_options);
 | 
			
		||||
 | 
			
		||||
    if ($this->entity->getEntityType()->hasKey('bundle')) {
 | 
			
		||||
      $fieldName = static::$fieldName;
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when 'value' data type is incorrect.
 | 
			
		||||
      $normalization = $this->getNormalizedPostEntity();
 | 
			
		||||
      $normalization[static::$fieldName][0]['value'] = [
 | 
			
		||||
        '2017', '03', '01', '21', '53', '00',
 | 
			
		||||
      ];
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when 'end_value' is not specified.
 | 
			
		||||
      $normalization = $this->getNormalizedPostEntity();
 | 
			
		||||
      unset($normalization[static::$fieldName][0]['end_value']);
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.end_value: This value should not be null.\n";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when 'end_value' data type is incorrect.
 | 
			
		||||
      $normalization = $this->getNormalizedPostEntity();
 | 
			
		||||
      $normalization[static::$fieldName][0]['end_value'] = [
 | 
			
		||||
        '2017', '03', '01', '21', '53', '00',
 | 
			
		||||
      ];
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // DX: 422 when end date value is invalid.
 | 
			
		||||
      $normalization = $this->getNormalizedPostEntity();
 | 
			
		||||
      $value = '2017-13-55T20:02:00+00:00';
 | 
			
		||||
      $normalization[static::$fieldName][0]['end_value'] = $value;
 | 
			
		||||
 | 
			
		||||
      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
 | 
			
		||||
      $response = $this->request($method, $url, $request_options);
 | 
			
		||||
      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
 | 
			
		||||
      $this->assertResourceErrorResponse(422, $message, $response);
 | 
			
		||||
 | 
			
		||||
      // @todo Expand in https://www.drupal.org/project/drupal/issues/2847041.
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,15 +4,18 @@ namespace Drupal\hal\Normalizer;
 | 
			
		|||
 | 
			
		||||
use Drupal\Core\Field\FieldItemInterface;
 | 
			
		||||
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
 | 
			
		||||
use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts values for TimestampItem to and from common formats for hal.
 | 
			
		||||
 *
 | 
			
		||||
 * Overrides FieldItemNormalizer to
 | 
			
		||||
 * - during normalization, add the 'format' key to assist consumers
 | 
			
		||||
 * - during denormalization, use
 | 
			
		||||
 *   \Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
 */
 | 
			
		||||
class TimestampItemNormalizer extends FieldItemNormalizer {
 | 
			
		||||
 | 
			
		||||
  use TimeStampItemNormalizerTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -22,8 +25,21 @@ class TimestampItemNormalizer extends FieldItemNormalizer {
 | 
			
		|||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
 | 
			
		||||
    $normalized = parent::normalizedFieldValues($field_item, $format, $context);
 | 
			
		||||
    return $this->processNormalizedValues($normalized);
 | 
			
		||||
    return parent::normalizedFieldValues($field_item, $format, $context) + [
 | 
			
		||||
      // 'format' is not a property on Timestamp objects. This is present to
 | 
			
		||||
      // assist consumers of this data.
 | 
			
		||||
      'format' => \DateTime::RFC3339,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function constructValue($data, $context) {
 | 
			
		||||
    if (!empty($data['format'])) {
 | 
			
		||||
      $context['datetime_allowed_formats'] = [$data['format']];
 | 
			
		||||
    }
 | 
			
		||||
    return ['value' => $this->serializer->denormalize($data['value'], Timestamp::class, NULL, $context)];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,8 @@ trait BcTimestampNormalizerUnixTestTrait {
 | 
			
		|||
    // \Drupal\serialization\Normalizer\TimestampItemNormalizer will produce.
 | 
			
		||||
    $date = new \DateTime();
 | 
			
		||||
    $date->setTimestamp($timestamp);
 | 
			
		||||
    // Per \Drupal\Core\TypedData\Plugin\DataType\Timestamp::getDateTime(), they
 | 
			
		||||
    // default to string representations in the UTC timezone.
 | 
			
		||||
    $date->setTimezone(new \DateTimeZone('UTC'));
 | 
			
		||||
 | 
			
		||||
    // Format is also added to the expected return values.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,6 +58,18 @@ services:
 | 
			
		|||
      # Priority must be higher than serializer.normalizer.field_item and lower
 | 
			
		||||
      # than hal normalizers.
 | 
			
		||||
      - { name: normalizer, priority: 8, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
 | 
			
		||||
  serializer.normalizer.timestamp:
 | 
			
		||||
    class: Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
    arguments: ['@config.factory']
 | 
			
		||||
    tags:
 | 
			
		||||
      # Priority must be higher than serializer.normalizer.primitive_data.
 | 
			
		||||
      - { name: normalizer, priority: 20, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
 | 
			
		||||
  serializer.normalizer.datetimeiso8601:
 | 
			
		||||
    class: \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
 | 
			
		||||
    arguments: ['@config.factory']
 | 
			
		||||
    tags:
 | 
			
		||||
      # Priority must be higher than serializer.normalizer.primitive_data.
 | 
			
		||||
      - { name: normalizer, priority: 20 }
 | 
			
		||||
  serializer.normalizer.password_field_item:
 | 
			
		||||
      class: Drupal\serialization\Normalizer\NullNormalizer
 | 
			
		||||
      arguments: ['Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem']
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,100 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\serialization\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
 | 
			
		||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 | 
			
		||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts values for the DateTimeIso8601 data type to RFC3339.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
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.
 | 
			
		||||
    // @see https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates
 | 
			
		||||
    // @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT
 | 
			
		||||
    'date-only' => 'Y-m-d',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $supportedInterfaceOrClass = DateTimeIso8601::class;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function normalize($datetime, $format = NULL, array $context = []) {
 | 
			
		||||
    assert($datetime instanceof DateTimeIso8601);
 | 
			
		||||
    $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) {
 | 
			
		||||
        return $drupal_date_time;
 | 
			
		||||
      }
 | 
			
		||||
      return $drupal_date_time->format($this->allowedFormats['date-only']);
 | 
			
		||||
    }
 | 
			
		||||
    return parent::normalize($datetime, $format, $context);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function denormalize($data, $class, $format = NULL, array $context = []) {
 | 
			
		||||
    // @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);
 | 
			
		||||
    if ($field_definition === NULL) {
 | 
			
		||||
      throw new InvalidArgumentException('$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $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);
 | 
			
		||||
      if (!$datetime instanceof \DateTime) {
 | 
			
		||||
        return $datetime;
 | 
			
		||||
      }
 | 
			
		||||
      return $datetime->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $context['datetime_allowed_formats'] = array_diff_key($this->allowedFormats, ['date-only' => TRUE]);
 | 
			
		||||
    try {
 | 
			
		||||
      $datetime = parent::denormalize($data, $class, $format, $context);
 | 
			
		||||
    }
 | 
			
		||||
    catch (\UnexpectedValueException $e) {
 | 
			
		||||
      // If denormalization didn't work using any of the actively supported
 | 
			
		||||
      // formats, try again with the BC format too. Explicitly label it as
 | 
			
		||||
      // being deprecated and trigger a deprecation error.
 | 
			
		||||
      $using_deprecated_format = TRUE;
 | 
			
		||||
      $context['datetime_allowed_formats']['backward compatibility — deprecated'] = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
 | 
			
		||||
      $datetime = parent::denormalize($data, $class, $format, $context);
 | 
			
		||||
    }
 | 
			
		||||
    if (!$datetime instanceof \DateTime) {
 | 
			
		||||
      return $datetime;
 | 
			
		||||
    }
 | 
			
		||||
    if (isset($using_deprecated_format)) {
 | 
			
		||||
      @trigger_error('The provided datetime string format (Y-m-d\\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\\TH:i:sP).', E_USER_DEPRECATED);
 | 
			
		||||
    }
 | 
			
		||||
    $datetime->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
 | 
			
		||||
    return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\serialization\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\ConfigFactoryInterface;
 | 
			
		||||
use Drupal\Core\TypedData\Type\DateTimeInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts values for datetime objects to RFC3339 and from common formats.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Allowed datetime formats for the denormalizer.
 | 
			
		||||
   *
 | 
			
		||||
   * The list is chosen to be unambiguous and language neutral, but also common
 | 
			
		||||
   * for data interchange.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string[]
 | 
			
		||||
   *
 | 
			
		||||
   * @see http://php.net/manual/en/datetime.createfromformat.php
 | 
			
		||||
   */
 | 
			
		||||
  protected $allowedFormats = [
 | 
			
		||||
    'RFC 3339' => \DateTime::RFC3339,
 | 
			
		||||
    'ISO 8601' => \DateTime::ISO8601,
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $supportedInterfaceOrClass = DateTimeInterface::class;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The system's date configuration.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Config\ImmutableConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $systemDateConfig;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs a new DateTimeNormalizer instance.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
 | 
			
		||||
   *   A config factory for retrieving required config objects.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct(ConfigFactoryInterface $config_factory) {
 | 
			
		||||
    $this->systemDateConfig = $config_factory->get('system.date');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function normalize($datetime, $format = NULL, array $context = []) {
 | 
			
		||||
    assert($datetime instanceof DateTimeInterface);
 | 
			
		||||
    $drupal_date_time = $datetime->getDateTime();
 | 
			
		||||
    if ($drupal_date_time === NULL) {
 | 
			
		||||
      return $drupal_date_time;
 | 
			
		||||
    }
 | 
			
		||||
    return $drupal_date_time
 | 
			
		||||
      // Set an explicit timezone. Otherwise, timestamps may end up being
 | 
			
		||||
      // normalized using the user's preferred timezone. Which would result in
 | 
			
		||||
      // many variations and complex caching.
 | 
			
		||||
      // @see \Drupal\Core\Datetime\DrupalDateTime::prepareTimezone()
 | 
			
		||||
      // @see drupal_get_user_timezone()
 | 
			
		||||
      ->setTimezone($this->getNormalizationTimezone())
 | 
			
		||||
      ->format(\DateTime::RFC3339);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the timezone to be used during normalization.
 | 
			
		||||
   *
 | 
			
		||||
   * @see ::normalize
 | 
			
		||||
   *
 | 
			
		||||
   * @returns \DateTimeZone
 | 
			
		||||
   *   The timezone to use.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getNormalizationTimezone() {
 | 
			
		||||
    $default_site_timezone = $this->systemDateConfig->get('timezone.default');
 | 
			
		||||
    return new \DateTimeZone($default_site_timezone);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function denormalize($data, $class, $format = NULL, array $context = []) {
 | 
			
		||||
    // This only knows how to denormalize datetime strings and timestamps. If
 | 
			
		||||
    // something else is received, let validation constraints handle this.
 | 
			
		||||
    if (!is_string($data) && !is_numeric($data)) {
 | 
			
		||||
      return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Loop through the allowed formats and create a \DateTime from the
 | 
			
		||||
    // 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.
 | 
			
		||||
    $allowed_formats = isset($context['datetime_allowed_formats'])
 | 
			
		||||
      ? $context['datetime_allowed_formats']
 | 
			
		||||
      : $this->allowedFormats;
 | 
			
		||||
    foreach ($allowed_formats as $format) {
 | 
			
		||||
      $date = \DateTime::createFromFormat($format, $data);
 | 
			
		||||
      $errors = \DateTime::getLastErrors();
 | 
			
		||||
      if ($date !== FALSE && empty($errors['errors']) && empty($errors['warnings'])) {
 | 
			
		||||
        return $date;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $format_strings = [];
 | 
			
		||||
 | 
			
		||||
    foreach ($allowed_formats as $label => $format) {
 | 
			
		||||
      $format_strings[] = "\"$format\" ($label)";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $formats = implode(', ', $format_strings);
 | 
			
		||||
    throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data, $formats));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -4,8 +4,12 @@ namespace Drupal\serialization\Normalizer;
 | 
			
		|||
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
 | 
			
		||||
@trigger_error(__NAMESPACE__ . '\TimeStampItemNormalizerTrait is deprecated in Drupal 8.7.0 and will be removed in Drupal 9.0.0. Use \Drupal\serialization\Normalizer\TimestampNormalizer instead.', E_USER_DEPRECATED);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A trait for TimestampItem normalization functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @deprecated in 8.7.0, use \Drupal\serialization\Normalizer\TimestampNormalizer instead.
 | 
			
		||||
 */
 | 
			
		||||
trait TimeStampItemNormalizerTrait {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,15 +3,19 @@
 | 
			
		|||
namespace Drupal\serialization\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts values for TimestampItem to and from common formats.
 | 
			
		||||
 *
 | 
			
		||||
 * Overrides FieldItemNormalizer to use \Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
 *
 | 
			
		||||
 * Overrides FieldItemNormalizer to
 | 
			
		||||
 * - during normalization, add the 'format' key to assist consumers
 | 
			
		||||
 * - during denormalization, use \Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
 */
 | 
			
		||||
class TimestampItemNormalizer extends FieldItemNormalizer {
 | 
			
		||||
 | 
			
		||||
  use TimeStampItemNormalizerTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -20,21 +24,22 @@ class TimestampItemNormalizer extends FieldItemNormalizer {
 | 
			
		|||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function normalize($field_item, $format = NULL, array $context = []) {
 | 
			
		||||
    $data = parent::normalize($field_item, $format, $context);
 | 
			
		||||
 | 
			
		||||
    return $this->processNormalizedValues($data);
 | 
			
		||||
  public function normalize($object, $format = NULL, array $context = []) {
 | 
			
		||||
    return parent::normalize($object, $format, $context) + [
 | 
			
		||||
      // 'format' is not a property on Timestamp objects. This is present to
 | 
			
		||||
      // assist consumers of this data.
 | 
			
		||||
      'format' => \DateTime::RFC3339,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function denormalize($data, $class, $format = NULL, array $context = []) {
 | 
			
		||||
    if (empty($data['value'])) {
 | 
			
		||||
      throw new InvalidArgumentException('No "value" attribute present');
 | 
			
		||||
  protected function constructValue($data, $context) {
 | 
			
		||||
    if (!empty($data['format'])) {
 | 
			
		||||
      $context['datetime_allowed_formats'] = [$data['format']];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return parent::denormalize($data, $class, $format, $context);
 | 
			
		||||
    return ['value' => $this->serializer->denormalize($data['value'], Timestamp::class, NULL, $context)];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\serialization\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts values for the Timestamp data type to and from common formats.
 | 
			
		||||
 *
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * Note that \Drupal\Core\TypedData\Plugin\DataType\Timestamp::getDateTime()
 | 
			
		||||
 * explicitly sets a default timezone of UTC. This ensures the string
 | 
			
		||||
 * representation generated by DateTimeNormalizer::normalize() is also in UTC.
 | 
			
		||||
 */
 | 
			
		||||
class TimestampNormalizer extends DateTimeNormalizer {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $allowedFormats = [
 | 
			
		||||
    'UNIX timestamp' => 'U',
 | 
			
		||||
    'ISO 8601' => \DateTime::ISO8601,
 | 
			
		||||
    'RFC 3339' => \DateTime::RFC3339,
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $supportedInterfaceOrClass = Timestamp::class;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function getNormalizationTimezone() {
 | 
			
		||||
    return new \DateTimeZone('UTC');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function denormalize($data, $class, $format = NULL, array $context = []) {
 | 
			
		||||
    $denormalized = parent::denormalize($data, $class, $format, $context);
 | 
			
		||||
    return $denormalized->getTimestamp();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,283 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\serialization\Unit\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\ConfigFactoryInterface;
 | 
			
		||||
use Drupal\Core\Config\ImmutableConfig;
 | 
			
		||||
use Drupal\Core\Datetime\DrupalDateTime;
 | 
			
		||||
use Drupal\Core\Field\FieldDefinitionInterface;
 | 
			
		||||
use Drupal\Core\Field\FieldItemBase;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
 | 
			
		||||
use Drupal\Core\TypedData\Type\DateTimeInterface;
 | 
			
		||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 | 
			
		||||
use Drupal\serialization\Normalizer\DateTimeIso8601Normalizer;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Unit test coverage for the "datetime_iso8601" @DataType.
 | 
			
		||||
 *
 | 
			
		||||
 * @coversDefaultClass \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
 | 
			
		||||
 * @group serialization
 | 
			
		||||
 * @see \Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601
 | 
			
		||||
 * @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATE
 | 
			
		||||
 */
 | 
			
		||||
class DateTimeIso8601NormalizerTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The tested data type's normalizer.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
 | 
			
		||||
   */
 | 
			
		||||
  protected $normalizer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The tested data type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601
 | 
			
		||||
   */
 | 
			
		||||
  protected $data;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp() {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $system_date_config = $this->prophesize(ImmutableConfig::class);
 | 
			
		||||
    $system_date_config->get('timezone.default')
 | 
			
		||||
      ->willReturn('Australia/Sydney');
 | 
			
		||||
    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
 | 
			
		||||
    $config_factory->get('system.date')
 | 
			
		||||
      ->willReturn($system_date_config->reveal());
 | 
			
		||||
 | 
			
		||||
    $this->normalizer = new DateTimeIso8601Normalizer($config_factory->reveal());
 | 
			
		||||
    $this->data = $this->prophesize(DateTimeIso8601::class);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::supportsNormalization
 | 
			
		||||
   */
 | 
			
		||||
  public function testSupportsNormalization() {
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsNormalization($this->data->reveal()));
 | 
			
		||||
 | 
			
		||||
    $datetime = $this->prophesize(DateTimeInterface::class);
 | 
			
		||||
    $this->assertFalse($this->normalizer->supportsNormalization($datetime->reveal()));
 | 
			
		||||
 | 
			
		||||
    $integer = $this->prophesize(IntegerData::class);
 | 
			
		||||
    $this->assertFalse($this->normalizer->supportsNormalization($integer->reveal()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::supportsDenormalization
 | 
			
		||||
   */
 | 
			
		||||
  public function testSupportsDenormalization() {
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), DateTimeIso8601::class));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::normalize
 | 
			
		||||
   * @dataProvider providerTestNormalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testNormalize($parent_field_item_class, $datetime_type, $expected_format) {
 | 
			
		||||
    $formatted_string = $this->randomMachineName();
 | 
			
		||||
 | 
			
		||||
    $field_item = $this->prophesize($parent_field_item_class);
 | 
			
		||||
    if ($parent_field_item_class === DateTimeItem::class) {
 | 
			
		||||
      $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
 | 
			
		||||
      $field_storage_definition->getSetting('datetime_type')
 | 
			
		||||
        ->willReturn($datetime_type);
 | 
			
		||||
      $field_definition = $this->prophesize(FieldDefinitionInterface::class);
 | 
			
		||||
      $field_definition->getFieldStorageDefinition()
 | 
			
		||||
        ->willReturn($field_storage_definition);
 | 
			
		||||
      $field_item->getFieldDefinition()
 | 
			
		||||
        ->willReturn($field_definition);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $field_item->getFieldDefinition(Argument::any())
 | 
			
		||||
        ->shouldNotBeCalled();
 | 
			
		||||
    }
 | 
			
		||||
    $this->data->getParent()
 | 
			
		||||
      ->willReturn($field_item);
 | 
			
		||||
 | 
			
		||||
    $drupal_date_time = $this->prophesize(DateTimeIso8601NormalizerTestDrupalDateTime::class);
 | 
			
		||||
    $drupal_date_time->setTimezone(new \DateTimeZone('Australia/Sydney'))
 | 
			
		||||
      ->willReturn($drupal_date_time->reveal());
 | 
			
		||||
    $drupal_date_time->format($expected_format)
 | 
			
		||||
      ->willReturn($formatted_string);
 | 
			
		||||
    $this->data->getDateTime()
 | 
			
		||||
      ->willReturn($drupal_date_time->reveal());
 | 
			
		||||
 | 
			
		||||
    $normalized = $this->normalizer->normalize($this->data->reveal());
 | 
			
		||||
    $this->assertSame($formatted_string, $normalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::normalize
 | 
			
		||||
   * @dataProvider providerTestNormalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testNormalizeWhenNull($parent_field_item_class, $datetime_type, $expected_format) {
 | 
			
		||||
    $field_item = $this->prophesize($parent_field_item_class);
 | 
			
		||||
    if ($parent_field_item_class === DateTimeItem::class) {
 | 
			
		||||
      $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
 | 
			
		||||
      $field_storage_definition->getSetting('datetime_type')
 | 
			
		||||
        ->willReturn($datetime_type);
 | 
			
		||||
      $field_definition = $this->prophesize(FieldDefinitionInterface::class);
 | 
			
		||||
      $field_definition->getFieldStorageDefinition()
 | 
			
		||||
        ->willReturn($field_storage_definition);
 | 
			
		||||
      $field_item->getFieldDefinition()
 | 
			
		||||
        ->willReturn($field_definition);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $field_item->getFieldDefinition(Argument::any())
 | 
			
		||||
        ->shouldNotBeCalled();
 | 
			
		||||
    }
 | 
			
		||||
    $this->data->getParent()
 | 
			
		||||
      ->willReturn($field_item);
 | 
			
		||||
 | 
			
		||||
    $this->data->getDateTime()
 | 
			
		||||
      ->willReturn(NULL);
 | 
			
		||||
 | 
			
		||||
    $normalized = $this->normalizer->normalize($this->data->reveal());
 | 
			
		||||
    $this->assertNull($normalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testNormalize.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   */
 | 
			
		||||
  public function providerTestNormalize() {
 | 
			
		||||
    return [
 | 
			
		||||
      // @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATE
 | 
			
		||||
      'datetime field, configured to store only date: must be handled by DateTimeIso8601Normalizer' => [
 | 
			
		||||
        DateTimeItem::class,
 | 
			
		||||
        DateTimeItem::DATETIME_TYPE_DATE,
 | 
			
		||||
        // This expected format call proves that normalization is handled by \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer::normalize().
 | 
			
		||||
        'Y-m-d',
 | 
			
		||||
      ],
 | 
			
		||||
      // @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATETIME
 | 
			
		||||
      'datetime field, configured to store date and time; must be handled by the parent normalizer' => [
 | 
			
		||||
        DateTimeItem::class,
 | 
			
		||||
        DateTimeItem::DATETIME_TYPE_DATETIME,
 | 
			
		||||
        \DateTime::RFC3339,
 | 
			
		||||
      ],
 | 
			
		||||
      'non-datetime field; must be handled by the parent normalizer' => [
 | 
			
		||||
        FieldItemBase::class,
 | 
			
		||||
        NULL,
 | 
			
		||||
        \DateTime::RFC3339,
 | 
			
		||||
      ],
 | 
			
		||||
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with good data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   * @dataProvider providerTestDenormalizeValidFormats
 | 
			
		||||
   */
 | 
			
		||||
  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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testDenormalizeValidFormats.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   */
 | 
			
		||||
  public function providerTestDenormalizeValidFormats() {
 | 
			
		||||
    $data = [];
 | 
			
		||||
    $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 the date+time deprecated format.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   * @group legacy
 | 
			
		||||
   * @expectedDeprecation The provided datetime string format (Y-m-d\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\TH:i:sP).
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeDateAndTimeDeprecatedFormat() {
 | 
			
		||||
    $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()]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with bad data for the date-only case.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   */
 | 
			
		||||
  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';
 | 
			
		||||
 | 
			
		||||
    $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 "on a rainy day" is not in an accepted format: "Y-m-d\TH:i:sP" (RFC 3339), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:s" (backward compatibility — deprecated).');
 | 
			
		||||
 | 
			
		||||
    $normalized = 'on a rainy day';
 | 
			
		||||
 | 
			
		||||
    $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()]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with incomplete serialization context.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeNoTargetInstanceOrFieldDefinitionException() {
 | 
			
		||||
    $this->setExpectedException(InvalidArgumentException::class, '$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
 | 
			
		||||
    $this->normalizer->denormalize('', DateTimeIso8601::class, NULL, []);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Note: Prophecy does not support magic methods. By subclassing and specifying
 | 
			
		||||
 * an explicit method, Prophecy works.
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/338
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/34
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/80
 | 
			
		||||
 */
 | 
			
		||||
class DateTimeIso8601NormalizerTestDrupalDateTime extends DrupalDateTime {
 | 
			
		||||
 | 
			
		||||
  public function setTimezone(\DateTimeZone $timezone) {
 | 
			
		||||
    parent::setTimezone($timezone);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,192 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\serialization\Unit\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\ConfigFactoryInterface;
 | 
			
		||||
use Drupal\Core\Config\ImmutableConfig;
 | 
			
		||||
use Drupal\Core\Datetime\DrupalDateTime;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
 | 
			
		||||
use Drupal\Core\TypedData\Type\DateTimeInterface;
 | 
			
		||||
use Drupal\serialization\Normalizer\DateTimeNormalizer;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Unit test coverage for @DataTypes implementing DateTimeInterface.
 | 
			
		||||
 *
 | 
			
		||||
 * @group serialization
 | 
			
		||||
 * @coversDefaultClass \Drupal\serialization\Normalizer\DateTimeNormalizer
 | 
			
		||||
 * @see \Drupal\Core\TypedData\Type\DateTimeInterface
 | 
			
		||||
 */
 | 
			
		||||
class DateTimeNormalizerTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The tested data type's normalizer.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\serialization\Normalizer\DateTimeNormalizer
 | 
			
		||||
   */
 | 
			
		||||
  protected $normalizer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The tested data type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\TypedData\Type\DateTimeInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $data;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp() {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $system_date_config = $this->prophesize(ImmutableConfig::class);
 | 
			
		||||
    $system_date_config->get('timezone.default')
 | 
			
		||||
      ->willReturn('Australia/Sydney');
 | 
			
		||||
    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
 | 
			
		||||
    $config_factory->get('system.date')
 | 
			
		||||
      ->willReturn($system_date_config->reveal());
 | 
			
		||||
 | 
			
		||||
    $this->normalizer = new DateTimeNormalizer($config_factory->reveal());
 | 
			
		||||
    $this->data = $this->prophesize(DateTimeInterface::class);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::supportsNormalization
 | 
			
		||||
   */
 | 
			
		||||
  public function testSupportsNormalization() {
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsNormalization($this->data->reveal()));
 | 
			
		||||
 | 
			
		||||
    $datetimeiso8601 = $this->prophesize(DateTimeIso8601::class);
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsNormalization($datetimeiso8601->reveal()));
 | 
			
		||||
 | 
			
		||||
    $integer = $this->prophesize(IntegerData::class);
 | 
			
		||||
    $this->assertFalse($this->normalizer->supportsNormalization($integer->reveal()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::supportsDenormalization
 | 
			
		||||
   */
 | 
			
		||||
  public function testSupportsDenormalization() {
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), DateTimeInterface::class));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::normalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testNormalize() {
 | 
			
		||||
    $random_rfc_3339_string = $this->randomMachineName();
 | 
			
		||||
 | 
			
		||||
    $drupal_date_time = $this->prophesize(DateTimeNormalizerTestDrupalDateTime::class);
 | 
			
		||||
    $drupal_date_time->setTimezone(new \DateTimeZone('Australia/Sydney'))
 | 
			
		||||
      ->willReturn($drupal_date_time->reveal());
 | 
			
		||||
    $drupal_date_time->format(\DateTime::RFC3339)
 | 
			
		||||
      ->willReturn($random_rfc_3339_string);
 | 
			
		||||
 | 
			
		||||
    $this->data->getDateTime()
 | 
			
		||||
      ->willReturn($drupal_date_time->reveal());
 | 
			
		||||
 | 
			
		||||
    $normalized = $this->normalizer->normalize($this->data->reveal());
 | 
			
		||||
    $this->assertSame($random_rfc_3339_string, $normalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::normalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testNormalizeWhenNull() {
 | 
			
		||||
    $this->data->getDateTime()
 | 
			
		||||
      ->willReturn(NULL);
 | 
			
		||||
 | 
			
		||||
    $normalized = $this->normalizer->normalize($this->data->reveal());
 | 
			
		||||
    $this->assertNull($normalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with good data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   * @dataProvider providerTestDenormalizeValidFormats
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeValidFormats($normalized, $expected) {
 | 
			
		||||
    $denormalized = $this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, []);
 | 
			
		||||
    $this->assertSame(0, $denormalized->getTimestamp() - $expected->getTimestamp());
 | 
			
		||||
    $this->assertEquals($expected, $denormalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testDenormalizeValidFormats.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   */
 | 
			
		||||
  public function providerTestDenormalizeValidFormats() {
 | 
			
		||||
    $data = [];
 | 
			
		||||
 | 
			
		||||
    $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', new \DateTimeImmutable('2016-11-06T09:02:00+00:00')];
 | 
			
		||||
    $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', new \DateTimeImmutable('2016-11-06T09:02:00+01:00')];
 | 
			
		||||
    $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', new \DateTimeImmutable('2016-11-06T09:02:00-06:00')];
 | 
			
		||||
 | 
			
		||||
    $data['ISO8601'] = ['2016-11-06T09:02:00+0000', new \DateTimeImmutable('2016-11-06T09:02:00+00:00')];
 | 
			
		||||
    $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', new \DateTimeImmutable('2016-11-06T09:02:00+01:00')];
 | 
			
		||||
    $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', new \DateTimeImmutable('2016-11-06T09:02:00-06:00')];
 | 
			
		||||
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with a user supplied format.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   * @dataProvider providerTestDenormalizeUserFormats
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeUserFormats($normalized, $format, $expected) {
 | 
			
		||||
    $denormalized = $this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, ['datetime_allowed_formats' => [$format]]);
 | 
			
		||||
    $this->assertSame(0, $denormalized->getTimestamp() - $expected->getTimestamp());
 | 
			
		||||
    $this->assertEquals($expected, $denormalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testDenormalizeUserFormats.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   */
 | 
			
		||||
  public function providerTestDenormalizeUserFormats() {
 | 
			
		||||
    $data = [];
 | 
			
		||||
 | 
			
		||||
    $data['Y/m/d H:i:s P'] = ['2016/11/06 09:02:00 +00:00', 'Y/m/d H:i:s P', new \DateTimeImmutable('2016-11-06T09:02:00+00:00')];
 | 
			
		||||
    $data['H:i:s Y/m/d P'] = ['09:02:00 2016/11/06  +01:00', 'H:i:s Y/m/d P', new \DateTimeImmutable('2016-11-06T09:02:00+01:00')];
 | 
			
		||||
    $data['Y/m/d H:i:s'] = ['09:02:00 2016/11/06', 'H:i:s Y/m/d', new \DateTimeImmutable('2016-11-06T09:02:00+11:00')];
 | 
			
		||||
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with bad data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeException() {
 | 
			
		||||
    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" 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/06 09:02am GMT';
 | 
			
		||||
 | 
			
		||||
    $this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, []);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Note: Prophecy does not support magic methods. By subclassing and specifying
 | 
			
		||||
 * an explicit method, Prophecy works.
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/338
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/34
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/80
 | 
			
		||||
 */
 | 
			
		||||
class DateTimeNormalizerTestDrupalDateTime extends DrupalDateTime {
 | 
			
		||||
 | 
			
		||||
  public function setTimezone(\DateTimeZone $timezone) {
 | 
			
		||||
    parent::setTimezone($timezone);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\serialization\Unit\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that TimeStampItemNormalizerTrait throws a deprecation error.
 | 
			
		||||
 *
 | 
			
		||||
 * @group serialization
 | 
			
		||||
 * @group legacy
 | 
			
		||||
 * @coversDefaultClass \Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait
 | 
			
		||||
 */
 | 
			
		||||
class TimeStampItemNormalizerTraitDeprecatedTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that TimeStampItemNormalizerTrait throws a deprecation error.
 | 
			
		||||
   *
 | 
			
		||||
   * @expectedDeprecation Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait is deprecated in Drupal 8.7.0 and will be removed in Drupal 9.0.0. Use \Drupal\serialization\Normalizer\TimestampNormalizer instead.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDeprecated() {
 | 
			
		||||
    $test = new TimeStampItemNormalizerTraitDeprecatedTestClass();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\serialization\Unit\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For testing that TimeStampItemNormalizerTrait throws a deprecation error.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\Tests\serialization\Unit\Normalizer\TimeStampItemNormalizerTraitDeprecatedTest
 | 
			
		||||
 */
 | 
			
		||||
class TimeStampItemNormalizerTraitDeprecatedTestClass {
 | 
			
		||||
  use TimeStampItemNormalizerTrait;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,21 +5,21 @@ namespace Drupal\Tests\serialization\Unit\Normalizer;
 | 
			
		|||
use Drupal\Core\Field\Plugin\Field\FieldType\CreatedItem;
 | 
			
		||||
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 | 
			
		||||
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
 | 
			
		||||
use Drupal\Core\TypedData\DataDefinitionInterface;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 | 
			
		||||
use Drupal\serialization\Normalizer\TimestampItemNormalizer;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Serializer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests that entities can be serialized to supported core formats.
 | 
			
		||||
 * Tests that TimestampItem (de)normalization uses Timestamp (de)normalization.
 | 
			
		||||
 *
 | 
			
		||||
 * @group serialization
 | 
			
		||||
 * @coversDefaultClass \Drupal\serialization\Normalizer\TimestampItemNormalizer
 | 
			
		||||
 * @see \Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
 */
 | 
			
		||||
class TimestampItemNormalizerTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  use InternalTypedDataTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var \Drupal\serialization\Normalizer\TimestampItemNormalizer
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -68,90 +68,78 @@ class TimestampItemNormalizerTest extends UnitTestCase {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the normalize function.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::normalize
 | 
			
		||||
   * @see \Drupal\Tests\serialization\Unit\Normalizer\TimestampNormalizerTest
 | 
			
		||||
   */
 | 
			
		||||
  public function testNormalize() {
 | 
			
		||||
    $expected = ['value' => '2016-11-06T09:02:00+00:00', 'format' => \DateTime::RFC3339];
 | 
			
		||||
 | 
			
		||||
    // Mock TimestampItem @FieldType, which contains a Timestamp @DataType,
 | 
			
		||||
    // which has a DataDefinition.
 | 
			
		||||
    $data_definition = $this->prophesize(DataDefinitionInterface::class);
 | 
			
		||||
    $data_definition->isInternal()
 | 
			
		||||
      ->willReturn(FALSE)
 | 
			
		||||
      ->shouldBeCalled();
 | 
			
		||||
    $timestamp = $this->prophesize(Timestamp::class);
 | 
			
		||||
    $timestamp->getDataDefinition()
 | 
			
		||||
      ->willReturn($data_definition->reveal())
 | 
			
		||||
      ->shouldBeCalled();
 | 
			
		||||
    $timestamp = $timestamp->reveal();
 | 
			
		||||
    $timestamp_item = $this->createTimestampItemProphecy();
 | 
			
		||||
    $timestamp_item->getIterator()
 | 
			
		||||
      ->willReturn(new \ArrayIterator(['value' => 1478422920]));
 | 
			
		||||
 | 
			
		||||
    $value_property = $this->getTypedDataProperty(FALSE);
 | 
			
		||||
    $timestamp_item->getProperties(TRUE)
 | 
			
		||||
      ->willReturn(['value' => $value_property])
 | 
			
		||||
      ->willReturn(['value' => $timestamp])
 | 
			
		||||
      ->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
    // Mock Serializer service, to assert that the Timestamp @DataType
 | 
			
		||||
    // normalizer would be called.
 | 
			
		||||
    $timestamp_datetype_normalization = $this->randomMachineName();
 | 
			
		||||
    $serializer_prophecy = $this->prophesize(Serializer::class);
 | 
			
		||||
 | 
			
		||||
    $serializer_prophecy->normalize($value_property, NULL, [])
 | 
			
		||||
      ->willReturn(1478422920)
 | 
			
		||||
    // This is where \Drupal\serialization\Normalizer\TimestampNormalizer would
 | 
			
		||||
    // be called.
 | 
			
		||||
    $serializer_prophecy->normalize($timestamp, NULL, [])
 | 
			
		||||
      ->willReturn($timestamp_datetype_normalization)
 | 
			
		||||
      ->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
    $this->normalizer->setSerializer($serializer_prophecy->reveal());
 | 
			
		||||
 | 
			
		||||
    $normalized = $this->normalizer->normalize($timestamp_item->reveal());
 | 
			
		||||
    $this->assertSame($expected, $normalized);
 | 
			
		||||
    $this->assertSame(['value' => $timestamp_datetype_normalization, 'format' => \DateTime::RFC3339], $normalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with good data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   * @dataProvider providerTestDenormalizeValidFormats
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeValidFormats($value, $expected) {
 | 
			
		||||
    $normalized = ['value' => $value];
 | 
			
		||||
  public function testDenormalize() {
 | 
			
		||||
    $timestamp_item_normalization = [
 | 
			
		||||
      'value' => $this->randomMachineName(),
 | 
			
		||||
      'format' => \DateTime::RFC3339,
 | 
			
		||||
    ];
 | 
			
		||||
    $timestamp_data_denormalization = $this->randomMachineName();
 | 
			
		||||
 | 
			
		||||
    $timestamp_item = $this->createTimestampItemProphecy();
 | 
			
		||||
    // The field item should be set with the expected timestamp.
 | 
			
		||||
    $timestamp_item->setValue(['value' => $expected])
 | 
			
		||||
    // The field item should get the Timestamp @DataType denormalization set as
 | 
			
		||||
    // a value, in FieldItemNormalizer::denormalize().
 | 
			
		||||
    $timestamp_item->setValue(['value' => $timestamp_data_denormalization])
 | 
			
		||||
      ->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
    $context = ['target_instance' => $timestamp_item->reveal()];
 | 
			
		||||
    $context = [
 | 
			
		||||
      'target_instance' => $timestamp_item->reveal(),
 | 
			
		||||
      'datetime_allowed_formats' => [\DateTime::RFC3339],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $denormalized = $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
 | 
			
		||||
    // Mock Serializer service, to assert that the Timestamp @DataType
 | 
			
		||||
    // denormalizer would be called.
 | 
			
		||||
    $serializer_prophecy = $this->prophesize(Serializer::class);
 | 
			
		||||
    // This is where \Drupal\serialization\Normalizer\TimestampNormalizer would
 | 
			
		||||
    // be called.
 | 
			
		||||
    $serializer_prophecy->denormalize($timestamp_item_normalization['value'], Timestamp::class, NULL, $context)
 | 
			
		||||
      ->willReturn($timestamp_data_denormalization)
 | 
			
		||||
      ->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
    $this->normalizer->setSerializer($serializer_prophecy->reveal());
 | 
			
		||||
 | 
			
		||||
    $denormalized = $this->normalizer->denormalize($timestamp_item_normalization, TimestampItem::class, NULL, $context);
 | 
			
		||||
    $this->assertTrue($denormalized instanceof TimestampItem);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testDenormalizeValidFormats.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   */
 | 
			
		||||
  public function providerTestDenormalizeValidFormats() {
 | 
			
		||||
    $expected_stamp = 1478422920;
 | 
			
		||||
 | 
			
		||||
    $data = [];
 | 
			
		||||
 | 
			
		||||
    $data['U'] = [$expected_stamp, $expected_stamp];
 | 
			
		||||
    $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', $expected_stamp];
 | 
			
		||||
    $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', $expected_stamp - 1 * 3600];
 | 
			
		||||
    $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', $expected_stamp + 6 * 3600];
 | 
			
		||||
 | 
			
		||||
    $data['ISO8601'] = ['2016-11-06T09:02:00+0000', $expected_stamp];
 | 
			
		||||
    $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', $expected_stamp - 1 * 3600];
 | 
			
		||||
    $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', $expected_stamp + 6 * 3600];
 | 
			
		||||
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with bad data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeException() {
 | 
			
		||||
    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
 | 
			
		||||
 | 
			
		||||
    $context = ['target_instance' => $this->createTimestampItemProphecy()->reveal()];
 | 
			
		||||
 | 
			
		||||
    $normalized = ['value' => '2016/11/06 09:02am GMT'];
 | 
			
		||||
    $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a TimestampItem prophecy.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,147 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\serialization\Unit\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Config\ConfigFactoryInterface;
 | 
			
		||||
use Drupal\Core\Datetime\DrupalDateTime;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
 | 
			
		||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 | 
			
		||||
use Drupal\Core\TypedData\Type\DateTimeInterface;
 | 
			
		||||
use Drupal\serialization\Normalizer\TimestampNormalizer;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Unit test coverage for the "Timestamp" @DataType.
 | 
			
		||||
 *
 | 
			
		||||
 * @group serialization
 | 
			
		||||
 * @coversDefaultClass \Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
 * @see \Drupal\Core\TypedData\Plugin\DataType\Timestamp
 | 
			
		||||
 */
 | 
			
		||||
class TimestampNormalizerTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The tested data type's normalizer.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\serialization\Normalizer\TimestampNormalizer
 | 
			
		||||
   */
 | 
			
		||||
  protected $normalizer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The tested data type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem
 | 
			
		||||
   */
 | 
			
		||||
  protected $data;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp() {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->normalizer = new TimestampNormalizer($this->prophesize(ConfigFactoryInterface::class)->reveal());
 | 
			
		||||
    $this->data = $this->prophesize(Timestamp::class);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::supportsNormalization
 | 
			
		||||
   */
 | 
			
		||||
  public function testSupportsNormalization() {
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsNormalization($this->data->reveal()));
 | 
			
		||||
 | 
			
		||||
    $integer = $this->prophesize(IntegerData::class);
 | 
			
		||||
    $this->assertFalse($this->normalizer->supportsNormalization($integer->reveal()));
 | 
			
		||||
 | 
			
		||||
    $datetime = $this->prophesize(DateTimeInterface::class);
 | 
			
		||||
    $this->assertFalse($this->normalizer->supportsNormalization($datetime->reveal()));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::supportsDenormalization
 | 
			
		||||
   */
 | 
			
		||||
  public function testSupportsDenormalization() {
 | 
			
		||||
    $this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), Timestamp::class));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::normalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testNormalize() {
 | 
			
		||||
    $random_rfc_3339_string = $this->randomMachineName();
 | 
			
		||||
 | 
			
		||||
    $drupal_date_time = $this->prophesize(TimestampNormalizerTestDrupalDateTime::class);
 | 
			
		||||
    $drupal_date_time->setTimezone(new \DateTimeZone('UTC'))
 | 
			
		||||
      ->willReturn($drupal_date_time->reveal());
 | 
			
		||||
    $drupal_date_time->format(\DateTime::RFC3339)
 | 
			
		||||
      ->willReturn($random_rfc_3339_string);
 | 
			
		||||
 | 
			
		||||
    $this->data->getDateTime()
 | 
			
		||||
      ->willReturn($drupal_date_time->reveal());
 | 
			
		||||
 | 
			
		||||
    $normalized = $this->normalizer->normalize($this->data->reveal());
 | 
			
		||||
    $this->assertSame($random_rfc_3339_string, $normalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with good data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   * @dataProvider providerTestDenormalizeValidFormats
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeValidFormats($normalized, $expected) {
 | 
			
		||||
    $denormalized = $this->normalizer->denormalize($normalized, Timestamp::class, NULL, []);
 | 
			
		||||
    $this->assertSame($expected, $denormalized);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testDenormalizeValidFormats.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   */
 | 
			
		||||
  public function providerTestDenormalizeValidFormats() {
 | 
			
		||||
    $expected_stamp = 1478422920;
 | 
			
		||||
 | 
			
		||||
    $data = [];
 | 
			
		||||
 | 
			
		||||
    $data['U'] = [$expected_stamp, $expected_stamp];
 | 
			
		||||
    $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', $expected_stamp];
 | 
			
		||||
    $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', $expected_stamp - 1 * 3600];
 | 
			
		||||
    $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', $expected_stamp + 6 * 3600];
 | 
			
		||||
 | 
			
		||||
    $data['ISO8601'] = ['2016-11-06T09:02:00+0000', $expected_stamp];
 | 
			
		||||
    $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', $expected_stamp - 1 * 3600];
 | 
			
		||||
    $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', $expected_stamp + 6 * 3600];
 | 
			
		||||
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the denormalize function with bad data.
 | 
			
		||||
   *
 | 
			
		||||
   * @covers ::denormalize
 | 
			
		||||
   */
 | 
			
		||||
  public function testDenormalizeException() {
 | 
			
		||||
    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
 | 
			
		||||
 | 
			
		||||
    $normalized = '2016/11/06 09:02am GMT';
 | 
			
		||||
 | 
			
		||||
    $this->normalizer->denormalize($normalized, Timestamp::class, NULL, []);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Note: Prophecy does not support magic methods. By subclassing and specifying
 | 
			
		||||
 * an explicit method, Prophecy works.
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/338
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/34
 | 
			
		||||
 * @see https://github.com/phpspec/prophecy/issues/80
 | 
			
		||||
 */
 | 
			
		||||
class TimestampNormalizerTestDrupalDateTime extends DrupalDateTime {
 | 
			
		||||
 | 
			
		||||
  public function setTimezone(\DateTimeZone $timezone) {
 | 
			
		||||
    parent::setTimezone($timezone);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue