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

8.7.x
Francesco Placella 2019-02-16 00:13:43 +01:00
parent c1f7ffae3c
commit c8dc57eede
18 changed files with 1244 additions and 85 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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.
}
}
}

View File

@ -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)];
}
}

View File

@ -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.

View File

@ -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']

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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)];
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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.
*

View File

@ -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);
}
}