summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2017-05-24 20:47:56 (GMT)
committerAlex Pott2017-05-24 20:47:56 (GMT)
commitda74f223c328f68370d7d1a2cf0acd2238a18f7e (patch)
treeb0e8914501232f1351501305ed087b2c2e96da31
parentf3b477f00698464b98cb256d7a65b0423774cfe2 (diff)
Issue #2768651 by damiankloip, Wim Leers, mpdonadio, Munavijayalakshmi, Jo Fitzgerald, jhedstrom: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX
-rw-r--r--core/modules/hal/hal.services.yml5
-rw-r--r--core/modules/hal/src/Normalizer/FieldItemNormalizer.php43
-rw-r--r--core/modules/hal/src/Normalizer/TimestampItemNormalizer.php31
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php2
-rw-r--r--core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php2
-rw-r--r--core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php43
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php11
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php85
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php7
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php7
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php15
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php7
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php7
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php19
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php7
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php11
-rw-r--r--core/modules/serialization/config/install/serialization.settings.yml7
-rw-r--r--core/modules/serialization/config/schema/serialization.schema.yml3
-rw-r--r--core/modules/serialization/serialization.install9
-rw-r--r--core/modules/serialization/serialization.services.yml6
-rw-r--r--core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php2
-rw-r--r--core/modules/serialization/src/Normalizer/EntityNormalizer.php17
-rw-r--r--core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php87
-rw-r--r--core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php42
-rw-r--r--core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php9
-rw-r--r--core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php74
-rw-r--r--core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php156
27 files changed, 633 insertions, 81 deletions
diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml
index 97fcbfb..b2c898f 100644
--- a/core/modules/hal/hal.services.yml
+++ b/core/modules/hal/hal.services.yml
@@ -17,6 +17,11 @@ services:
tags:
- { name: normalizer, priority: 20 }
arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler']
+ serializer.normalizer.timestamp_item.hal:
+ class: Drupal\hal\Normalizer\TimestampItemNormalizer
+ tags:
+ # Priority must be higher than serializer.normalizer.field_item.hal.
+ - { name: normalizer, priority: 20, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
serializer.normalizer.entity.hal:
class: Drupal\hal\Normalizer\ContentEntityNormalizer
arguments: ['@hal.link_manager', '@entity.manager', '@module_handler']
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index dc5aec9..0c2ee9e 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -21,25 +21,13 @@ class FieldItemNormalizer extends NormalizerBase {
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []) {
- $values = [];
- // We normalize each individual property, so each can do their own casting,
- // if needed.
- /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
- foreach ($field_item as $property_name => $property) {
- $values[$property_name] = $this->serializer->normalize($property, $format, $context);
- }
-
- if (isset($context['langcode'])) {
- $values['lang'] = $context['langcode'];
- }
-
// The values are wrapped in an array, and then wrapped in another array
// keyed by field name so that field items can be merged by the
// FieldNormalizer. This is necessary for the EntityReferenceItemNormalizer
// to be able to place values in the '_links' array.
$field = $field_item->getParent();
return [
- $field->getName() => [$values],
+ $field->getName() => [$this->normalizedFieldValues($field_item, $format, $context)],
];
}
@@ -86,6 +74,35 @@ class FieldItemNormalizer extends NormalizerBase {
}
/**
+ * Normalizes field values for an item.
+ *
+ * @param \Drupal\Core\Field\FieldItemInterface $field_item
+ * The field item instance.
+ * @param string|null $format
+ * The normalization format.
+ * @param array $context
+ * The context passed into the normalizer.
+ *
+ * @return array
+ * An array of field item values, keyed by property name.
+ */
+ protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
+ $denormalized = [];
+ // We normalize each individual property, so each can do their own casting,
+ // if needed.
+ /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
+ foreach ($field_item as $property_name => $property) {
+ $denormalized[$property_name] = $this->serializer->normalize($property, $format, $context);
+ }
+
+ if (isset($context['langcode'])) {
+ $denormalized['lang'] = $context['langcode'];
+ }
+
+ return $denormalized;
+ }
+
+ /**
* Get a translated version of the field item instance.
*
* To indicate that a field item applies to one translation of an entity and
diff --git a/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
new file mode 100644
index 0000000..c389e79
--- /dev/null
+++ b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\hal\Normalizer;
+
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
+use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
+
+/**
+ * Converts values for TimestampItem to and from common formats for hal.
+ */
+class TimestampItemNormalizer extends FieldItemNormalizer {
+
+ use TimeStampItemNormalizerTrait;
+
+ /**
+ * The interface or class that this Normalizer supports.
+ *
+ * @var string
+ */
+ protected $supportedInterfaceOrClass = TimestampItem::class;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
+ $normalized = parent::normalizedFieldValues($field_item, $format, $context);
+ return $this->processNormalizedValues($normalized);
+ }
+
+}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
index d4ed47c..aeac85a 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -34,11 +34,11 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
+ 'revision_timestamp',
'created',
'changed',
'promote',
'sticky',
- 'revision_timestamp',
'revision_uid',
];
diff --git a/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php b/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
index 5b752f7..2597af4 100644
--- a/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/EntityTranslationNormalizeTest.php
@@ -43,7 +43,7 @@ class EntityTranslationNormalizeTest extends NormalizerTestBase {
$node = Node::create([
'title' => $this->randomMachineName(),
- 'uid' => $user->id(),
+ 'uid' => (int) $user->id(),
'type' => $node_type->id(),
'status' => NodeInterface::PUBLISHED,
'langcode' => 'en',
diff --git a/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
new file mode 100644
index 0000000..a275f64
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional;
+
+/**
+ * Trait for ResourceTestBase subclasses formatting expected timestamp data.
+ */
+trait BcTimestampNormalizerUnixTestTrait {
+
+ /**
+ * Formats a UNIX timestamp.
+ *
+ * Depending on the 'bc_timestamp_normalizer_unix' setting. The return will be
+ * an RFC3339 date string or the same timestamp that was passed in.
+ *
+ * @param int $timestamp
+ * The timestamp value to format.
+ *
+ * @return string|int
+ * The formatted RFC3339 date string or UNIX timestamp.
+ *
+ * @see \Drupal\serialization\Normalizer\TimestampItemNormalizer
+ */
+ protected function formatExpectedTimestampItemValues($timestamp) {
+ // If the setting is enabled, just return the timestamp as-is now.
+ if ($this->config('serialization.settings')->get('bc_timestamp_normalizer_unix')) {
+ return ['value' => $timestamp];
+ }
+
+ // Otherwise, format the date string to the same that
+ // \Drupal\serialization\Normalizer\TimestampItemNormalizer will produce.
+ $date = new \DateTime();
+ $date->setTimestamp($timestamp);
+ $date->setTimezone(new \DateTimeZone('UTC'));
+
+ // Format is also added to the expected return values.
+ return [
+ 'value' => $date->format(\DateTime::RFC3339),
+ 'format' => \DateTime::RFC3339,
+ ];
+ }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
index 9614412..7b65c84 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -6,13 +6,14 @@ use Drupal\comment\Entity\Comment;
use Drupal\comment\Entity\CommentType;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
abstract class CommentResourceTestBase extends EntityResourceTestBase {
- use CommentTestTrait;
+ use CommentTestTrait, BcTimestampNormalizerUnixTestTrait;
/**
* {@inheritdoc}
@@ -148,14 +149,10 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
],
],
'created' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 6e34de2..277c470 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -465,9 +465,9 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// for the keys with the array order the same (it needs to match with
// identical comparison).
$expected = $this->getExpectedNormalizedEntity();
- ksort($expected);
+ static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
- ksort($actual);
+ static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
// Not only assert the normalization, also assert deserialization of the
@@ -507,12 +507,14 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
$this->assertSame($get_headers, $head_headers);
+ // BC: serialization_update_8302().
// Only run this for fieldable entities. It doesn't make sense for config
// entities as config values are already casted. They also run through the
// ConfigEntityNormalizer, which doesn't deal with fields individually.
if ($this->entity instanceof FieldableEntityInterface) {
+ // Test primitive data casting BC (strings).
$this->config('serialization.settings')->set('bc_primitives_as_strings', TRUE)->save(TRUE);
- // Rebuild the container so new config is reflected in the removal of the
+ // Rebuild the container so new config is reflected in the addition of the
// PrimitiveDataNormalizer.
$this->rebuildAll();
@@ -528,10 +530,48 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// Config entities are not affected.
// @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
$expected = static::castToString($expected);
- ksort($expected);
+ static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
- ksort($actual);
+ static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
+
+ // Reset the config value and rebuild.
+ $this->config('serialization.settings')->set('bc_primitives_as_strings', FALSE)->save(TRUE);
+ $this->rebuildAll();
+ }
+
+ // BC: serialization_update_8401().
+ // Only run this for fieldable entities. It doesn't make sense for config
+ // entities as config values always use the raw values (as per the config
+ // schema), returned directly from the ConfigEntityNormalizer, which
+ // doesn't deal with fields individually.
+ if ($this->entity instanceof FieldableEntityInterface) {
+ // Test the BC settings for timestamp values.
+ $this->config('serialization.settings')->set('bc_timestamp_normalizer_unix', TRUE)->save(TRUE);
+ // Rebuild the container so new config is reflected in the addition of the
+ // TimestampItemNormalizer.
+ $this->rebuildAll();
+
+
+ $response = $this->request('GET', $url, $request_options);
+ $this->assertResourceResponse(200, FALSE, $response);
+
+
+ // This ensures the BC layer for bc_timestamp_normalizer_unix works as
+ // expected. This method should be using
+ // ::formatExpectedTimestampValue() to generate the timestamp value. This
+ // will take into account the above config setting.
+ $expected = $this->getExpectedNormalizedEntity();
+ // Config entities are not affected.
+ // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
+ static::recursiveKSort($expected);
+ $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
+ static::recursiveKSort($actual);
+ $this->assertSame($expected, $actual);
+
+ // Reset the config value and rebuild.
+ $this->config('serialization.settings')->set('bc_timestamp_normalizer_unix', FALSE)->save(TRUE);
+ $this->rebuildAll();
}
@@ -635,6 +675,27 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
/**
+ * Recursively sorts an array by key.
+ *
+ * @param array $array
+ * An array to sort.
+ *
+ * @return array
+ * The sorted array.
+ */
+ protected static function recursiveKSort(array &$array) {
+ // First, sort the main array.
+ ksort($array);
+
+ // Then check for child arrays.
+ foreach ($array as $key => &$value) {
+ if (is_array($value)) {
+ static::recursiveKSort($value);
+ }
+ }
+ }
+
+ /**
* Tests a POST request for an entity, plus edge cases to ensure good DX.
*/
public function testPost() {
@@ -968,15 +1029,19 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 403 when sending PATCH request with read-only fields.
- foreach (static::$patchProtectedFieldNames as $field_name) {
- $normalization = $this->getNormalizedPatchEntity() + [$field_name => [['value' => $this->randomString()]]];
- $request_options[RequestOptions::BODY] = $this->serializer->serialize($normalization, static::$format);
+ // First send all fields (the "maximum normalization"). Assert the expected
+ // error message for the first PATCH-protected field. Remove that field from
+ // the normalization, send another request, assert the next PATCH-protected
+ // field error message. And so on.
+ $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
+ for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) {
+ $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i));
+ $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
$response = $this->request('PATCH', $url, $request_options);
- $this->assertResourceErrorResponse(403, "Access denied on updating field '$field_name'.", $response);
+ $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response);
}
// 200 for well-formed request that sends the maximum number of fields.
- $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
$max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames);
$request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
$response = $this->request('PATCH', $url, $request_options);
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
index 2935a00..204c7ff 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
@@ -3,11 +3,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class EntityTestResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -92,9 +95,7 @@ abstract class EntityTestResourceTestBase extends EntityResourceTestBase {
]
],
'created' => [
- [
- 'value' => (int) $this->entity->get('created')->value,
- ]
+ $this->formatExpectedTimestampItemValues((int) $this->entity->get('created')->value)
],
'user_id' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
index d030787..26d0eca 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
@@ -3,11 +3,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTestLabel;
use Drupal\entity_test\Entity\EntityTestLabel;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class EntityTestLabelResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -94,9 +97,7 @@ abstract class EntityTestLabelResourceTestBase extends EntityResourceTestBase {
],
],
'created' => [
- [
- 'value' => (int) $this->entity->get('created')->value,
- ],
+ $this->formatExpectedTimestampItemValues((int) $this->entity->get('created')->value),
],
'user_id' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php
index f15a5e0..1e79395 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Feed/FeedResourceTestBase.php
@@ -2,11 +2,14 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Feed;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase;
use Drupal\aggregator\Entity\Feed;
abstract class FeedResourceTestBase extends EntityTestResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -92,14 +95,10 @@ abstract class FeedResourceTestBase extends EntityTestResourceTestBase {
]
],
'checked' => [
- [
- 'value' => 123456789
- ]
+ $this->formatExpectedTimestampItemValues(123456789),
],
'queued' => [
- [
- 'value' => 123456789
- ]
+ $this->formatExpectedTimestampItemValues(123456789),
],
'link' => [
[
@@ -127,9 +126,7 @@ abstract class FeedResourceTestBase extends EntityTestResourceTestBase {
]
],
'modified' => [
- [
- 'value' => 123456789
- ]
+ $this->formatExpectedTimestampItemValues(123456789),
],
];
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php
index 9b72b66..f217b97 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Item/ItemResourceTestBase.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Item;
use Drupal\aggregator\Entity\Feed;
use Drupal\aggregator\Entity\Item;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
@@ -11,6 +12,8 @@ use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
*/
abstract class ItemResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -113,9 +116,7 @@ abstract class ItemResourceTestBase extends EntityResourceTestBase {
'author' => [],
'description' => [],
'timestamp' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'guid' => [],
];
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
index f907d9a..0b1f967 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/MenuLinkContent/MenuLinkContentResourceTestBase.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\rest\Functional\EntityResource\MenuLinkContent;
use Drupal\menu_link_content\Entity\MenuLinkContent;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
@@ -10,6 +11,8 @@ use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
*/
abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -161,9 +164,7 @@ abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
],
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
index 92fad1e..53367b8 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
@@ -4,11 +4,14 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Node;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class NodeResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -23,12 +26,12 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
+ 'revision_timestamp',
+ 'revision_uid',
'created',
'changed',
'promote',
'sticky',
- 'revision_timestamp',
- 'revision_uid',
];
/**
@@ -119,14 +122,10 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
],
],
'created' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'promote' => [
[
@@ -139,9 +138,7 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
],
],
'revision_timestamp' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'revision_translation_affected' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
index 698451b..7dfe46e 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -4,10 +4,13 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Term;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class TermResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -107,9 +110,7 @@ abstract class TermResourceTestBase extends EntityResourceTestBase {
],
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
index 0195dda..25dcf75 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
@@ -3,12 +3,15 @@
namespace Drupal\Tests\rest\Functional\EntityResource\User;
use Drupal\Core\Url;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
abstract class UserResourceTestBase extends EntityResourceTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* {@inheritdoc}
*/
@@ -98,14 +101,10 @@ abstract class UserResourceTestBase extends EntityResourceTestBase {
],
],
'created' => [
- [
- 'value' => 123456789,
- ],
+ $this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
- [
- 'value' => $this->entity->getChangedTime(),
- ],
+ $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'default_langcode' => [
[
diff --git a/core/modules/serialization/config/install/serialization.settings.yml b/core/modules/serialization/config/install/serialization.settings.yml
index 39c583a..a2758c2 100644
--- a/core/modules/serialization/config/install/serialization.settings.yml
+++ b/core/modules/serialization/config/install/serialization.settings.yml
@@ -2,3 +2,10 @@
# this was usually returned from database storage. A primitive data normalizer
# has been introduced to get the casted value instead.
bc_primitives_as_strings: false
+# Before Drupal 8.3, timestamps were always returned as Unix timestamps, which
+# are not a universal format for interchange. Now, RFC3339 timestamps are
+# returned. New Drupal installations opt out from this by default (hence this
+# is set to false), existing installations opt in to it.
+# @see serialization_update_8301()
+# @see https://www.drupal.org/node/2768651
+bc_timestamp_normalizer_unix: false
diff --git a/core/modules/serialization/config/schema/serialization.schema.yml b/core/modules/serialization/config/schema/serialization.schema.yml
index 0fdd2da..1d44b3d 100644
--- a/core/modules/serialization/config/schema/serialization.schema.yml
+++ b/core/modules/serialization/config/schema/serialization.schema.yml
@@ -5,3 +5,6 @@ serialization.settings:
bc_primitives_as_strings:
type: boolean
label: 'Whether to retain pre Drupal 8.3 behavior of serializing all primitive items as strings.'
+ bc_timestamp_normalizer_unix:
+ type: boolean
+ label: 'Whether the pre Drupal 8.4 behavior of returning Unix timestamps instead of RFC3339 timestamps for TimestampItem fields is enabled or not.'
diff --git a/core/modules/serialization/serialization.install b/core/modules/serialization/serialization.install
index 6620b44..afc3311 100644
--- a/core/modules/serialization/serialization.install
+++ b/core/modules/serialization/serialization.install
@@ -46,3 +46,12 @@ function serialization_update_8302() {
return t('The REST API will no longer output all values as strings. Integers/booleans will be used where appropriate. If your site depends on these value being strings, <a href="https://www.drupal.org/node/2837696">read the change record to learn how to enable the BC mode.</a>');
}
+
+/**
+ * Enable BC for timestamp formatting: continue to return UNIX timestamps.
+ */
+function serialization_update_8401() {
+ $config_factory = \Drupal::configFactory();
+ $serialization_settings = $config_factory->getEditable('serialization.settings');
+ $serialization_settings->set('bc_timestamp_normalizer_unix', TRUE)->save(TRUE);
+}
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 3607182..dca6094 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -52,6 +52,12 @@ services:
# Priority must be higher than serialization.normalizer.field but less
# than hal field normalizer.
- { name: normalizer, priority: 9 }
+ serializer.normalizer.timestamp_item:
+ class: Drupal\serialization\Normalizer\TimestampItemNormalizer
+ tags:
+ # 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.password_field_item:
class: Drupal\serialization\Normalizer\NullNormalizer
arguments: ['Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem']
diff --git a/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php b/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
index 79d3a90..4cb794f 100644
--- a/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
+++ b/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
@@ -46,7 +46,7 @@ class BcConfigSubscriber implements EventSubscriberInterface {
$saved_config = $event->getConfig();
if ($saved_config->getName() === 'serialization.settings') {
- if ($event->isChanged('bc_primitives_as_strings')) {
+ if ($event->isChanged('bc_primitives_as_strings') || $event->isChanged('bc_timestamp_normalizer_unix')) {
$this->kernel->invalidateContainer();
}
}
diff --git a/core/modules/serialization/src/Normalizer/EntityNormalizer.php b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
index 84baec9..4103d1d 100644
--- a/core/modules/serialization/src/Normalizer/EntityNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
@@ -40,13 +40,20 @@ class EntityNormalizer extends ComplexDataNormalizer implements DenormalizerInte
// The bundle property will be required to denormalize a bundleable
// fieldable entity.
- if ($entity_type_definition->hasKey('bundle') && $entity_type_definition->isSubclassOf(FieldableEntityInterface::class)) {
- // Get an array containing the bundle only. This also remove the bundle
- // key from the $data array.
- $bundle_data = $this->extractBundleData($data, $entity_type_definition);
+ if ($entity_type_definition->isSubclassOf(FieldableEntityInterface::class)) {
+ // Extract bundle data to pass into entity creation if the entity type uses
+ // bundles.
+ if ($entity_type_definition->hasKey('bundle')) {
+ // Get an array containing the bundle only. This also remove the bundle
+ // key from the $data array.
+ $create_params = $this->extractBundleData($data, $entity_type_definition);
+ }
+ else {
+ $create_params = [];
+ }
// Create the entity from bundle data only, then apply field values after.
- $entity = $this->entityManager->getStorage($entity_type_id)->create($bundle_data);
+ $entity = $this->entityManager->getStorage($entity_type_id)->create($create_params);
$this->denormalizeFieldData($data, $entity, $format, $context);
}
diff --git a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
new file mode 100644
index 0000000..78f6030
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * A trait for TimestampItem normalization functionality.
+ */
+trait TimeStampItemNormalizerTrait {
+
+ /**
+ * Allowed timestamps formats for the denormalizer.
+ *
+ * The denormalizer allows deserialization to timestamps from three
+ * different formats. Validation of the input data and creation of the
+ * numerical timestamp value is handled with \DateTime::createFromFormat().
+ * 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 = [
+ 'UNIX timestamp' => 'U',
+ 'ISO 8601' => \DateTime::ISO8601,
+ 'RFC 3339' => \DateTime::RFC3339,
+ ];
+
+ /**
+ * Processes normalized timestamp values to add a formatted date and format.
+ *
+ * @param array $normalized
+ * The normalized field data to process.
+ * @return array
+ * The processed data.
+ */
+ protected function processNormalizedValues(array $normalized) {
+ // Use a RFC 3339 timestamp with the time zone set to UTC to replace the
+ // timestamp value.
+ $date = new \DateTime();
+ $date->setTimestamp($normalized['value']);
+ $date->setTimezone(new \DateTimeZone('UTC'));
+ $normalized['value'] = $date->format(\DateTime::RFC3339);
+ // 'format' is not a property on TimestampItem fields. This is present to
+ // assist consumers of this data.
+ $normalized['format'] = \DateTime::RFC3339;
+
+ return $normalized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function constructValue($data, $context) {
+ // Loop through the allowed formats and create a TimestampItem 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.
+ $timezone = new \DateTimeZone('UTC');
+
+ // First check for a provided format.
+ if (!empty($data['format']) && in_array($data['format'], $this->allowedFormats)) {
+ $date = \DateTime::createFromFormat($data['format'], $data['value'], $timezone);
+ return ['value' => $date->getTimestamp()];
+ }
+ // Otherwise, loop through formats.
+ else {
+ foreach ($this->allowedFormats as $format) {
+ if (($date = \DateTime::createFromFormat($format, $data['value'], $timezone)) !== FALSE) {
+ return ['value' => $date->getTimestamp()];
+ }
+ }
+ }
+
+ $format_strings = [];
+
+ foreach ($this->allowedFormats 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['value'], $formats));
+ }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
new file mode 100644
index 0000000..704b22f
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+
+/**
+ * Converts values for TimestampItem to and from common formats.
+ */
+class TimestampItemNormalizer extends FieldItemNormalizer {
+
+ use TimeStampItemNormalizerTrait;
+
+ /**
+ * The interface or class that this Normalizer supports.
+ *
+ * @var string
+ */
+ protected $supportedInterfaceOrClass = TimestampItem::class;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function normalize($field_item, $format = NULL, array $context = []) {
+ $data = parent::normalize($field_item, $format, $context);
+
+ return $this->processNormalizedValues($data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function denormalize($data, $class, $format = NULL, array $context = []) {
+ if (empty($data['value'])) {
+ throw new InvalidArgumentException('No "value" attribute present');
+ }
+
+ return parent::denormalize($data, $class, $format, $context);
+ }
+
+}
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 71fa6b3..ebd4e03 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\serialization\Kernel;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
/**
* Tests that entities can be serialized to supported core formats.
@@ -12,6 +13,8 @@ use Drupal\entity_test\Entity\EntityTestMulRev;
*/
class EntitySerializationTest extends NormalizerTestBase {
+ use BcTimestampNormalizerUnixTestTrait;
+
/**
* Modules to install.
*
@@ -106,7 +109,7 @@ class EntitySerializationTest extends NormalizerTestBase {
['value' => 'entity_test_mulrev'],
],
'created' => [
- ['value' => $this->entity->created->value],
+ $this->formatExpectedTimestampItemValues($this->entity->created->value),
],
'user_id' => [
[
@@ -182,13 +185,15 @@ class EntitySerializationTest extends NormalizerTestBase {
// Generate the expected xml in a way that allows changes to entity property
// order.
+ $expected_created = $this->formatExpectedTimestampItemValues($this->entity->created->value);
+
$expected = [
'id' => '<id><value>' . $this->entity->id() . '</value></id>',
'uuid' => '<uuid><value>' . $this->entity->uuid() . '</value></uuid>',
'langcode' => '<langcode><value>en</value></langcode>',
'name' => '<name><value>' . $this->values['name'] . '</value></name>',
'type' => '<type><value>entity_test_mulrev</value></type>',
- 'created' => '<created><value>' . $this->entity->created->value . '</value></created>',
+ 'created' => '<created><value>' . $expected_created['value'] . '</value><format>' . $expected_created['format'] . '</format></created>',
'user_id' => '<user_id><target_id>' . $this->user->id() . '</target_id><target_type>' . $this->user->getEntityTypeId() . '</target_type><target_uuid>' . $this->user->uuid() . '</target_uuid><url>' . $this->user->url() . '</url></user_id>',
'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
index bbddbdd..689d654 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
@@ -303,6 +303,10 @@ class EntityNormalizerTest extends UnitTestCase {
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->once())
+ ->method('isSubClassOf')
+ ->with(FieldableEntityInterface::class)
+ ->willReturn(TRUE);
+ $entity_type->expects($this->once())
->method('hasKey')
->with('bundle')
->will($this->returnValue(FALSE));
@@ -314,6 +318,76 @@ class EntityNormalizerTest extends UnitTestCase {
->with('test')
->will($this->returnValue($entity_type));
+ $key_1 = $this->getMock(FieldItemListInterface::class);
+ $key_2 = $this->getMock(FieldItemListInterface::class);
+
+ $entity = $this->getMock(FieldableEntityInterface::class);
+ $entity->expects($this->at(0))
+ ->method('get')
+ ->with('key_1')
+ ->willReturn($key_1);
+ $entity->expects($this->at(1))
+ ->method('get')
+ ->with('key_2')
+ ->willReturn($key_2);
+
+ $storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
+ $storage->expects($this->once())
+ ->method('create')
+ ->with([])
+ ->will($this->returnValue($entity));
+
+ $this->entityManager->expects($this->once())
+ ->method('getStorage')
+ ->with('test')
+ ->will($this->returnValue($storage));
+
+ $this->entityManager->expects($this->never())
+ ->method('getBaseFieldDefinitions');
+
+ // Setup expectations for the serializer. This will be called for each field
+ // item.
+ $serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')
+ ->disableOriginalConstructor()
+ ->setMethods(['denormalize'])
+ ->getMock();
+ $serializer->expects($this->at(0))
+ ->method('denormalize')
+ ->with('value_1', get_class($key_1), NULL, ['target_instance' => $key_1, 'entity_type' => 'test']);
+ $serializer->expects($this->at(1))
+ ->method('denormalize')
+ ->with('value_2', get_class($key_2), NULL, ['target_instance' => $key_2, 'entity_type' => 'test']);
+
+ $this->entityNormalizer->setSerializer($serializer);
+
+ $this->assertNotNull($this->entityNormalizer->denormalize($test_data, 'Drupal\Core\Entity\ContentEntityBase', NULL, ['entity_type' => 'test']));
+ }
+
+ /**
+ * Tests the denormalize method with no bundle defined.
+ *
+ * @covers ::denormalize
+ */
+ public function testDenormalizeWithNoFieldableEntityType() {
+ $test_data = [
+ 'key_1' => 'value_1',
+ 'key_2' => 'value_2',
+ ];
+
+ $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+ $entity_type->expects($this->once())
+ ->method('isSubClassOf')
+ ->with(FieldableEntityInterface::class)
+ ->willReturn(FALSE);
+
+ $entity_type->expects($this->never())
+ ->method('getKey');
+
+ $this->entityManager->expects($this->once())
+ ->method('getDefinition')
+ ->with('test')
+ ->will($this->returnValue($entity_type));
+
$storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->once())
->method('create')
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
new file mode 100644
index 0000000..fd1fc9c
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -0,0 +1,156 @@
+<?php
+
+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\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.
+ *
+ * @group serialization
+ * @coversDefaultClass \Drupal\serialization\Normalizer\TimestampItemNormalizer
+ */
+class TimestampItemNormalizerTest extends UnitTestCase {
+
+ /**
+ * @var \Drupal\serialization\Normalizer\TimestampItemNormalizer
+ */
+ protected $normalizer;
+
+ /**
+ * The test TimestampItem.
+ *
+ * @var \Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem
+ */
+ protected $item;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->normalizer = new TimestampItemNormalizer();
+ }
+
+ /**
+ * @covers ::supportsNormalization
+ */
+ public function testSupportsNormalization() {
+ $timestamp_item = $this->createTimestampItemProphecy();
+ $this->assertTrue($this->normalizer->supportsNormalization($timestamp_item->reveal()));
+
+ $entity_ref_item = $this->prophesize(EntityReferenceItem::class);
+ $this->assertFalse($this->normalizer->supportsNormalization($entity_ref_item->reveal()));
+ }
+
+ /**
+ * @covers ::supportsDenormalization
+ */
+ public function testSupportsDenormalization() {
+ $timestamp_item = $this->createTimestampItemProphecy();
+ $this->assertTrue($this->normalizer->supportsDenormalization($timestamp_item->reveal(), TimestampItem::class));
+
+ // CreatedItem extends regular TimestampItem.
+ $timestamp_item = $this->prophesize(CreatedItem::class);
+ $this->assertTrue($this->normalizer->supportsDenormalization($timestamp_item->reveal(), TimestampItem::class));
+
+ $entity_ref_item = $this->prophesize(EntityReferenceItem::class);
+ $this->assertFalse($this->normalizer->supportsNormalization($entity_ref_item->reveal(), TimestampItem::class));
+ }
+
+ /**
+ * Tests the normalize function.
+ *
+ * @covers ::normalize
+ */
+ public function testNormalize() {
+ $expected = ['value' => '2016-11-06T09:02:00+00:00', 'format' => \DateTime::RFC3339];
+
+ $timestamp_item = $this->createTimestampItemProphecy();
+ $timestamp_item->getIterator()
+ ->willReturn(new \ArrayIterator(['value' => 1478422920]));
+
+ $serializer = new Serializer();
+ $this->normalizer->setSerializer($serializer);
+
+ $normalized = $this->normalizer->normalize($timestamp_item->reveal());
+ $this->assertSame($expected, $normalized);
+ }
+
+ /**
+ * Tests the denormalize function with good data.
+ *
+ * @covers ::denormalize
+ * @dataProvider providerTestDenormalizeValidFormats
+ */
+ public function testDenormalizeValidFormats($value, $expected) {
+ $normalized = ['value' => $value];
+
+ $timestamp_item = $this->createTimestampItemProphecy();
+ // The field item should be set with the expected timestamp.
+ $timestamp_item->setValue(['value' => $expected])
+ ->shouldBeCalled();
+
+ $context = ['target_instance' => $timestamp_item->reveal()];
+
+ $denormalized = $this->normalizer->denormalize($normalized, 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.
+ *
+ * @return \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem
+ */
+ protected function createTimestampItemProphecy() {
+ $timestamp_item = $this->prophesize(TimestampItem::class);
+ $timestamp_item->getParent()
+ ->willReturn(TRUE);
+
+ return $timestamp_item;
+ }
+
+}