first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -0,0 +1,11 @@
# Before Drupal 8.3, typed data primitive values were normalized as strings, as
# 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
@@ -0,0 +1,10 @@
serialization.settings:
type: config_object
label: 'Serialization settings'
mapping:
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.'
@@ -0,0 +1,6 @@
name: Serialization
type: module
description: 'Provides a service for (de)serializing data to/from formats such as JSON and XML.'
package: Web services
version: VERSION
core: 8.x
@@ -0,0 +1,58 @@
<?php
/**
* @file
* Update functions for the Serialization module.
*/
/**
* Implements hook_requirements().
*/
function serialization_requirements($phase) {
$requirements = [];
if ($phase == 'runtime') {
$requirements['serialization_as_strings'] = [
'title' => t('Serialized data types'),
'severity' => REQUIREMENT_INFO,
];
if (\Drupal::config('serialization.settings')->get('bc_primitives_as_strings')) {
$requirements['serialization_as_strings']['value'] = t('Enabled');
$requirements['serialization_as_strings']['description'] = t('The Serialization API is configured to output only string values for REST and other applications (instead of integers or Booleans when appropriate). <a href="https://www.drupal.org/node/2837696">Disabling this backwards compatibility mode</a> is recommended unless your sites or applications require string output.');
}
else {
$requirements['serialization_as_strings']['value'] = t('Not enabled');
$requirements['serialization_as_strings']['description'] = t('The Serialization API is configured with the recommended default and outputs typed values (integers, Booleans, or strings as appropriate) for REST and other applications. If your site or applications require string output, you can <a href="https://www.drupal.org/node/2837696">enable backwards compatibility mode</a>.');
}
}
return $requirements;
}
/**
* @see hal_update_8301()
*/
function serialization_update_8301() {
}
/**
* Add serialization.settings::bc_primitives_as_strings configuration.
*/
function serialization_update_8302() {
$config_factory = \Drupal::configFactory();
$config_factory->getEditable('serialization.settings')
->set('bc_primitives_as_strings', FALSE)
->save(TRUE);
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);
}
@@ -0,0 +1,26 @@
<?php
/**
* @file
* Provides a service for (de)serializing data to/from formats such as JSON and XML.
*/
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function serialization_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.serialization':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Serialization module provides a service for serializing and deserializing data to and from formats such as JSON and XML.') . '</p>';
$output .= '<p>' . t('Serialization is the process of converting data structures like arrays and objects into a string. This allows the data to be represented in a way that is easy to exchange and store (for example, for transmission over the Internet or for storage in a local file system). These representations can then be deserialized to get back to the original data structures.') . '</p>';
$output .= '<p>' . t('The serializer splits this process into two parts. Normalization converts an object to a normalized array structure. Encoding takes that array and converts it to a string.') . '</p>';
$output .= '<p>' . t('This module does not have a user interface. It is used by other modules which need to serialize data, such as <a href=":rest">REST</a>.', [':rest' => (\Drupal::moduleHandler()->moduleExists('rest')) ? Url::fromRoute('help.page', ['name' => 'rest'])->toString() : '#']) . '</p>';
$output .= '<p>' . t('For more information, see the <a href=":doc_url">online documentation for the Serialization module</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/serialization']) . '</p>';
return $output;
}
}
@@ -0,0 +1,119 @@
services:
serializer:
class: Symfony\Component\Serializer\Serializer
arguments: [{ }, { }]
serializer.normalizer.config_entity:
class: Drupal\serialization\Normalizer\ConfigEntityNormalizer
tags:
- { name: normalizer }
arguments: ['@entity_type.manager', '@entity_type.repository', '@entity_field.manager']
serializer.normalizer.content_entity:
class: Drupal\serialization\Normalizer\ContentEntityNormalizer
tags:
- { name: normalizer }
arguments: ['@entity_type.manager', '@entity_type.repository', '@entity_field.manager']
serializer.normalizer.entity:
class: Drupal\serialization\Normalizer\EntityNormalizer
tags:
- { name: normalizer }
arguments: ['@entity_type.manager', '@entity_type.repository', '@entity_field.manager']
serializer.normalizer.primitive_data:
class: Drupal\serialization\Normalizer\PrimitiveDataNormalizer
tags:
- { name: normalizer, priority: 5, bc: bc_primitives_as_strings, bc_config_name: 'serialization.settings' }
serializer.normalizer.complex_data:
class: Drupal\serialization\Normalizer\ComplexDataNormalizer
tags:
- { name: normalizer }
serializer.normalizer.entity_reference_field_item:
class: Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizer
tags:
# Set the priority lower than the hal entity reference field item
# normalizer, so that we do not replace that for hal_json but higher than
# this modules generic field item normalizer.
# @todo Find a better way for this in https://www.drupal.org/node/2575761.
- { name: normalizer, priority: 8 }
arguments: ['@entity.repository']
serialization.normalizer.field_item:
class: Drupal\serialization\Normalizer\FieldItemNormalizer
tags:
# Priority must be lower than serializer.normalizer.field_item.hal and any
# field type specific normalizer such as
# serializer.normalizer.entity_reference_field_item.
- { name: normalizer, priority: 6 }
serialization.normalizer.field:
class: Drupal\serialization\Normalizer\FieldNormalizer
tags:
# Priority must be lower than serializer.normalizer.field.hal.
- { name: normalizer, priority: 6 }
serializer.normalizer.list:
class: Drupal\serialization\Normalizer\ListNormalizer
tags:
# 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.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']
tags:
- { name: normalizer, priority: 20 }
serializer.normalizer.safe_string:
class: Drupal\serialization\Normalizer\MarkupNormalizer
tags:
- { name: normalizer }
serializer.normalizer.typed_data:
class: Drupal\serialization\Normalizer\TypedDataNormalizer
tags:
- { name: normalizer }
serializer.encoder.json:
class: Drupal\serialization\Encoder\JsonEncoder
tags:
- { name: encoder, format: json }
serializer.encoder.xml:
class: Drupal\serialization\Encoder\XmlEncoder
tags:
- { name: encoder, format: xml }
serializer.entity_resolver:
class: Drupal\serialization\EntityResolver\ChainEntityResolver
serializer.entity_resolver.uuid:
class: Drupal\serialization\EntityResolver\UuidResolver
tags:
- { name: entity_resolver}
arguments: ['@entity.repository']
serialization.entity_resolver.target_id:
class: Drupal\serialization\EntityResolver\TargetIdResolver
tags:
- { name: entity_resolver}
serialization.exception.default:
class: Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber
tags:
- { name: event_subscriber }
arguments: ['@serializer', '%serializer.formats%']
serialization.user_route_alter_subscriber:
class: Drupal\serialization\EventSubscriber\UserRouteAlterSubscriber
tags:
- { name: event_subscriber }
arguments: ['%serializer.formats%']
serialization.bc_config_subscriber:
class: Drupal\serialization\EventSubscriber\BcConfigSubscriber
tags:
- { name: event_subscriber }
arguments: ['@kernel']
@@ -0,0 +1,81 @@
<?php
namespace Drupal\serialization\Encoder;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder as BaseJsonEncoder;
/**
* Adds 'ajax to the supported content types of the JSON encoder'
*
* @internal
* This encoder should not be used directly. Rather, use the `serializer`
* service.
*/
class JsonEncoder extends BaseJsonEncoder implements EncoderInterface, DecoderInterface {
/**
* The formats that this Encoder supports.
*
* @var array
*/
protected static $format = ['json', 'ajax'];
/**
* {@inheritdoc}
*/
public function __construct(JsonEncode $encodingImpl = NULL, JsonDecode $decodingImpl = NULL) {
$this->encodingImpl = $encodingImpl ?: $this->getJsonEncode();
$this->decodingImpl = $decodingImpl ?: $this->getJsonDecode();
}
/**
* Instantiates a JsonEncode instance.
*
* @internal this exists to bridge Symfony 3 to Symfony 4, and can be removed
* once Drupal requires Symfony 4.2 or higher.
*/
private function getJsonEncode() {
// Encode <, >, ', &, and " for RFC4627-compliant JSON, which may also be
// embedded into HTML.
// @see \Symfony\Component\HttpFoundation\JsonResponse
$json_encoding_options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT;
$reflection = new \ReflectionClass(JsonEncode::class);
if (array_key_exists('OPTIONS', $reflection->getConstants())) {
return new JsonEncode([JsonEncode::OPTIONS => $json_encoding_options]);
}
return new JsonEncode($json_encoding_options);
}
/**
* Instantiates a JsonDecode instance.
*
* @internal this exists to bridge Symfony 3 to Symfony 4, and can be removed
* once Drupal requires Symfony 4.2 or higher.
*/
private function getJsonDecode() {
$reflection = new \ReflectionClass(JsonDecode::class);
if (array_key_exists('ASSOCIATIVE', $reflection->getConstants())) {
return new JsonDecode([JsonDecode::ASSOCIATIVE => TRUE]);
}
return new JsonDecode(TRUE);
}
/**
* {@inheritdoc}
*/
public function supportsEncoding($format) {
return in_array($format, static::$format);
}
/**
* {@inheritdoc}
*/
public function supportsDecoding($format) {
return in_array($format, static::$format);
}
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\serialization\Encoder;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\XmlEncoder as BaseXmlEncoder;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* Adds XML support for serializer.
*
* This acts as a wrapper class for Symfony's XmlEncoder so that it is not
* implementing NormalizationAwareInterface, and can be normalized externally.
*
* @internal
* This encoder should not be used directly. Rather, use the `serializer`
* service.
*/
class XmlEncoder implements SerializerAwareInterface, EncoderInterface, DecoderInterface {
use SerializerAwareTrait;
/**
* The formats that this Encoder supports.
*
* @var array
*/
protected static $format = ['xml'];
/**
* An instance of the Symfony XmlEncoder to perform the actual encoding.
*
* @var \Symfony\Component\Serializer\Encoder\XmlEncoder
*/
protected $baseEncoder;
/**
* Gets the base encoder instance.
*
* @return \Symfony\Component\Serializer\Encoder\XmlEncoder
* The base encoder.
*/
public function getBaseEncoder() {
if (!isset($this->baseEncoder)) {
$this->baseEncoder = new BaseXmlEncoder();
$this->baseEncoder->setSerializer($this->serializer);
}
return $this->baseEncoder;
}
/**
* Sets the base encoder instance.
*
* @param \Symfony\Component\Serializer\Encoder\XmlEncoder $encoder
*/
public function setBaseEncoder($encoder) {
$this->baseEncoder = $encoder;
}
/**
* {@inheritdoc}
*/
public function encode($data, $format, array $context = []) {
return $this->getBaseEncoder()->encode($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsEncoding($format) {
return in_array($format, static::$format);
}
/**
* {@inheritdoc}
*/
public function decode($data, $format, array $context = []) {
return $this->getBaseEncoder()->decode($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsDecoding($format) {
return in_array($format, static::$format);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Drupal\serialization\EntityResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Resolver delegating the entity resolution to a chain of resolvers.
*/
class ChainEntityResolver implements ChainEntityResolverInterface {
/**
* The concrete resolvers.
*
* @var \Drupal\serialization\EntityResolver\EntityResolverInterface[]
*/
protected $resolvers = [];
/**
* Constructs a ChainEntityResolver object.
*
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface[] $resolvers
* The array of concrete resolvers.
*/
public function __construct(array $resolvers = []) {
$this->resolvers = $resolvers;
}
/**
* {@inheritdoc}
*/
public function addResolver(EntityResolverInterface $resolver) {
$this->resolvers[] = $resolver;
}
/**
* {@inheritdoc}
*/
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
foreach ($this->resolvers as $resolver) {
$resolved = $resolver->resolve($normalizer, $data, $entity_type);
if (isset($resolved)) {
return $resolved;
}
}
}
}
@@ -0,0 +1,18 @@
<?php
namespace Drupal\serialization\EntityResolver;
/**
* An interface for delegating a entity resolution to a chain of resolvers.
*/
interface ChainEntityResolverInterface extends EntityResolverInterface {
/**
* Adds an entity resolver.
*
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface $resolver
* The entity resolver to add.
*/
public function addResolver(EntityResolverInterface $resolver);
}
@@ -0,0 +1,38 @@
<?php
namespace Drupal\serialization\EntityResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
interface EntityResolverInterface {
/**
* Returns the local ID of an entity referenced by serialized data.
*
* Drupal entities are loaded by and internally referenced by a local ID.
* Because different websites can use the same local ID to refer to different
* entities (e.g., node "1" can be a different node on foo.com and bar.com, or
* on example.com and staging.example.com), it is generally unsuitable for use
* in hypermedia data exchanges. Instead, UUIDs, URIs, or other globally
* unique IDs are preferred.
*
* This function takes a $data array representing partially deserialized data
* for an entity reference, and resolves it to a local entity ID. For example,
* depending on the data specification being used, $data might contain a
* 'uuid' key, a 'uri' key, a 'href' key, or some other data identifying the
* entity, and it is up to the implementor of this interface to resolve that
* appropriately for the specification being used.
*
* @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
* The Normalizer which is handling the data.
* @param array $data
* The data passed into the calling Normalizer.
* @param string $entity_type
* The type of entity being resolved; e.g., 'node' or 'user'.
*
* @return string|null
* Returns the local entity ID, if found. Otherwise, returns NULL.
*/
public function resolve(NormalizerInterface $normalizer, $data, $entity_type);
}
@@ -0,0 +1,22 @@
<?php
namespace Drupal\serialization\EntityResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Resolves entities from data that contains an entity target ID.
*/
class TargetIdResolver implements EntityResolverInterface {
/**
* {@inheritdoc}
*/
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
if (isset($data['target_id'])) {
return $data['target_id'];
}
return NULL;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\serialization\EntityResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Interface for extracting UUID from entity reference data when denormalizing.
*/
interface UuidReferenceInterface extends NormalizerInterface {
/**
* Get the uuid from the data array.
*
* @param array $data
* The data, as was passed into the Normalizer.
*
* @return string
* A UUID.
*/
public function getUuid($data);
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\serialization\EntityResolver;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Resolves entities from data that contains an entity UUID.
*/
class UuidResolver implements EntityResolverInterface {
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a UuidResolver object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityRepositoryInterface $entity_repository) {
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
// The normalizer is what knows the specification of the data being
// deserialized. If it can return a UUID from that data, and if there's an
// entity with that UUID, then return its ID.
if (($normalizer instanceof UuidReferenceInterface) && ($uuid = $normalizer->getUuid($data))) {
if ($entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid)) {
return $entity->id();
}
}
return NULL;
}
}
@@ -0,0 +1,55 @@
<?php
namespace Drupal\serialization\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\DrupalKernelInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Config event subscriber to rebuild the container when BC config is saved.
*/
class BcConfigSubscriber implements EventSubscriberInterface {
/**
* The Drupal Kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $kernel;
/**
* BcConfigSubscriber constructor.
*
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The Drupal Kernel.
*/
public function __construct(DrupalKernelInterface $kernel) {
$this->kernel = $kernel;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = 'onConfigSave';
return $events;
}
/**
* Invalidates the service container if serialization BC config gets updated.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
*/
public function onConfigSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
if ($saved_config->getName() === 'serialization.settings') {
if ($event->isChanged('bc_primitives_as_strings') || $event->isChanged('bc_timestamp_normalizer_unix')) {
$this->kernel->invalidateContainer();
}
}
}
}
@@ -0,0 +1,95 @@
<?php
namespace Drupal\serialization\EventSubscriber;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Handles default error responses in serialization formats.
*/
class DefaultExceptionSubscriber extends HttpExceptionSubscriberBase {
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* The available serialization formats.
*
* @var array
*/
protected $serializerFormats = [];
/**
* DefaultExceptionSubscriber constructor.
*
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
* The serializer service.
* @param array $serializer_formats
* The available serialization formats.
*/
public function __construct(SerializerInterface $serializer, array $serializer_formats) {
$this->serializer = $serializer;
$this->serializerFormats = $serializer_formats;
}
/**
* {@inheritdoc}
*/
protected function getHandledFormats() {
return $this->serializerFormats;
}
/**
* {@inheritdoc}
*/
protected static function getPriority() {
// This will fire after the most common HTML handler, since HTML requests
// are still more common than HTTP requests. But it has a lower priority
// than \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx(), so
// that this also handles the 'json' format. Then all serialization formats
// (::getHandledFormats()) are handled by this exception subscriber, which
// results in better consistency.
return -70;
}
/**
* Handles all 4xx errors for all serialization failures.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The event to process.
*/
public function on4xx(GetResponseForExceptionEvent $event) {
/** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
$exception = $event->getException();
$request = $event->getRequest();
$format = $request->getRequestFormat();
$content = ['message' => $exception->getMessage()];
$encoded_content = $this->serializer->serialize($content, $format);
$headers = $exception->getHeaders();
// Add the MIME type from the request to send back in the header.
$headers['Content-Type'] = $request->getMimeType($format);
// If the exception is cacheable, generate a cacheable response.
if ($exception instanceof CacheableDependencyInterface) {
$response = new CacheableResponse($encoded_content, $exception->getStatusCode(), $headers);
$response->addCacheableDependency($exception);
}
else {
$response = new Response($encoded_content, $exception->getStatusCode(), $headers);
}
$event->setResponse($response);
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\serialization\EventSubscriber;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Alters user authentication routes to support additional serialization formats.
*/
class UserRouteAlterSubscriber implements EventSubscriberInterface {
/**
* The available serialization formats.
*
* @var array
*/
protected $serializerFormats = [];
/**
* UserRouteAlterSubscriber constructor.
*
* @param array $serializer_formats
* The available serializer formats.
*/
public function __construct(array $serializer_formats) {
$this->serializerFormats = $serializer_formats;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = 'onRoutingAlterAddFormats';
return $events;
}
/**
* Adds supported formats to the user authentication HTTP routes.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The event to process.
*/
public function onRoutingAlterAddFormats(RouteBuildEvent $event) {
$route_names = [
'user.login_status.http',
'user.login.http',
'user.logout.http',
'user.pass.http',
];
$routes = $event->getRouteCollection();
foreach ($route_names as $route_name) {
if ($route = $routes->get($route_name)) {
$formats = explode('|', $route->getRequirement('_format'));
$formats = array_unique(array_merge($formats, $this->serializerFormats));
$route->setRequirement('_format', implode('|', $formats));
}
}
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\serialization\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Defines the interface for normalizers producing cacheable normalizations.
*
* @see cache
*/
interface CacheableNormalizerInterface extends NormalizerInterface {
/**
* Name of key for bubbling cacheability metadata via serialization context.
*
* @see \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
* @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::renderResponseBody()
*/
const SERIALIZATION_CONTEXT_CACHEABILITY = 'cacheability';
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
/**
* Converts the Drupal entity object structures to a normalized array.
*
* This is the default Normalizer for entities. All formats that have Encoders
* registered with the Serializer in the DIC will be normalized with this
* class unless another Normalizer is registered which supersedes it. If a
* module wants to use format-specific or class-specific normalization, then
* that module can register a new Normalizer and give it a higher priority than
* this one.
*/
class ComplexDataNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = ComplexDataInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$attributes = [];
// $object will not always match $supportedInterfaceOrClass.
// @see \Drupal\serialization\Normalizer\EntityNormalizer
// Other normalizers that extend this class may only provide $object that
// implements \Traversable.
if ($object instanceof ComplexDataInterface) {
// If there are no properties to normalize, just normalize the value.
$object = !empty($object->getProperties(TRUE))
? TypedDataInternalPropertiesHelper::getNonInternalProperties($object)
: $object->getValue();
}
/** @var \Drupal\Core\TypedData\TypedDataInterface $property */
foreach ($object as $name => $property) {
$attributes[$name] = $this->serializer->normalize($property, $format, $context);
}
return $attributes;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Normalizes/denormalizes Drupal config entity objects into an array structure.
*/
class ConfigEntityNormalizer extends EntityNormalizer {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = ConfigEntityInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
return static::getDataWithoutInternals($object->toArray());
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
return parent::denormalize(static::getDataWithoutInternals($data), $class, $format, $context);
}
/**
* Gets the given data without the internal implementation details.
*
* @param array $data
* The data that is either currently or about to be stored in configuration.
*
* @return array
* The same data, but without internals. Currently, that is only the '_core'
* key, which is reserved by Drupal core to handle complex edge cases
* correctly. Data in the '_core' key is irrelevant to clients reading
* configuration, and is not allowed to be set by clients writing
* configuration: it is for Drupal core only, and managed by Drupal core.
*
* @see https://www.drupal.org/node/2653358
*/
protected static function getDataWithoutInternals(array $data) {
return array_diff_key($data, ['_core' => TRUE]);
}
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
/**
* Normalizes/denormalizes Drupal content entities into an array structure.
*/
class ContentEntityNormalizer extends EntityNormalizer {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = ContentEntityInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($entity, $format = NULL, array $context = []) {
$context += [
'account' => NULL,
];
$attributes = [];
/** @var \Drupal\Core\Entity\Entity $entity */
foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData()) as $name => $field_items) {
if ($field_items->access('view', $context['account'])) {
$attributes[$name] = $this->serializer->normalize($field_items, $format, $context);
}
}
return $attributes;
}
}
@@ -0,0 +1,100 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Converts values for the DateTimeIso8601 data type to RFC3339.
*
* @internal
*/
class DateTimeIso8601Normalizer extends DateTimeNormalizer {
/**
* {@inheritdoc}
*/
protected $allowedFormats = [
'RFC 3339' => \DateTime::RFC3339,
'ISO 8601' => \DateTime::ISO8601,
// @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416.
// RFC3339 only covers combined date and time representations. For date-only
// representations, we need to use ISO 8601. There isn't a constant on the
// \DateTime class that we can use, so we have to hardcode the format.
// @see https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates
// @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT
'date-only' => 'Y-m-d',
];
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = DateTimeIso8601::class;
/**
* {@inheritdoc}
*/
public function normalize($datetime, $format = NULL, array $context = []) {
assert($datetime instanceof DateTimeIso8601);
$field_item = $datetime->getParent();
// @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416.
if ($field_item instanceof DateTimeItem && $field_item->getFieldDefinition()->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
$drupal_date_time = $datetime->getDateTime();
if ($drupal_date_time === NULL) {
return $drupal_date_time;
}
return $drupal_date_time->format($this->allowedFormats['date-only']);
}
return parent::normalize($datetime, $format, $context);
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
// @todo Move the date-only handling out of here in https://www.drupal.org/project/drupal/issues/2958416.
$field_definition = isset($context['target_instance'])
? $context['target_instance']->getFieldDefinition()
: (isset($context['field_definition']) ? $context['field_definition'] : NULL);
if ($field_definition === NULL) {
throw new InvalidArgumentException('$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
}
$datetime_type = $field_definition->getSetting('datetime_type');
$is_date_only = $datetime_type === DateTimeItem::DATETIME_TYPE_DATE;
if ($is_date_only) {
$context['datetime_allowed_formats'] = array_intersect_key($this->allowedFormats, ['date-only' => TRUE]);
$datetime = parent::denormalize($data, $class, $format, $context);
if (!$datetime instanceof \DateTime) {
return $datetime;
}
return $datetime->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
}
$context['datetime_allowed_formats'] = array_diff_key($this->allowedFormats, ['date-only' => TRUE]);
try {
$datetime = parent::denormalize($data, $class, $format, $context);
}
catch (\UnexpectedValueException $e) {
// If denormalization didn't work using any of the actively supported
// formats, try again with the BC format too. Explicitly label it as
// being deprecated and trigger a deprecation error.
$using_deprecated_format = TRUE;
$context['datetime_allowed_formats']['backward compatibility — deprecated'] = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
$datetime = parent::denormalize($data, $class, $format, $context);
}
if (!$datetime instanceof \DateTime) {
return $datetime;
}
if (isset($using_deprecated_format)) {
@trigger_error('The provided datetime string format (Y-m-d\\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\\TH:i:sP).', E_USER_DEPRECATED);
}
$datetime->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
}
}
@@ -0,0 +1,121 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\TypedData\Type\DateTimeInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Converts values for datetime objects to RFC3339 and from common formats.
*
* @internal
*/
class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface {
/**
* Allowed datetime formats for the denormalizer.
*
* The list is chosen to be unambiguous and language neutral, but also common
* for data interchange.
*
* @var string[]
*
* @see http://php.net/manual/en/datetime.createfromformat.php
*/
protected $allowedFormats = [
'RFC 3339' => \DateTime::RFC3339,
'ISO 8601' => \DateTime::ISO8601,
];
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = DateTimeInterface::class;
/**
* The system's date configuration.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $systemDateConfig;
/**
* Constructs a new DateTimeNormalizer instance.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A config factory for retrieving required config objects.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->systemDateConfig = $config_factory->get('system.date');
}
/**
* {@inheritdoc}
*/
public function normalize($datetime, $format = NULL, array $context = []) {
assert($datetime instanceof DateTimeInterface);
$drupal_date_time = $datetime->getDateTime();
if ($drupal_date_time === NULL) {
return $drupal_date_time;
}
return $drupal_date_time
// Set an explicit timezone. Otherwise, timestamps may end up being
// normalized using the user's preferred timezone. Which would result in
// many variations and complex caching.
// @see \Drupal\Core\Datetime\DrupalDateTime::prepareTimezone()
// @see drupal_get_user_timezone()
->setTimezone($this->getNormalizationTimezone())
->format(\DateTime::RFC3339);
}
/**
* Gets the timezone to be used during normalization.
*
* @see ::normalize
*
* @returns \DateTimeZone
* The timezone to use.
*/
protected function getNormalizationTimezone() {
$default_site_timezone = $this->systemDateConfig->get('timezone.default');
return new \DateTimeZone($default_site_timezone);
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
// This only knows how to denormalize datetime strings and timestamps. If
// something else is received, let validation constraints handle this.
if (!is_string($data) && !is_numeric($data)) {
return $data;
}
// Loop through the allowed formats and create a \DateTime from the
// input data if it matches the defined pattern. Since the formats are
// unambiguous (i.e., they reference an absolute time with a defined time
// zone), only one will ever match.
$allowed_formats = isset($context['datetime_allowed_formats'])
? $context['datetime_allowed_formats']
: $this->allowedFormats;
foreach ($allowed_formats as $format) {
$date = \DateTime::createFromFormat($format, $data);
$errors = \DateTime::getLastErrors();
if ($date !== FALSE && empty($errors['errors']) && empty($errors['warnings'])) {
return $date;
}
}
$format_strings = [];
foreach ($allowed_formats as $label => $format) {
$format_strings[] = "\"$format\" ($label)";
}
$formats = implode(', ', $format_strings);
throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data, $formats));
}
}
@@ -0,0 +1,86 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Normalizes/denormalizes Drupal entity objects into an array structure.
*/
class EntityNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
use FieldableEntityNormalizerTrait;
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = EntityInterface::class;
/**
* Constructs an EntityNormalizer object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
* The entity type repository.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeRepositoryInterface $entity_type_repository = NULL, EntityFieldManagerInterface $entity_field_manager = NULL) {
$this->entityTypeManager = $entity_type_manager;
if (!$entity_type_repository) {
@trigger_error('The entity_type.repository service must be passed to EntityNormalizer::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_type_repository = \Drupal::service('entity_type.repository');
}
$this->entityTypeRepository = $entity_type_repository;
if (!$entity_field_manager) {
@trigger_error('The entity_field.manager service must be passed to EntityNormalizer::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$entity_field_manager = \Drupal::service('entity_field.manager');
}
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
$entity_type_id = $this->determineEntityTypeId($class, $context);
$entity_type_definition = $this->getEntityTypeDefinition($entity_type_id);
// The bundle property will be required to denormalize a bundleable
// fieldable entity.
if ($entity_type_definition->entityClassImplements(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->entityTypeManager->getStorage($entity_type_id)->create($create_params);
$this->denormalizeFieldData($data, $entity, $format, $context);
}
else {
// Create the entity from all data.
$entity = $this->entityTypeManager->getStorage($entity_type_id)->create($data);
}
// Pass the names of the fields whose values can be merged.
// @todo https://www.drupal.org/node/2456257 remove this.
$entity->_restSubmittedFields = array_keys($data);
return $entity;
}
}
@@ -0,0 +1,95 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Adds the file URI to embedded file entities.
*/
class EntityReferenceFieldItemNormalizer extends FieldItemNormalizer {
use EntityReferenceFieldItemNormalizerTrait;
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = EntityReferenceItem::class;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a EntityReferenceFieldItemNormalizer object.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityRepositoryInterface $entity_repository) {
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []) {
$values = parent::normalize($field_item, $format, $context);
$this->normalizeRootReferenceValue($values, $field_item);
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if ($entity = $field_item->get('entity')->getValue()) {
$values['target_type'] = $entity->getEntityTypeId();
// Add the target entity UUID to the normalized output values.
$values['target_uuid'] = $entity->uuid();
// Add a 'url' value if there is a reference and a canonical URL. Hard
// code 'canonical' here as config entities override the default $rel
// parameter value to 'edit-form.
if ($entity->hasLinkTemplate('canonical') && !$entity->isNew() && $url = $entity->toUrl('canonical')->toString(TRUE)) {
$this->addCacheableDependency($context, $url);
$values['url'] = $url->getGeneratedUrl();
}
// @todo Remove in https://www.drupal.org/project/drupal/issues/2925520
// @see \Drupal\hal\Normalizer\FileEntityNormalizer
elseif ($entity->getEntityTypeId() === 'file') {
$values['url'] = file_create_url($entity->getFileUri());
}
}
return $values;
}
/**
* {@inheritdoc}
*/
protected function constructValue($data, $context) {
if (isset($data['target_uuid'])) {
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item */
$field_item = $context['target_instance'];
if (empty($data['target_uuid'])) {
throw new InvalidArgumentException(sprintf('If provided "target_uuid" cannot be empty for field "%s".', $data['target_type'], $data['target_uuid'], $field_item->getName()));
}
$target_type = $field_item->getFieldDefinition()->getSetting('target_type');
if (!empty($data['target_type']) && $target_type !== $data['target_type']) {
throw new UnexpectedValueException(sprintf('The field "%s" property "target_type" must be set to "%s" or omitted.', $field_item->getFieldDefinition()->getName(), $target_type));
}
if ($entity = $this->entityRepository->loadEntityByUuid($target_type, $data['target_uuid'])) {
return ['target_id' => $entity->id()] + array_intersect_key($data, $field_item->getProperties());
}
else {
// Unable to load entity by uuid.
throw new InvalidArgumentException(sprintf('No "%s" entity found with UUID "%s" for field "%s".', $data['target_type'], $data['target_uuid'], $field_item->getName()));
}
}
return parent::constructValue($data, $context);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
/**
* Converts empty reference values for entity reference items.
*/
trait EntityReferenceFieldItemNormalizerTrait {
protected function normalizeRootReferenceValue(&$values, EntityReferenceItem $field_item) {
// @todo Generalize for all tree-structured entity types.
if ($this->fieldItemReferencesTaxonomyTerm($field_item) && empty($values['target_id'])) {
$values['target_id'] = NULL;
}
}
/**
* Determines if a field item references a taxonomy term.
*
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item
*
* @return bool
*/
protected function fieldItemReferencesTaxonomyTerm(EntityReferenceItem $field_item) {
return $field_item->getFieldDefinition()->getSetting('target_type') === 'taxonomy_term';
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\FieldItemInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Denormalizes field item object structure by updating the entity field values.
*/
class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
use FieldableEntityNormalizerTrait;
use SerializedColumnNormalizerTrait;
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = FieldItemInterface::class;
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
if (!isset($context['target_instance'])) {
throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
}
if ($context['target_instance']->getParent() == NULL) {
throw new InvalidArgumentException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
}
/** @var \Drupal\Core\Field\FieldItemInterface $field_item */
$field_item = $context['target_instance'];
$this->checkForSerializedStrings($data, $class, $field_item);
$field_item->setValue($this->constructValue($data, $context));
return $field_item;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\FieldItemListInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Denormalizes data to Drupal field values.
*
* This class simply calls denormalize() on the individual FieldItems. The
* FieldItem normalizers are responsible for setting the field values for each
* item.
*
* @see \Drupal\serialization\Normalizer\FieldItemNormalizer.
*/
class FieldNormalizer extends ListNormalizer implements DenormalizerInterface {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = FieldItemListInterface::class;
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
if (!isset($context['target_instance'])) {
throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
}
if ($context['target_instance']->getParent() == NULL) {
throw new InvalidArgumentException('The field passed in via $context[\'target_instance\'] must have a parent set.');
}
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
$items = $context['target_instance'];
$item_class = $items->getItemDefinition()->getClass();
if (!is_array($data)) {
throw new UnexpectedValueException(sprintf('Field values for "%s" must use an array structure', $items->getName()));
}
foreach ($data as $item_data) {
// Create a new item and pass it as the target for the unserialization of
// $item_data. All items in field should have removed before this method
// was called.
// @see \Drupal\serialization\Normalizer\ContentEntityNormalizer::denormalize().
$context['target_instance'] = $items->appendItem();
$this->serializer->denormalize($item_data, $item_class, $format, $context);
}
return $items;
}
}
@@ -0,0 +1,265 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* A trait for providing fieldable entity normalization/denormalization methods.
*
* @todo Move this into a FieldableEntityNormalizer in Drupal 9. This is a trait
* used in \Drupal\serialization\Normalizer\EntityNormalizer to maintain BC.
* @see https://www.drupal.org/node/2834734
*/
trait FieldableEntityNormalizerTrait {
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
*/
protected $entityTypeRepository;
/**
* Determines the entity type ID to denormalize as.
*
* @param string $class
* The entity type class to be denormalized to.
* @param array $context
* The serialization context data.
*
* @return string
* The entity type ID.
*/
protected function determineEntityTypeId($class, $context) {
// Get the entity type ID while letting context override the $class param.
return !empty($context['entity_type']) ? $context['entity_type'] : $this->getEntityTypeRepository()->getEntityTypeFromClass($class);
}
/**
* Gets the entity type definition.
*
* @param string $entity_type_id
* The entity type ID to load the definition for.
*
* @return \Drupal\Core\Entity\EntityTypeInterface
* The loaded entity type definition.
*
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
*/
protected function getEntityTypeDefinition($entity_type_id) {
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition */
// Get the entity type definition.
$entity_type_definition = $this->getEntityTypeManager()->getDefinition($entity_type_id, FALSE);
// Don't try to create an entity without an entity type id.
if (!$entity_type_definition) {
throw new UnexpectedValueException(sprintf('The specified entity type "%s" does not exist. A valid entity type is required for denormalization', $entity_type_id));
}
return $entity_type_definition;
}
/**
* Denormalizes the bundle property so entity creation can use it.
*
* @param array $data
* The data being denormalized.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition
* The entity type definition.
*
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
*
* @return string
* The valid bundle name.
*/
protected function extractBundleData(array &$data, EntityTypeInterface $entity_type_definition) {
$bundle_key = $entity_type_definition->getKey('bundle');
// Get the base field definitions for this entity type.
$base_field_definitions = $this->getEntityFieldManager()->getBaseFieldDefinitions($entity_type_definition->id());
// Get the ID key from the base field definition for the bundle key or
// default to 'value'.
$key_id = isset($base_field_definitions[$bundle_key]) ? $base_field_definitions[$bundle_key]->getFieldStorageDefinition()->getMainPropertyName() : 'value';
// Normalize the bundle if it is not explicitly set.
$bundle_value = isset($data[$bundle_key][0][$key_id]) ? $data[$bundle_key][0][$key_id] : (isset($data[$bundle_key]) ? $data[$bundle_key] : NULL);
// Unset the bundle from the data.
unset($data[$bundle_key]);
// Get the bundle entity type from the entity type definition.
$bundle_type_id = $entity_type_definition->getBundleEntityType();
$bundle_types = $bundle_type_id ? $this->getEntityTypeManager()->getStorage($bundle_type_id)->getQuery()->execute() : [];
// Make sure a bundle has been provided.
if (!is_string($bundle_value)) {
throw new UnexpectedValueException(sprintf('Could not determine entity type bundle: "%s" field is missing.', $bundle_key));
}
// Make sure the submitted bundle is a valid bundle for the entity type.
if ($bundle_types && !in_array($bundle_value, $bundle_types)) {
throw new UnexpectedValueException(sprintf('"%s" is not a valid bundle type for denormalization.', $bundle_value));
}
return [$bundle_key => $bundle_value];
}
/**
* Denormalizes entity data by denormalizing each field individually.
*
* @param array $data
* The data to denormalize.
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The fieldable entity to set field values for.
* @param string $format
* The serialization format.
* @param array $context
* The context data.
*/
protected function denormalizeFieldData(array $data, FieldableEntityInterface $entity, $format, array $context) {
foreach ($data as $field_name => $field_data) {
$field_item_list = $entity->get($field_name);
// Remove any values that were set as a part of entity creation (e.g
// uuid). If the incoming field data is set to an empty array, this will
// also have the effect of emptying the field in REST module.
$field_item_list->setValue([]);
$field_item_list_class = get_class($field_item_list);
if ($field_data) {
// The field instance must be passed in the context so that the field
// denormalizer can update field values for the parent entity.
$context['target_instance'] = $field_item_list;
$this->serializer->denormalize($field_data, $field_item_list_class, $format, $context);
}
}
}
/**
* Returns the entity type repository.
*
* @return \Drupal\Core\Entity\EntityTypeRepositoryInterface
* The entity type repository.
*/
protected function getEntityTypeRepository() {
if (!$this->entityTypeRepository) {
@trigger_error('The entityTypeRepository property must be set on the FieldEntityNormalizerTrait, it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$this->entityTypeRepository = \Drupal::service('entity_type.repository');
}
return $this->entityTypeRepository;
}
/**
* Returns the entity field manager.
*
* @return \Drupal\Core\Entity\EntityFieldManagerInterface
* The entity field manager.
*/
protected function getEntityFieldManager() {
if (!$this->entityFieldManager) {
@trigger_error('The entityFieldManager property must be set on the FieldEntityNormalizerTrait, it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$this->entityFieldManager = \Drupal::service('entity_field.manager');
}
return $this->entityFieldManager;
}
/**
* Returns the entity type manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The entity type manager.
*/
protected function getEntityTypeManager() {
if (!$this->entityTypeManager) {
@trigger_error('The entityTypeManager property must be set on the FieldEntityNormalizerTrait, it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
$this->entityTypeManager = \Drupal::service('entity_type.manager');
}
return $this->entityTypeManager;
}
/**
* Build the field item value using the incoming data.
*
* Most normalizers that extend this class can simply use this method to
* construct the denormalized value without having to override denormalize()
* and reimplementing its validation logic or its call to set the field value.
*
* It's recommended to not override this and instead provide a (de)normalizer
* at the DataType level.
*
* @param mixed $data
* The incoming data for this field item.
* @param array $context
* The context passed into the Normalizer.
*
* @return mixed
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
$field_item = $context['target_instance'];
// Get the property definitions.
assert($field_item instanceof FieldItemInterface);
$field_definition = $field_item->getFieldDefinition();
$item_definition = $field_definition->getItemDefinition();
assert($item_definition instanceof FieldItemDataDefinitionInterface);
$property_definitions = $item_definition->getPropertyDefinitions();
$serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
$denormalize_property = function ($property_name, $property_value, $property_value_class, $context) use ($serialized_property_names) {
if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
return $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
}
else {
if (in_array($property_name, $serialized_property_names, TRUE)) {
$property_value = serialize($property_value);
}
return $property_value;
}
};
if (!is_array($data)) {
$property_value = $data;
$property_name = $item_definition->getMainPropertyName();
$property_value_class = $property_definitions[$property_name]->getClass();
return $denormalize_property($property_name, $property_value, $property_value_class, $context);
}
$data_internal = [];
if (!empty($property_definitions)) {
foreach ($property_definitions as $property_name => $property_definition) {
// Not every property is required to be sent.
if (!array_key_exists($property_name, $data)) {
continue;
}
$property_value = $data[$property_name];
$property_value_class = $property_definition->getClass();
$data_internal[$property_name] = $denormalize_property($property_name, $property_value, $property_value_class, $context);
}
}
else {
$data_internal = $data;
}
return $data_internal;
}
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\TypedData\ListInterface;
/**
* Converts list objects to arrays.
*
* Ordinarily, this would be handled automatically by Serializer, but since
* there is a TypedDataNormalizer and the Field class extends TypedData, any
* Field will be handled by that Normalizer instead of being traversed. This
* class ensures that TypedData classes that also implement ListInterface are
* traversed instead of simply returning getValue().
*/
class ListNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = ListInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$attributes = [];
foreach ($object as $fieldItem) {
$attributes[] = $this->serializer->normalize($fieldItem, $format, $context);
}
return $attributes;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Component\Render\MarkupInterface;
/**
* Normalizes MarkupInterface objects into a string.
*/
class MarkupNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = MarkupInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
return (string) $object;
}
}
@@ -0,0 +1,101 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* Base class for Normalizers.
*/
abstract class NormalizerBase implements SerializerAwareInterface, CacheableNormalizerInterface {
use SerializerAwareTrait;
/**
* The interface or class that this Normalizer supports.
*
* @var string|array
*/
protected $supportedInterfaceOrClass;
/**
* List of formats which supports (de-)normalization.
*
* @var string|string[]
*/
protected $format;
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = NULL) {
// If we aren't dealing with an object or the format is not supported return
// now.
if (!is_object($data) || !$this->checkFormat($format)) {
return FALSE;
}
$supported = (array) $this->supportedInterfaceOrClass;
return (bool) array_filter($supported, function ($name) use ($data) {
return $data instanceof $name;
});
}
/**
* Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization()
*
* This class doesn't implement DenormalizerInterface, but most of its child
* classes do, so this method is implemented at this level to reduce code
* duplication.
*/
public function supportsDenormalization($data, $type, $format = NULL) {
// If the format is not supported return now.
if (!$this->checkFormat($format)) {
return FALSE;
}
$supported = (array) $this->supportedInterfaceOrClass;
$subclass_check = function ($name) use ($type) {
return (class_exists($name) || interface_exists($name)) && is_subclass_of($type, $name, TRUE);
};
return in_array($type, $supported) || array_filter($supported, $subclass_check);
}
/**
* Checks if the provided format is supported by this normalizer.
*
* @param string $format
* The format to check.
*
* @return bool
* TRUE if the format is supported, FALSE otherwise. If no format is
* specified this will return TRUE.
*/
protected function checkFormat($format = NULL) {
if (!isset($format) || !isset($this->format)) {
return TRUE;
}
return in_array($format, (array) $this->format, TRUE);
}
/**
* Adds cacheability if applicable.
*
* @param array $context
* Context options for the normalizer.
* @param $data
* The data that might have cacheability information.
*/
protected function addCacheableDependency(array $context, $data) {
if ($data instanceof CacheableDependencyInterface && isset($context[static::SERIALIZATION_CONTEXT_CACHEABILITY])) {
$context[static::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($data);
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\serialization\Normalizer;
/**
* Null normalizer.
*/
class NullNormalizer extends NormalizerBase {
/**
* Constructs a NullNormalizer object.
*
* @param string|array $supported_interface_of_class
* The supported interface(s) or class(es).
*/
public function __construct($supported_interface_of_class) {
$this->supportedInterfaceOrClass = $supported_interface_of_class;
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
return NULL;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\PrimitiveInterface;
/**
* Converts primitive data objects to their casted values.
*/
class PrimitiveDataNormalizer extends NormalizerBase {
use SerializedColumnNormalizerTrait;
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = PrimitiveInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$parent = $object->getParent();
if ($parent instanceof FieldItemInterface && $object->getValue()) {
$serialized_property_names = $this->getCustomSerializedPropertyNames($parent);
if (in_array($object->getName(), $serialized_property_names, TRUE)) {
return unserialize($object->getValue());
}
}
// Typed data casts NULL objects to their empty variants, so for example
// the empty string ('') for string type data, or 0 for integer typed data.
// In a better world with typed data implementing algebraic data types,
// getCastedValue would return NULL, but as typed data is not aware of real
// optional values on the primitive level, we implement our own optional
// value normalization here.
return $object->getValue() === NULL ? NULL : $object->getCastedValue();
}
}
@@ -0,0 +1,116 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Field\FieldItemInterface;
/**
* A trait providing methods for serialized columns.
*/
trait SerializedColumnNormalizerTrait {
/**
* Checks if there is a serialized string for a column.
*
* @param mixed $data
* The field item data to denormalize.
* @param string $class
* The expected class to instantiate.
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
*/
protected function checkForSerializedStrings($data, $class, FieldItemInterface $field_item) {
// Require specialized denormalizers for fields with 'serialize' columns.
// Note: this cannot be checked in ::supportsDenormalization() because at
// that time we only have the field item class. ::hasSerializeColumn()
// must be able to call $field_item->schema(), which requires a field
// storage definition. To determine that, the entity type and bundle
// must be known, which is contextual information that the Symfony
// serializer does not pass to ::supportsDenormalization().
if (!is_array($data)) {
$data = [$field_item->getDataDefinition()->getMainPropertyName() => $data];
}
if ($this->dataHasStringForSerializeColumn($field_item, $data)) {
$field_name = $field_item->getParent() ? $field_item->getParent()->getName() : $field_item->getName();
throw new \LogicException(sprintf('The generic FieldItemNormalizer cannot denormalize string values for "%s" properties of the "%s" field (field item class: %s).', implode('", "', $this->getSerializedPropertyNames($field_item)), $field_name, $class));
}
}
/**
* Checks if the data contains string value for serialize column.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
* @param array $data
* The data being denormalized.
*
* @return bool
* TRUE if there is a string value for serialize column, otherwise FALSE.
*/
protected function dataHasStringForSerializeColumn(FieldItemInterface $field_item, array $data) {
foreach ($this->getSerializedPropertyNames($field_item) as $property_name) {
if (isset($data[$property_name]) && is_string($data[$property_name])) {
return TRUE;
}
}
return FALSE;
}
/**
* Gets the names of all serialized properties.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
*
* @return string[]
* The property names for serialized properties.
*/
protected function getSerializedPropertyNames(FieldItemInterface $field_item) {
$field_storage_definition = $field_item->getFieldDefinition()->getFieldStorageDefinition();
if ($custom_property_names = $this->getCustomSerializedPropertyNames($field_item)) {
return $custom_property_names;
}
$field_storage_schema = $field_item->schema($field_storage_definition);
// If there are no columns then there are no serialized properties.
if (!isset($field_storage_schema['columns'])) {
return [];
}
$serialized_columns = array_filter($field_storage_schema['columns'], function ($column_schema) {
return isset($column_schema['serialize']) && $column_schema['serialize'] === TRUE;
});
return array_keys($serialized_columns);
}
/**
* Gets the names of all properties the plugin treats as serialized data.
*
* This allows the field storage definition or entity type to provide a
* setting for serialized properties. This can be used for fields that
* handle serialized data themselves and do not rely on the serialized schema
* flag.
*
* @param \Drupal\Core\Field\FieldItemInterface $field_item
* The field item.
*
* @return string[]
* The property names for serialized properties.
*/
protected function getCustomSerializedPropertyNames(FieldItemInterface $field_item) {
if ($field_item instanceof PluginInspectionInterface) {
$definition = $field_item->getPluginDefinition();
$serialized_fields = $field_item->getEntity()->getEntityType()->get('serialized_field_property_names');
$field_name = $field_item->getFieldDefinition()->getName();
if (is_array($serialized_fields) && isset($serialized_fields[$field_name]) && is_array($serialized_fields[$field_name])) {
return $serialized_fields[$field_name];
}
if (isset($definition['serialized_property_names']) && is_array($definition['serialized_property_names'])) {
return $definition['serialized_property_names'];
}
}
return [];
}
}
@@ -0,0 +1,91 @@
<?php
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 {
/**
* 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/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));
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
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 {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = TimestampItem::class;
/**
* {@inheritdoc}
*/
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}
*/
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)];
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
/**
* Converts values for the Timestamp data type to and from common formats.
*
* @internal
*
* Note that \Drupal\Core\TypedData\Plugin\DataType\Timestamp::getDateTime()
* explicitly sets a default timezone of UTC. This ensures the string
* representation generated by DateTimeNormalizer::normalize() is also in UTC.
*/
class TimestampNormalizer extends DateTimeNormalizer {
/**
* {@inheritdoc}
*/
protected $allowedFormats = [
'UNIX timestamp' => 'U',
'ISO 8601' => \DateTime::ISO8601,
'RFC 3339' => \DateTime::RFC3339,
];
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = Timestamp::class;
/**
* {@inheritdoc}
*/
protected function getNormalizationTimezone() {
return new \DateTimeZone('UTC');
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
$denormalized = parent::denormalize($data, $class, $format, $context);
return $denormalized->getTimestamp();
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Converts typed data objects to arrays.
*/
class TypedDataNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = TypedDataInterface::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$this->addCacheableDependency($context, $object);
$value = $object->getValue();
// Support for stringable value objects: avoid numerous custom normalizers.
if (is_object($value) && method_exists($value, '__toString')) {
$value = (string) $value;
}
return $value;
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\serialization;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services tagged 'normalizer' and 'encoder' to the Serializer.
*/
class RegisterEntityResolversCompilerPass implements CompilerPassInterface {
/**
* Adds services to the Serializer.
*
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
* The container to process.
*/
public function process(ContainerBuilder $container) {
$definition = $container->getDefinition('serializer.entity_resolver');
$resolvers = [];
// Retrieve registered Normalizers and Encoders from the container.
foreach ($container->findTaggedServiceIds('entity_resolver') as $id => $attributes) {
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
$resolvers[$priority][] = new Reference($id);
}
// Add the registered concrete EntityResolvers to the ChainEntityResolver.
foreach ($this->sort($resolvers) as $resolver) {
$definition->addMethodCall('addResolver', [$resolver]);
}
}
/**
* Sorts by priority.
*
* Order services from highest priority number to lowest (reverse sorting).
*
* @param array $services
* A nested array keyed on priority number. For each priority number, the
* value is an array of Symfony\Component\DependencyInjection\Reference
* objects, each a reference to a normalizer or encoder service.
*
* @return array
* A flattened array of Reference objects from $services, ordered from high
* to low priority.
*/
protected function sort($services) {
$sorted = [];
krsort($services);
// Flatten the array.
foreach ($services as $a) {
$sorted = array_merge($sorted, $a);
}
return $sorted;
}
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\serialization;
use Drupal\Core\Config\BootstrapConfigStorageFactory;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services tagged 'normalizer' and 'encoder' to the Serializer.
*/
class RegisterSerializationClassesCompilerPass implements CompilerPassInterface {
/**
* Adds services to the Serializer.
*
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
* The container to process.
*/
public function process(ContainerBuilder $container) {
$definition = $container->getDefinition('serializer');
// Retrieve registered Normalizers and Encoders from the container.
foreach ($container->findTaggedServiceIds('normalizer') as $id => $attributes) {
// The 'serializer' service is the public API: mark normalizers private.
$container->getDefinition($id)->setPublic(FALSE);
// If there is a BC key present, pass this to determine if the normalizer
// should be skipped.
if (isset($attributes[0]['bc']) && $this->normalizerBcSettingIsEnabled($attributes[0]['bc'], $attributes[0]['bc_config_name'])) {
continue;
}
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
$normalizers[$priority][] = new Reference($id);
}
foreach ($container->findTaggedServiceIds('encoder') as $id => $attributes) {
// The 'serializer' service is the public API: mark encoders private.
$container->getDefinition($id)->setPublic(FALSE);
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
$encoders[$priority][] = new Reference($id);
}
// Add the registered Normalizers and Encoders to the Serializer.
if (!empty($normalizers)) {
$definition->replaceArgument(0, $this->sort($normalizers));
}
if (!empty($encoders)) {
$definition->replaceArgument(1, $this->sort($encoders));
}
// Find all serialization formats known.
$formats = [];
$format_providers = [];
foreach ($container->findTaggedServiceIds('encoder') as $service_id => $attributes) {
$format = $attributes[0]['format'];
$formats[] = $format;
if ($provider_tag = $container->getDefinition($service_id)->getTag('_provider')) {
$format_providers[$format] = $provider_tag[0]['provider'];
}
}
$container->setParameter('serializer.formats', $formats);
$container->setParameter('serializer.format_providers', $format_providers);
}
/**
* Returns whether a normalizer BC setting is disabled or not.
*
* @param string $key
*
* @return bool
*/
protected function normalizerBcSettingIsEnabled($key, $config_name) {
$settings = BootstrapConfigStorageFactory::get()->read($config_name);
return !empty($settings[$key]);
}
/**
* Sorts by priority.
*
* Order services from highest priority number to lowest (reverse sorting).
*
* @param array $services
* A nested array keyed on priority number. For each priority number, the
* value is an array of Symfony\Component\DependencyInjection\Reference
* objects, each a reference to a normalizer or encoder service.
*
* @return array
* A flattened array of Reference objects from $services, ordered from high
* to low priority.
*/
protected function sort($services) {
$sorted = [];
krsort($services);
// Flatten the array.
foreach ($services as $a) {
$sorted = array_merge($sorted, $a);
}
return $sorted;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\serialization;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
/**
* Serialization dependency injection container.
*/
class SerializationServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
// Add a compiler pass for adding Normalizers and Encoders to Serializer.
$container->addCompilerPass(new RegisterSerializationClassesCompilerPass());
// Add a compiler pass for adding concrete Resolvers to chain Resolver.
$container->addCompilerPass(new RegisterEntityResolversCompilerPass());
}
}
@@ -0,0 +1,15 @@
<?php
namespace Drupal\serialization\Tests;
@trigger_error(__NAMESPACE__ . '\NormalizerTestBase is deprecated for removal before Drupal 9.0.0. Use \Drupal\Tests\serialization\Kernel\NormalizerTestBase instead.', E_USER_DEPRECATED);
use Drupal\Tests\serialization\Kernel\NormalizerTestBase as SerializationNormalizerTestBase;
/**
* Helper base class to set up some test fields for serialization testing.
*
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0.
* Use \Drupal\Tests\serialization\Kernel\NormalizerTestBase instead.
*/
abstract class NormalizerTestBase extends SerializationNormalizerTestBase {}
@@ -0,0 +1,6 @@
name: 'Entity serialization test support'
type: module
description: 'Provides test support for entity serialization tests.'
package: Testing
version: VERSION
core: 8.x
@@ -0,0 +1,23 @@
<?php
/**
* @file
* Test support module for entity serialization tests.
*/
use Drupal\Core\Access\AccessResult;
/**
* Implements hook_entity_field_access_alter().
*
* Overrides some default access control to support testing.
*
* @see Drupal\serialization\Tests\EntitySerializationTest::testUserNormalize()
*/
function entity_serialization_test_entity_field_access_alter(array &$grants, array $context) {
// Override default access control from UserAccessControlHandler to allow
// access to 'pass' field for the test user.
if ($context['field_definition']->getName() == 'pass' && $context['account']->getAccountName() == 'serialization_test_user') {
$grants[':default'] = AccessResult::allowed()->inheritCacheability($grants[':default'])->addCacheableDependency($context['items']->getEntity());
}
}
@@ -0,0 +1,6 @@
name: 'FieldItem normalization test support'
type: module
description: 'Provides test support for fieldItem normalization test support.'
package: Testing
version: VERSION
core: 8.x
@@ -0,0 +1,6 @@
services:
serializer.normalizer.silly_fielditem:
class: Drupal\field_normalization_test\Normalization\TextItemSillyNormalizer
tags:
# The priority must be higher than serialization.normalizer.field_item.
- { name: normalizer , priority: 9 }
@@ -0,0 +1,36 @@
<?php
namespace Drupal\field_normalization_test\Normalization;
use Drupal\serialization\Normalizer\FieldItemNormalizer;
use Drupal\text\Plugin\Field\FieldType\TextItemBase;
/**
* A test TextItem normalizer to test denormalization.
*/
class TextItemSillyNormalizer extends FieldItemNormalizer {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = TextItemBase::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$data = parent::normalize($object, $format, $context);
$data['value'] .= '::silly_suffix';
return $data;
}
/**
* {@inheritdoc}
*/
protected function constructValue($data, $context) {
$value = parent::constructValue($data, $context);
$value['value'] = str_replace('::silly_suffix', '', $value['value']);
return $value;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\test_datatype_boolean_emoji_normalizer\Normalizer;
use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
use Drupal\serialization\Normalizer\NormalizerBase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Normalizes boolean data weirdly: renders them as 👍 (TRUE) or 👎 (FALSE).
*/
class BooleanNormalizer extends NormalizerBase implements DenormalizerInterface {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = BooleanData::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
return $object->getValue() ? '👍' : '👎';
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
if (!in_array($data, ['👍', '👎'], TRUE)) {
throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
}
return $data === '👍';
}
}
@@ -0,0 +1,6 @@
name: 'Test @DataType normalizer'
type: module
description: 'Provides test support for @DataType-level normalization.'
package: Testing
version: VERSION
core: 8.x
@@ -0,0 +1,6 @@
services:
serializer.normalizer.boolean.datatype.emoji:
class: Drupal\test_datatype_boolean_emoji_normalizer\Normalizer\BooleanNormalizer
tags:
# The priority must be higher than serializer.normalizer.primitive_data.
- { name: normalizer , priority: 1000 }
@@ -0,0 +1,46 @@
<?php
namespace Drupal\test_fieldtype_boolean_emoji_normalizer\Normalizer;
use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
use Drupal\serialization\Normalizer\FieldItemNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Normalizes boolean fields weirdly: renders them as 👍 (TRUE) or 👎 (FALSE).
*/
class BooleanItemNormalizer extends FieldItemNormalizer implements DenormalizerInterface {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = BooleanItem::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$data = parent::normalize($object, $format, $context);
$data['value'] = $data['value'] ? '👍' : '👎';
return $data;
}
/**
* {@inheritdoc}
*/
protected function constructValue($data, $context) {
// Just like \Drupal\serialization\Normalizer\FieldItemNormalizer's logic
// for denormalization, which uses TypedDataInterface::setValue(), allow the
// keying by main property name ("value") to be implied.
if (!is_array($data)) {
$data = ['value' => $data];
}
if (!in_array($data['value'], ['👍', '👎'], TRUE)) {
throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
}
$data['value'] = ($data['value'] === '👍');
return $data;
}
}
@@ -0,0 +1,6 @@
name: 'Test @FieldType normalizer'
type: module
description: 'Provides test support for @FieldType-level normalization.'
package: Testing
version: VERSION
core: 8.x
@@ -0,0 +1,6 @@
services:
serializer.normalizer.boolean.fieldtype.emoji:
class: Drupal\test_fieldtype_boolean_emoji_normalizer\Normalizer\BooleanItemNormalizer
tags:
# The priority must be higher than serialization.normalizer.field_item.
- { name: normalizer , priority: 1000 }
@@ -0,0 +1,6 @@
name: Serialization test module
type: module
description: "Support module for serialization tests."
package: Testing
version: VERSION
core: 8.x
@@ -0,0 +1,9 @@
services:
serializer.normalizer.serialization_test:
class: Drupal\serialization_test\SerializationTestNormalizer
tags:
- { name: normalizer }
serializer.encoder.serialization_test:
class: Drupal\serialization_test\SerializationTestEncoder
tags:
- { name: encoder, format: serialization_test}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\serialization_test;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
class SerializationTestEncoder implements EncoderInterface {
/**
* The format that this Encoder supports.
*
* @var string
*/
protected static $format = 'serialization_test';
/**
* {@inheritdoc}
*/
public function encode($data, $format, array $context = []) {
// @see \Drupal\serialization_test\SerializationTestNormalizer::normalize().
return 'Normalized by ' . $data['normalized_by'] . ', Encoded by SerializationTestEncoder';
}
/**
* {@inheritdoc}
*/
public function supportsEncoding($format) {
return static::$format === $format;
}
}
@@ -0,0 +1,51 @@
<?php
namespace Drupal\serialization_test;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SerializationTestNormalizer implements NormalizerInterface {
/**
* The format that this Normalizer supports.
*
* @var string
*/
protected static $format = 'serialization_test';
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param object $object
* Object to normalize.
* @param string $format
* Format the normalization result will be encoded as.
*
* @return array
* An array containing a normalized representation of $object, appropriate
* for encoding to the requested format.
*/
public function normalize($object, $format = NULL, array $context = []) {
$normalized = (array) $object;
// Add identifying value that can be used to verify that the expected
// normalizer was invoked.
$normalized['normalized_by'] = 'SerializationTestNormalizer';
return $normalized;
}
/**
* Checks whether format is supported by this normalizer.
*
* @param mixed $data
* Data to normalize.
* @param string $format
* Format the normalization result will be encoded as.
*
* @return bool
* Returns TRUE if the normalizer can handle the request.
*/
public function supportsNormalization($data, $format = NULL) {
return static::$format === $format;
}
}
@@ -0,0 +1,15 @@
<?php
namespace Drupal\serialization_test;
use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
/**
* For testing that TimeStampItemNormalizerTrait throws a deprecation error.
*
* @see \Drupal\Tests\serialization\Unit\Normalizer\TimeStampItemNormalizerTraitDeprecatedTest
*/
class TimeStampItemNormalizerTraitDeprecatedTestClass {
use TimeStampItemNormalizerTrait;
}
@@ -0,0 +1,98 @@
<?php
namespace Drupal\Tests\serialization\Kernel;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests that entities references can be resolved.
*
* @group serialization
*/
class EntityResolverTest extends NormalizerTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['hal', 'rest'];
/**
* The format being tested.
*
* @var string
*/
protected $format = 'hal_json';
protected function setUp() {
parent::setUp();
\Drupal::service('router.builder')->rebuild();
// Create the test field storage.
FieldStorageConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_entity_reference',
'type' => 'entity_reference',
'settings' => [
'target_type' => 'entity_test_mulrev',
],
])->save();
// Create the test field.
FieldConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_entity_reference',
'bundle' => 'entity_test_mulrev',
])->save();
}
/**
* Test that fields referencing UUIDs can be denormalized.
*/
public function testUuidEntityResolver() {
// Create an entity to get the UUID from.
$entity = EntityTestMulRev::create(['type' => 'entity_test_mulrev']);
$entity->set('name', 'foobar');
$entity->set('field_test_entity_reference', [['target_id' => 1]]);
$entity->save();
$field_uri = Url::fromUri('base:rest/relation/entity_test_mulrev/entity_test_mulrev/field_test_entity_reference', ['absolute' => TRUE])->toString();
$data = [
'_links' => [
'type' => [
'href' => Url::fromUri('base:rest/type/entity_test_mulrev/entity_test_mulrev', ['absolute' => TRUE])->toString(),
],
$field_uri => [
[
'href' => $entity->toUrl()->toString(),
],
],
],
'_embedded' => [
$field_uri => [
[
'_links' => [
'self' => $entity->toUrl()->toString(),
],
'uuid' => [
[
'value' => $entity->uuid(),
],
],
],
],
],
];
$denormalized = $this->container->get('serializer')->denormalize($data, 'Drupal\entity_test\Entity\EntityTestMulRev', $this->format);
$field_value = $denormalized->get('field_test_entity_reference')->getValue();
$this->assertEqual($field_value[0]['target_id'], 1, 'Entity reference resolved using UUID.');
}
}
@@ -0,0 +1,364 @@
<?php
namespace Drupal\Tests\serialization\Kernel;
use Drupal\Component\Serialization\Json;
use Drupal\entity_test\Entity\EntitySerializedField;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
/**
* Tests that entities can be serialized to supported core formats.
*
* @group serialization
*/
class EntitySerializationTest extends NormalizerTestBase {
use BcTimestampNormalizerUnixTestTrait;
/**
* Modules to install.
*
* @var array
*/
public static $modules = [
'serialization',
'system',
'field',
'entity_test',
'text',
'filter',
'user',
'entity_serialization_test',
];
/**
* The test values.
*
* @var array
*/
protected $values;
/**
* The test entity.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $entity;
/**
* The test user.
*
* @var \Drupal\user\Entity\User
*/
protected $user;
/**
* The serializer service.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* The class name of the test class.
*
* @var string
*/
protected $entityClass = 'Drupal\entity_test\Entity\EntityTest';
protected function setUp() {
parent::setUp();
// User create needs sequence table.
$this->installSchema('system', ['sequences']);
FilterFormat::create([
'format' => 'my_text_format',
'name' => 'My Text Format',
'filters' => [
'filter_html' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [
'allowed_html' => '<p>',
],
],
'filter_autop' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [],
],
],
])->save();
// Create a test user to use as the entity owner.
$this->user = \Drupal::entityTypeManager()->getStorage('user')->create([
'name' => 'serialization_test_user',
'mail' => 'foo@example.com',
'pass' => '123456',
]);
$this->user->save();
// Create a test entity to serialize.
$test_text_value = $this->randomMachineName();
$this->values = [
'name' => $this->randomMachineName(),
'user_id' => $this->user->id(),
'field_test_text' => [
'value' => $test_text_value,
'format' => 'my_text_format',
],
];
$this->entity = EntityTestMulRev::create($this->values);
$this->entity->save();
$this->serializer = $this->container->get('serializer');
$this->installConfig(['field']);
}
/**
* Test the normalize function.
*/
public function testNormalize() {
$expected = [
'id' => [
['value' => 1],
],
'uuid' => [
['value' => $this->entity->uuid()],
],
'langcode' => [
['value' => 'en'],
],
'name' => [
['value' => $this->values['name']],
],
'type' => [
['value' => 'entity_test_mulrev'],
],
'created' => [
$this->formatExpectedTimestampItemValues($this->entity->created->value),
],
'user_id' => [
[
// id() will return the string value as it comes from the database.
'target_id' => (int) $this->user->id(),
'target_type' => $this->user->getEntityTypeId(),
'target_uuid' => $this->user->uuid(),
'url' => $this->user->toUrl()->toString(),
],
],
'revision_id' => [
['value' => 1],
],
'default_langcode' => [
['value' => TRUE],
],
'revision_translation_affected' => [
['value' => TRUE],
],
'non_rev_field' => [],
'non_mul_field' => [],
'field_test_text' => [
[
'value' => $this->values['field_test_text']['value'],
'format' => $this->values['field_test_text']['format'],
'processed' => "<p>{$this->values['field_test_text']['value']}</p>",
],
],
];
$normalized = $this->serializer->normalize($this->entity);
foreach (array_keys($expected) as $fieldName) {
$this->assertSame($expected[$fieldName], $normalized[$fieldName], "Normalization produces expected array for $fieldName.");
}
$this->assertEqual(array_diff_key($normalized, $expected), [], 'No unexpected data is added to the normalized array.');
}
/**
* Tests user normalization, using the entity_serialization_test module to
* override some default access controls.
*/
public function testUserNormalize() {
// Test password isn't available.
$normalized = $this->serializer->normalize($this->user);
$this->assertArrayNotHasKey('pass', $normalized);
$this->assertArrayNotHasKey('mail', $normalized);
// Test again using our test user, so that our access control override will
// allow password viewing.
$normalized = $this->serializer->normalize($this->user, NULL, ['account' => $this->user]);
// The key 'pass' will now exist, but the password value should be
// normalized to NULL.
$this->assertIdentical($normalized['pass'], [NULL], '"pass" value is normalized to [NULL]');
}
/**
* Test registered Serializer's entity serialization for core's formats.
*/
public function testSerialize() {
// Test that Serializer responds using the ComplexDataNormalizer and
// JsonEncoder. The output of ComplexDataNormalizer::normalize() is tested
// elsewhere, so we can just assume that it works properly here.
$normalized = $this->serializer->normalize($this->entity, 'json');
$expected = Json::encode($normalized);
// Test 'json'.
$actual = $this->serializer->serialize($this->entity, 'json');
$this->assertIdentical($actual, $expected, 'Entity serializes to JSON when "json" is requested.');
$actual = $this->serializer->serialize($normalized, 'json');
$this->assertIdentical($actual, $expected, 'A normalized array serializes to JSON when "json" is requested');
// Test 'ajax'.
$actual = $this->serializer->serialize($this->entity, 'ajax');
$this->assertIdentical($actual, $expected, 'Entity serializes to JSON when "ajax" is requested.');
$actual = $this->serializer->serialize($normalized, 'ajax');
$this->assertIdentical($actual, $expected, 'A normalized array serializes to JSON when "ajax" is requested');
// 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>' . $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->toUrl()->toString() . '</url></user_id>',
'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
'revision_translation_affected' => '<revision_translation_affected><value>1</value></revision_translation_affected>',
'non_mul_field' => '<non_mul_field/>',
'non_rev_field' => '<non_rev_field/>',
'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format><processed><![CDATA[<p>' . $this->values['field_test_text']['value'] . '</p>]]></processed></field_test_text>',
];
// Sort it in the same order as normalized.
$expected = array_merge($normalized, $expected);
// Add header and footer.
array_unshift($expected, '<?xml version="1.0"?>' . PHP_EOL . '<response>');
$expected[] = '</response>' . PHP_EOL;
// Reduced the array to a string.
$expected = implode('', $expected);
// Test 'xml'. The output should match that of Symfony's XmlEncoder.
$actual = $this->serializer->serialize($this->entity, 'xml');
$this->assertIdentical($actual, $expected);
$actual = $this->serializer->serialize($normalized, 'xml');
$this->assertIdentical($actual, $expected);
}
/**
* Tests denormalization of an entity.
*/
public function testDenormalize() {
$normalized = $this->serializer->normalize($this->entity);
foreach (['json', 'xml'] as $type) {
$denormalized = $this->serializer->denormalize($normalized, $this->entityClass, $type, ['entity_type' => 'entity_test_mulrev']);
$this->assertInstanceOf($this->entityClass, $denormalized);
$this->assertIdentical($denormalized->getEntityTypeId(), $this->entity->getEntityTypeId(), 'Expected entity type found.');
$this->assertIdentical($denormalized->bundle(), $this->entity->bundle(), 'Expected entity bundle found.');
$this->assertIdentical($denormalized->uuid(), $this->entity->uuid(), 'Expected entity UUID found.');
}
}
/**
* Tests denormalizing serialized columns.
*/
public function testDenormalizeSerializedItem() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized" field (field item class: Drupal\entity_test\Plugin\Field\FieldType\SerializedItem).');
$this->serializer->denormalize([
'serialized' => [
[
'value' => 'boo',
],
],
'type' => 'entity_test_serialized_field',
], EntitySerializedField::class);
}
/**
* Tests normalizing/denormalizing custom serialized columns.
*/
public function testDenormalizeCustomSerializedItem() {
$entity = EntitySerializedField::create(['serialized_text' => serialize(['Hello world!'])]);
$normalized = $this->serializer->normalize($entity);
$this->assertEquals($normalized['serialized_text'][0]['value'], ['Hello world!']);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_text" field (field item class: Drupal\entity_test\Plugin\Field\FieldType\SerializedPropertyItem).');
$this->serializer->denormalize([
'serialized_text' => [
[
'value' => 'boo',
],
],
'type' => 'entity_test_serialized_field',
], EntitySerializedField::class);
}
/**
* Tests normalizing/denormalizing invalid custom serialized fields.
*/
public function testDenormalizeInvalidCustomSerializedField() {
$entity = EntitySerializedField::create(['serialized_long' => serialize(['Hello world!'])]);
$normalized = $this->serializer->normalize($entity);
$this->assertEquals($normalized['serialized_long'][0]['value'], ['Hello world!']);
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_long" field (field item class: Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem).');
$this->serializer->denormalize([
'serialized_long' => [
[
'value' => 'boo',
],
],
'type' => 'entity_test_serialized_field',
], EntitySerializedField::class);
}
/**
* Tests normalizing/denormalizing empty custom serialized fields.
*/
public function testDenormalizeEmptyCustomSerializedField() {
$entity = EntitySerializedField::create(['serialized_long' => serialize([])]);
$normalized = $this->serializer->normalize($entity);
$this->assertEquals([], $normalized['serialized_long'][0]['value']);
$entity = $this->serializer->denormalize($normalized, EntitySerializedField::class);
$this->assertEquals(serialize([]), $entity->get('serialized_long')->value);
}
/**
* Tests normalizing/denormalizing valid custom serialized fields.
*/
public function testDenormalizeValidCustomSerializedField() {
$entity = EntitySerializedField::create(['serialized_long' => serialize(['key' => 'value'])]);
$normalized = $this->serializer->normalize($entity);
$this->assertEquals(['key' => 'value'], $normalized['serialized_long'][0]['value']);
$entity = $this->serializer->denormalize($normalized, EntitySerializedField::class);
$this->assertEquals(serialize(['key' => 'value']), $entity->get('serialized_long')->value);
}
/**
* Tests normalizing/denormalizing using string values.
*/
public function testDenormalizeStringValue() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The generic FieldItemNormalizer cannot denormalize string values for "value" properties of the "serialized_long" field (field item class: Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem).');
$this->serializer->denormalize([
'serialized_long' => ['boo'],
'type' => 'entity_test_serialized_field',
], EntitySerializedField::class);
}
}
@@ -0,0 +1,253 @@
<?php
namespace Drupal\Tests\serialization\Kernel;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Test field level normalization process.
*
* @group serialization
*/
class FieldItemSerializationTest extends NormalizerTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'serialization',
'system',
'field',
'entity_test',
'text',
'filter',
'user',
'field_normalization_test',
];
/**
* The class name of the test class.
*
* @var string
*/
protected $entityClass = 'Drupal\entity_test\Entity\EntityTestMulRev';
/**
* The test values.
*
* @var array
*/
protected $values;
/**
* The test entity.
*
* @var \Drupal\Core\Entity\ContentEntityBase
*/
protected $entity;
/**
* The serializer service.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Auto-create a field for testing default field values.
FieldStorageConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_text_default',
'type' => 'text',
'cardinality' => 1,
'translatable' => FALSE,
])->save();
FieldConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_text_default',
'bundle' => 'entity_test_mulrev',
'label' => 'Test text-field with default',
'default_value' => [
[
'value' => 'This is the default',
'format' => 'full_html',
],
],
'widget' => [
'type' => 'text_textfield',
'weight' => 0,
],
])->save();
FieldStorageConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_boolean',
'type' => 'boolean',
'cardinality' => 1,
'translatable' => FALSE,
])->save();
FieldConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_boolean',
'bundle' => 'entity_test_mulrev',
'label' => 'Test boolean',
])->save();
// Create a test entity to serialize.
$this->values = [
'name' => $this->randomMachineName(),
'field_test_text' => [
'value' => $this->randomMachineName(),
'format' => 'full_html',
],
'field_test_boolean' => [
'value' => FALSE,
],
];
$this->entity = EntityTestMulRev::create($this->values);
$this->entity->save();
$this->serializer = $this->container->get('serializer');
$this->installConfig(['field']);
}
/**
* Tests normalizing and denormalizing an entity with field item normalizer.
*/
public function testFieldNormalizeDenormalize() {
$normalized = $this->serializer->normalize($this->entity, 'json');
$expected_field_value = $this->entity->field_test_text[0]->getValue()['value'] . '::silly_suffix';
$this->assertEquals($expected_field_value, $normalized['field_test_text'][0]['value'], 'Text field item normalized');
$denormalized = $this->serializer->denormalize($normalized, $this->entityClass, 'json');
$this->assertEquals($denormalized->field_test_text[0]->getValue(), $this->entity->field_test_text[0]->getValue(), 'Text field item denormalized.');
$this->assertEquals($denormalized->field_test_text_default[0]->getValue(), $this->entity->field_test_text_default[0]->getValue(), 'Text field item with default denormalized.');
// Unset the values for text field that has a default value.
unset($normalized['field_test_text_default']);
$denormalized_without_all_fields = $this->serializer->denormalize($normalized, $this->entityClass, 'json');
// Check that denormalized entity is still the same even if not all fields
// are not provided.
$this->assertEquals($denormalized_without_all_fields->field_test_text[0]->getValue(), $this->entity->field_test_text[0]->getValue(), 'Text field item denormalized.');
// Even though field_test_text_default value was unset before
// denormalization it should still have the default values for the field.
$this->assertEquals($denormalized_without_all_fields->field_test_text_default[0]->getValue(), $this->entity->field_test_text_default[0]->getValue(), 'Text field item with default denormalized.');
}
/**
* Tests denormalizing using a scalar field value.
*/
public function testFieldDenormalizeWithScalarValue() {
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Field values for "uuid" must use an array structure');
$normalized = $this->serializer->normalize($this->entity, 'json');
// Change the UUID value to use the UUID directly. No array structure.
$normalized['uuid'] = $normalized['uuid'][0]['value'];
$this->serializer->denormalize($normalized, $this->entityClass, 'json');
}
/**
* Tests a format-agnostic normalizer.
*
* @param string[] $test_modules
* The test modules to install.
* @param string $format
* The format to test. (NULL results in the format-agnostic normalization.)
*
* @dataProvider providerTestCustomBooleanNormalization
*/
public function testCustomBooleanNormalization(array $test_modules, $format) {
// Asserts the entity contains the value we set.
$this->assertSame(FALSE, $this->entity->field_test_boolean->value);
// Asserts normalizing the entity using core's 'serializer' service DOES
// yield the value we set.
$core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
$this->assertSame(FALSE, $core_normalization['field_test_boolean'][0]['value']);
$assert_denormalization = function (array $normalization) use ($format) {
$denormalized_entity = $this->container->get('serializer')->denormalize($normalization, EntityTestMulRev::class, $format, []);
$this->assertInstanceOf(EntityTestMulRev::class, $denormalized_entity);
$this->assertSame(TRUE, $denormalized_entity->field_test_boolean->value);
};
// Asserts denormalizing the entity DOES yield the value we set:
// - when using the detailed representation
$core_normalization['field_test_boolean'][0]['value'] = TRUE;
$assert_denormalization($core_normalization);
// - and when using the shorthand representation
$core_normalization['field_test_boolean'][0] = TRUE;
$assert_denormalization($core_normalization);
// Install test module that contains a high-priority alternative normalizer.
$this->enableModules($test_modules);
// Asserts normalizing the entity DOES NOT ANYMORE yield the value we set.
$core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
$this->assertSame('👎', $core_normalization['field_test_boolean'][0]['value']);
// Asserts denormalizing the entity DOES NOT ANYMORE yield the value we set:
// - when using the detailed representation
$core_normalization['field_test_boolean'][0]['value'] = '👍';
$assert_denormalization($core_normalization);
// - and when using the shorthand representation
$core_normalization['field_test_boolean'][0] = '👍';
$assert_denormalization($core_normalization);
}
/**
* Data provider.
*
* @return array
* Test cases.
*/
public function providerTestCustomBooleanNormalization() {
return [
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the format-agnostic normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
NULL,
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the format-agnostic normalization' => [
['test_datatype_boolean_emoji_normalizer'],
NULL,
],
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the JSON normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'json',
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the JSON normalization' => [
['test_datatype_boolean_emoji_normalizer'],
'json',
],
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the HAL+JSON normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'hal_json',
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the HAL+JSON normalization' => [
['test_datatype_boolean_emoji_normalizer', 'hal'],
'hal_json',
],
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the XML normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'xml',
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the XML normalization' => [
['test_datatype_boolean_emoji_normalizer', 'hal'],
'xml',
],
];
}
}
@@ -0,0 +1,136 @@
<?php
namespace Drupal\Tests\serialization\Kernel;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\KernelTests\KernelTestBase;
/**
* @group typedData
*/
class MapDataNormalizerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['system', 'serialization'];
/**
* The serializer service.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* The typed data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->serializer = \Drupal::service('serializer');
$this->typedDataManager = \Drupal::typedDataManager();
}
/**
* Tests whether map data can be normalized.
*/
public function testMapNormalize() {
$typed_data = $this->buildExampleTypedData();
$data = $this->serializer->normalize($typed_data, 'json');
$expect_value = [
'key1' => 'value1',
'key2' => 'value2',
'key3' => 3,
'key4' => [
0 => TRUE,
1 => 'value6',
'key7' => 'value7',
],
];
$this->assertSame($expect_value, $data);
}
/**
* Test whether map data with properties can be normalized.
*/
public function testMapWithPropertiesNormalize() {
$typed_data = $this->buildExampleTypedDataWithProperties();
$data = $this->serializer->normalize($typed_data, 'json');
$expect_value = [
'key1' => 'value1',
'key2' => 'value2',
'key3' => 3,
'key4' => [
0 => TRUE,
1 => 'value6',
'key7' => 'value7',
],
];
$this->assertSame($expect_value, $data);
}
/**
* Builds some example typed data object with no properties.
*/
protected function buildExampleTypedData() {
$tree = [
'key1' => 'value1',
'key2' => 'value2',
'key3' => 3,
'key4' => [
0 => TRUE,
1 => 'value6',
'key7' => 'value7',
],
];
$map_data_definition = MapDataDefinition::create();
$typed_data = $this->typedDataManager->create(
$map_data_definition,
$tree,
'test name'
);
return $typed_data;
}
/**
* Builds some example typed data object with properties.
*/
protected function buildExampleTypedDataWithProperties() {
$tree = [
'key1' => 'value1',
'key2' => 'value2',
'key3' => 3,
'key4' => [
0 => TRUE,
1 => 'value6',
'key7' => 'value7',
],
];
$map_data_definition = MapDataDefinition::create()
->setPropertyDefinition('key1', DataDefinition::create('string'))
->setPropertyDefinition('key2', DataDefinition::create('string'))
->setPropertyDefinition('key3', DataDefinition::create('integer'))
->setPropertyDefinition('key4', MapDataDefinition::create()
->setPropertyDefinition(0, DataDefinition::create('boolean'))
->setPropertyDefinition(1, DataDefinition::create('string'))
->setPropertyDefinition('key7', DataDefinition::create('string'))
);
$typed_data = $this->typedDataManager->create(
$map_data_definition,
$tree,
'test name'
);
return $typed_data;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\Tests\serialization\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Helper base class to set up some test fields for serialization testing.
*/
abstract class NormalizerTestBase extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'serialization',
'system',
'field',
'entity_test',
'text',
'filter',
'user',
];
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_mulrev');
$this->installEntitySchema('user');
$this->installConfig(['field']);
\Drupal::service('router.builder')->rebuild();
\Drupal::moduleHandler()->invoke('rest', 'install');
// Auto-create a field for testing.
FieldStorageConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_text',
'type' => 'text',
'cardinality' => 1,
'translatable' => FALSE,
])->save();
FieldConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_text',
'bundle' => 'entity_test_mulrev',
'label' => 'Test text-field',
'widget' => [
'type' => 'text_textfield',
'weight' => 0,
],
])->save();
}
}
@@ -0,0 +1,55 @@
<?php
namespace Drupal\Tests\serialization\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Functional tests for serialization system.
*
* @group serialization
*/
class SerializationTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['serialization', 'serialization_test'];
/**
* The serializer service to test.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;
protected function setUp() {
parent::setUp();
$this->serializer = $this->container->get('serializer');
}
/**
* Confirms that modules can register normalizers and encoders.
*/
public function testSerializerComponentRegistration() {
$object = new \stdClass();
$format = 'serialization_test';
$expected = 'Normalized by SerializationTestNormalizer, Encoded by SerializationTestEncoder';
// Ensure the serialization invokes the expected normalizer and encoder.
$this->assertIdentical($this->serializer->serialize($object, $format), $expected);
// Ensure the serialization fails for an unsupported format.
try {
$this->serializer->serialize($object, 'unsupported_format');
$this->fail('The serializer was expected to throw an exception for an unsupported format, but did not.');
}
catch (UnexpectedValueException $e) {
$this->pass('The serializer threw an exception for an unsupported format.');
}
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\Tests\serialization\Unit\CompilerPass;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\serialization\RegisterSerializationClassesCompilerPass;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Serializer\Serializer;
/**
* @coversDefaultClass \Drupal\serialization\RegisterSerializationClassesCompilerPass
* @group serialization
*/
class RegisterSerializationClassesCompilerPassTest extends UnitTestCase {
/**
* @covers ::process
*/
public function testEncoders() {
$container = new ContainerBuilder();
$container->setDefinition('serializer', new Definition(Serializer::class, [[], []]));
$encoder_1_definition = new Definition('TestClass');
$encoder_1_definition->addTag('encoder', ['format' => 'xml']);
$encoder_1_definition->addTag('_provider', ['provider' => 'test_provider_a']);
$container->setDefinition('encoder_1', $encoder_1_definition);
$encoder_2_definition = new Definition('TestClass');
$encoder_2_definition->addTag('encoder', ['format' => 'json']);
$encoder_2_definition->addTag('_provider', ['provider' => 'test_provider_a']);
$container->setDefinition('encoder_2', $encoder_2_definition);
$encoder_3_definition = new Definition('TestClass');
$encoder_3_definition->addTag('encoder', ['format' => 'hal_json']);
$encoder_3_definition->addTag('_provider', ['provider' => 'test_provider_b']);
$container->setDefinition('encoder_3', $encoder_3_definition);
$normalizer_1_definition = new Definition('TestClass');
$normalizer_1_definition->addTag('normalizer');
$container->setDefinition('normalizer_1', $normalizer_1_definition);
$compiler_pass = new RegisterSerializationClassesCompilerPass();
$compiler_pass->process($container);
// Check registration of formats and providers.
$this->assertEquals(['xml', 'json', 'hal_json'], $container->getParameter('serializer.formats'));
$this->assertEquals(['xml' => 'test_provider_a', 'json' => 'test_provider_a', 'hal_json' => 'test_provider_b'], $container->getParameter('serializer.format_providers'));
// Check all encoder and normalizer service definitions are marked private.
$this->assertFalse($encoder_1_definition->isPublic());
$this->assertFalse($encoder_2_definition->isPublic());
$this->assertFalse($encoder_3_definition->isPublic());
$this->assertFalse($normalizer_1_definition->isPublic());
}
}
@@ -0,0 +1,25 @@
<?php
namespace Drupal\Tests\serialization\Unit\Encoder;
use Drupal\serialization\Encoder\JsonEncoder;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\serialization\Encoder\JsonEncoder
* @group serialization
*/
class JsonEncoderTest extends UnitTestCase {
/**
* Tests the supportsEncoding() method.
*/
public function testSupportsEncoding() {
$encoder = new JsonEncoder();
$this->assertTrue($encoder->supportsEncoding('json'));
$this->assertTrue($encoder->supportsEncoding('ajax'));
$this->assertFalse($encoder->supportsEncoding('xml'));
}
}
@@ -0,0 +1,105 @@
<?php
namespace Drupal\Tests\serialization\Unit\Encoder;
use Drupal\serialization\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder as BaseXmlEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\serialization\Encoder\XmlEncoder
* @group serialization
*/
class XmlEncoderTest extends UnitTestCase {
/**
* The XmlEncoder instance.
*
* @var \Drupal\serialization\Encoder\XmlEncoder
*/
protected $encoder;
/**
* @var \Symfony\Component\Serializer\Encoder\XmlEncoder|\PHPUnit\Framework\MockObject\MockObject
*/
protected $baseEncoder;
/**
* An array of test data.
*
* @var array
*/
protected $testArray = ['test' => 'test'];
protected function setUp() {
$this->baseEncoder = $this->createMock(BaseXmlEncoder::class);
$this->encoder = new XmlEncoder();
$this->encoder->setBaseEncoder($this->baseEncoder);
}
/**
* Tests the supportsEncoding() method.
*/
public function testSupportsEncoding() {
$this->assertTrue($this->encoder->supportsEncoding('xml'));
$this->assertFalse($this->encoder->supportsEncoding('json'));
}
/**
* Tests the supportsDecoding() method.
*/
public function testSupportsDecoding() {
$this->assertTrue($this->encoder->supportsDecoding('xml'));
$this->assertFalse($this->encoder->supportsDecoding('json'));
}
/**
* Tests the encode() method.
*/
public function testEncode() {
$this->baseEncoder->expects($this->once())
->method('encode')
->with($this->testArray, 'test', [])
->will($this->returnValue('test'));
$this->assertEquals('test', $this->encoder->encode($this->testArray, 'test'));
}
/**
* Tests the decode() method.
*/
public function testDecode() {
$this->baseEncoder->expects($this->once())
->method('decode')
->with('test', 'test', [])
->will($this->returnValue($this->testArray));
$this->assertEquals($this->testArray, $this->encoder->decode('test', 'test'));
}
/**
* @covers ::getBaseEncoder
*/
public function testDefaultEncoderHasSerializer() {
// The serializer should be set on the Drupal encoder, which should then
// set it on our default encoder.
$encoder = new XmlEncoder();
$serialzer = new Serializer([new GetSetMethodNormalizer()]);
$encoder->setSerializer($serialzer);
$base_encoder = $encoder->getBaseEncoder();
$this->assertInstanceOf(BaseXmlEncoder::class, $base_encoder);
// Test the encoder.
$base_encoder->encode(['a' => new TestObject()], 'xml');
}
}
class TestObject {
public function getA() {
return 'A';
}
}
@@ -0,0 +1,153 @@
<?php
namespace Drupal\Tests\serialization\Unit\EntityResolver;
use Drupal\Tests\UnitTestCase;
use Drupal\serialization\EntityResolver\ChainEntityResolver;
/**
* @coversDefaultClass \Drupal\serialization\EntityResolver\ChainEntityResolver
* @group serialization
*/
class ChainEntityResolverTest extends UnitTestCase {
/**
* A mocked normalizer.
*
* @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $testNormalizer;
/**
* Test data passed to the resolve method.
*
* @var \stdClass
*/
protected $testData;
/**
* A test entity type.
*
* @var string
*/
protected $testEntityType = 'test_type';
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->testNormalizer = $this->createMock('Symfony\Component\Serializer\Normalizer\NormalizerInterface');
$this->testData = new \stdClass();
}
/**
* Test the resolve method with no matching resolvers.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithNoneResolved() {
$resolvers = [
$this->createEntityResolverMock(),
$this->createEntityResolverMock(),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertNull($resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Test the resolve method with no matching resolvers, using addResolver.
*
* @covers ::addResolver
* @covers ::resolve
*/
public function testResolverWithNoneResolvedUsingAddResolver() {
$resolver = new ChainEntityResolver();
$resolver->addResolver($this->createEntityResolverMock());
$resolver->addResolver($this->createEntityResolverMock());
$this->assertNull($resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Test the resolve method with a matching resolver first.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithFirstResolved() {
$resolvers = [
$this->createEntityResolverMock(10),
$this->createEntityResolverMock(NULL, FALSE),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertSame(10, $resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Test the resolve method with a matching resolver last.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithLastResolved() {
$resolvers = [
$this->createEntityResolverMock(),
$this->createEntityResolverMock(10),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertSame(10, $resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Test the resolve method where one resolver returns 0.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithResolvedToZero() {
$resolvers = [
$this->createEntityResolverMock(0),
$this->createEntityResolverMock(NULL, FALSE),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertSame(0, $resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Creates a mock entity resolver.
*
* @param null|int $return
* Whether the mocked resolve method should return TRUE or FALSE.
* @param bool $called
* Whether or not the resolve method is expected to be called.
*
* @return \Drupal\serialization\EntityResolver\EntityResolverInterface|\PHPUnit\Framework\MockObject\MockObject
* The mocked entity resolver.
*/
protected function createEntityResolverMock($return = NULL, $called = TRUE) {
$mock = $this->createMock('Drupal\serialization\EntityResolver\EntityResolverInterface');
if ($called) {
$mock->expects($this->once())
->method('resolve')
->with($this->testNormalizer, $this->testData, $this->testEntityType)
->will($this->returnValue($return));
}
else {
$mock->expects($this->never())
->method('resolve');
}
return $mock;
}
}
@@ -0,0 +1,108 @@
<?php
namespace Drupal\Tests\serialization\Unit\EntityResolver;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\serialization\EntityResolver\UuidResolver;
/**
* @coversDefaultClass \Drupal\serialization\EntityResolver\UuidResolver
* @group serialization
*/
class UuidResolverTest extends UnitTestCase {
/**
* The UuidResolver instance.
*
* @var \Drupal\serialization\EntityResolver\UuidResolver
*/
protected $resolver;
/**
* The mock entity repository service.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityRepository;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->entityRepository = $this->createMock(EntityRepositoryInterface::class);
$this->resolver = new UuidResolver($this->entityRepository);
}
/**
* Test resolve() with a class using the incorrect interface.
*/
public function testResolveNotInInterface() {
$this->entityRepository->expects($this->never())
->method('loadEntityByUuid');
$normalizer = $this->createMock('Symfony\Component\Serializer\Normalizer\NormalizerInterface');
$this->assertNull($this->resolver->resolve($normalizer, [], 'test_type'));
}
/**
* Test resolve() with a class using the correct interface but no UUID.
*/
public function testResolveNoUuid() {
$this->entityRepository->expects($this->never())
->method('loadEntityByUuid');
$normalizer = $this->createMock('Drupal\serialization\EntityResolver\UuidReferenceInterface');
$normalizer->expects($this->once())
->method('getUuid')
->with([])
->will($this->returnValue(NULL));
$this->assertNull($this->resolver->resolve($normalizer, [], 'test_type'));
}
/**
* Test resolve() with correct interface but no matching entity for the UUID.
*/
public function testResolveNoEntity() {
$uuid = '392eab92-35c2-4625-872d-a9dab4da008e';
$this->entityRepository->expects($this->once())
->method('loadEntityByUuid')
->with('test_type')
->will($this->returnValue(NULL));
$normalizer = $this->createMock('Drupal\serialization\EntityResolver\UuidReferenceInterface');
$normalizer->expects($this->once())
->method('getUuid')
->with([])
->will($this->returnValue($uuid));
$this->assertNull($this->resolver->resolve($normalizer, [], 'test_type'));
}
/**
* Test resolve() when a UUID corresponds to an entity.
*/
public function testResolveWithEntity() {
$uuid = '392eab92-35c2-4625-872d-a9dab4da008e';
$entity = $this->createMock('Drupal\Core\Entity\EntityInterface');
$entity->expects($this->once())
->method('id')
->will($this->returnValue(1));
$this->entityRepository->expects($this->once())
->method('loadEntityByUuid')
->with('test_type', $uuid)
->will($this->returnValue($entity));
$normalizer = $this->createMock('Drupal\serialization\EntityResolver\UuidReferenceInterface');
$normalizer->expects($this->once())
->method('getUuid')
->with([])
->will($this->returnValue($uuid));
$this->assertSame(1, $this->resolver->resolve($normalizer, [], 'test_type'));
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\Tests\serialization\Unit\EventSubscriber;
use Drupal\serialization\Encoder\JsonEncoder;
use Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Serializer\Serializer;
/**
* @coversDefaultClass \Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber
* @group serialization
*/
class DefaultExceptionSubscriberTest extends UnitTestCase {
/**
* @covers ::on4xx
*/
public function testOn4xx() {
$kernel = $this->prophesize(HttpKernelInterface::class);
$request = Request::create('/test');
$request->setRequestFormat('json');
$e = new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message');
$event = new GetResponseForExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::MASTER_REQUEST, $e);
$subscriber = new DefaultExceptionSubscriber(new Serializer([], [new JsonEncoder()]), []);
$subscriber->on4xx($event);
$response = $event->getResponse();
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals('{"message":"test message"}', $response->getContent());
$this->assertEquals(405, $response->getStatusCode());
$this->assertEquals('POST, PUT', $response->headers->get('Allow'));
$this->assertEquals('application/json', $response->headers->get('Content-Type'));
}
}
@@ -0,0 +1,121 @@
<?php
/**
* @file
* Contains \Drupal\Tests\serialization\Unit\Normalizer\ComplexDataNormalizerTest.
*/
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\serialization\Normalizer\ComplexDataNormalizer;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Serializer\Serializer;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\ComplexDataNormalizer
* @group serialization
*/
class ComplexDataNormalizerTest extends UnitTestCase {
use InternalTypedDataTestTrait;
/**
* Test format string.
*
* @var string
*/
const TEST_FORMAT = 'test_format';
/**
* The Complex data normalizer under test.
*
* @var \Drupal\serialization\Normalizer\ComplexDataNormalizer
*/
protected $normalizer;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->normalizer = new ComplexDataNormalizer();
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization() {
$complex_data = $this->prophesize(ComplexDataInterface::class)->reveal();
$this->assertTrue($this->normalizer->supportsNormalization($complex_data));
// Also test that an object not implementing ComplexDataInterface fails.
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* Test normalizing complex data.
*
* @covers ::normalize
*/
public function testNormalizeComplexData() {
$serializer_prophecy = $this->prophesize(Serializer::class);
$non_internal_property = $this->getTypedDataProperty(FALSE);
$serializer_prophecy->normalize($non_internal_property, static::TEST_FORMAT, [])
->willReturn('A-normalized')
->shouldBeCalled();
$this->normalizer->setSerializer($serializer_prophecy->reveal());
$complex_data = $this->prophesize(ComplexDataInterface::class);
$complex_data->getProperties(TRUE)
->willReturn([
'prop:a' => $non_internal_property,
'prop:internal' => $this->getTypedDataProperty(TRUE),
])
->shouldBeCalled();
$normalized = $this->normalizer->normalize($complex_data->reveal(), static::TEST_FORMAT);
$this->assertEquals(['prop:a' => 'A-normalized'], $normalized);
}
/**
* Test normalize() where $object does not implement ComplexDataInterface.
*
* Normalizers extending ComplexDataNormalizer may have a different supported
* class.
*
* @covers ::normalize
*/
public function testNormalizeNonComplex() {
$normalizer = new TestExtendedNormalizer();
$serialization_context = ['test' => 'test'];
$serializer_prophecy = $this->prophesize(Serializer::class);
$serializer_prophecy->normalize('A', static::TEST_FORMAT, $serialization_context)
->willReturn('A-normalized')
->shouldBeCalled();
$serializer_prophecy->normalize('B', static::TEST_FORMAT, $serialization_context)
->willReturn('B-normalized')
->shouldBeCalled();
$normalizer->setSerializer($serializer_prophecy->reveal());
$stdClass = new \stdClass();
$stdClass->a = 'A';
$stdClass->b = 'B';
$normalized = $normalizer->normalize($stdClass, static::TEST_FORMAT, $serialization_context);
$this->assertEquals(['a' => 'A-normalized', 'b' => 'B-normalized'], $normalized);
}
}
/**
* Test normalizer with a different supported class.
*/
class TestExtendedNormalizer extends ComplexDataNormalizer {
protected $supportedInterfaceOrClass = \stdClass::class;
}
@@ -0,0 +1,106 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\serialization\Normalizer\ConfigEntityNormalizer;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\ConfigEntityNormalizer
* @group serialization
*/
class ConfigEntityNormalizerTest extends UnitTestCase {
/**
* Tests the normalize() method.
*
* @covers ::normalize
*/
public function testNormalize() {
$test_export_properties = [
'test' => 'test',
'_core' => [
'default_config_hash' => $this->randomMachineName(),
$this->randomMachineName() => 'some random key',
],
];
$entity_field_manager = $this->createMock(EntityFieldManagerInterface::class);
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$entity_type_repository = $this->createMock(EntityTypeRepositoryInterface::class);
$normalizer = new ConfigEntityNormalizer(
$entity_type_manager,
$entity_type_repository,
$entity_field_manager
);
$config_entity = $this->createMock('Drupal\Core\Config\Entity\ConfigEntityInterface');
$config_entity->expects($this->once())
->method('toArray')
->will($this->returnValue($test_export_properties));
$this->assertSame(['test' => 'test'], $normalizer->normalize($config_entity));
}
/**
* @covers ::denormalize
*/
public function testDenormalize() {
$test_value = $this->randomMachineName();
$data = [
'test' => $test_value,
'_core' => [
'default_config_hash' => $this->randomMachineName(),
$this->randomMachineName() => 'some random key',
],
];
$expected_storage_data = [
'test' => $test_value,
];
// Mock of the entity storage, to test our expectation that the '_core' key
// never makes it to that point, thanks to the denormalizer omitting it.
$entity_storage = $this->prophesize(EntityStorageInterface::class);
$entity_storage->create($expected_storage_data)
->shouldBeCalled()
->will(function ($args) {
$entity = new \stdClass();
$entity->received_data = $args[0];
return $entity;
});
// Stubs for the denormalizer going from entity manager to entity storage.
$entity_type_id = $this->randomMachineName();
$entity_type_class = $this->randomMachineName();
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
$entity_type_manager->getDefinition($entity_type_id, FALSE)
->willReturn($this->prophesize(ConfigEntityTypeInterface::class)->reveal());
$entity_type_manager->getStorage($entity_type_id)
->willReturn($entity_storage->reveal());
$entity_type_repository = $this->prophesize(EntityTypeRepositoryInterface::class);
$entity_type_repository->getEntityTypeFromClass($entity_type_class)
->willReturn($entity_type_id);
$entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class);
$normalizer = new ConfigEntityNormalizer($entity_type_manager->reveal(), $entity_type_repository->reveal(), $entity_field_manager->reveal());
// Verify the denormalizer still works correctly: the mock above creates an
// artificial entity object containing exactly the data it received. It also
// should still set _restSubmittedFields correctly.
$expected_denormalization = (object) [
'_restSubmittedFields' => [
'test',
],
'received_data' => [
'test' => $test_value,
],
];
$this->assertEquals($expected_denormalization, $normalizer->denormalize($data, $entity_type_class, 'json'));
}
}
@@ -0,0 +1,171 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\serialization\Normalizer\ContentEntityNormalizer;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\ContentEntityNormalizer
* @group serialization
*/
class ContentEntityNormalizerTest extends UnitTestCase {
/**
* The mock serializer.
*
* @var \Symfony\Component\Serializer\SerializerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $serializer;
/**
* The normalizer under test.
*
* @var \Drupal\serialization\Normalizer\ContentEntityNormalizer
*/
protected $contentEntityNormalizer;
/**
* {@inheritdoc}
*/
protected function setUp() {
$entity_field_manager = $this->createMock(EntityFieldManagerInterface::class);
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$entity_type_repository = $this->createMock(EntityTypeRepositoryInterface::class);
$this->contentEntityNormalizer = new ContentEntityNormalizer($entity_type_manager, $entity_type_repository, $entity_field_manager);
$this->serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')
->disableOriginalConstructor()
->setMethods(['normalize'])
->getMock();
$this->contentEntityNormalizer->setSerializer($this->serializer);
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization() {
$content_mock = $this->createMock('Drupal\Core\Entity\ContentEntityInterface');
$config_mock = $this->createMock('Drupal\Core\Config\Entity\ConfigEntityInterface');
$this->assertTrue($this->contentEntityNormalizer->supportsNormalization($content_mock));
$this->assertFalse($this->contentEntityNormalizer->supportsNormalization($config_mock));
}
/**
* Tests the normalize() method.
*
* @covers ::normalize
*/
public function testNormalize() {
$this->serializer->expects($this->any())
->method('normalize')
->with($this->containsOnlyInstancesOf('Drupal\Core\Field\FieldItemListInterface'), 'test_format', ['account' => NULL])
->will($this->returnValue('test'));
$definitions = [
'field_accessible_external' => $this->createMockFieldListItem(TRUE, FALSE),
'field_non-accessible_external' => $this->createMockFieldListItem(FALSE, FALSE),
'field_accessible_internal' => $this->createMockFieldListItem(TRUE, TRUE),
'field_non-accessible_internal' => $this->createMockFieldListItem(FALSE, TRUE),
];
$content_entity_mock = $this->createMockForContentEntity($definitions);
$normalized = $this->contentEntityNormalizer->normalize($content_entity_mock, 'test_format');
$this->assertArrayHasKey('field_accessible_external', $normalized);
$this->assertEquals('test', $normalized['field_accessible_external']);
$this->assertArrayNotHasKey('field_non-accessible_external', $normalized);
$this->assertArrayNotHasKey('field_accessible_internal', $normalized);
$this->assertArrayNotHasKey('field_non-accessible_internal', $normalized);
}
/**
* Tests the normalize() method with account context passed.
*
* @covers ::normalize
*/
public function testNormalizeWithAccountContext() {
$mock_account = $this->createMock('Drupal\Core\Session\AccountInterface');
$context = [
'account' => $mock_account,
];
$this->serializer->expects($this->any())
->method('normalize')
->with($this->containsOnlyInstancesOf('Drupal\Core\Field\FieldItemListInterface'), 'test_format', $context)
->will($this->returnValue('test'));
// The mock account should get passed directly into the access() method on
// field items from $context['account'].
$definitions = [
'field_1' => $this->createMockFieldListItem(TRUE, FALSE, $mock_account),
'field_2' => $this->createMockFieldListItem(FALSE, FALSE, $mock_account),
];
$content_entity_mock = $this->createMockForContentEntity($definitions);
$normalized = $this->contentEntityNormalizer->normalize($content_entity_mock, 'test_format', $context);
$this->assertArrayHasKey('field_1', $normalized);
$this->assertEquals('test', $normalized['field_1']);
$this->assertArrayNotHasKey('field_2', $normalized);
}
/**
* Creates a mock content entity.
*
* @param $definitions
*
* @return \PHPUnit\Framework\MockObject\MockObject
*/
public function createMockForContentEntity($definitions) {
$content_entity_mock = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase')
->disableOriginalConstructor()
->setMethods(['getTypedData'])
->getMockForAbstractClass();
$typed_data = $this->prophesize(ComplexDataInterface::class);
$typed_data->getProperties(TRUE)
->willReturn($definitions)
->shouldBeCalled();
$content_entity_mock->expects($this->any())
->method('getTypedData')
->will($this->returnValue($typed_data->reveal()));
return $content_entity_mock;
}
/**
* Creates a mock field list item.
*
* @param bool $access
* @param bool $internal
* @param \Drupal\Core\Session\AccountInterface $user_context
*
* @return \Drupal\Core\Field\FieldItemListInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected function createMockFieldListItem($access, $internal, AccountInterface $user_context = NULL) {
$data_definition = $this->prophesize(DataDefinitionInterface::class);
$mock = $this->createMock('Drupal\Core\Field\FieldItemListInterface');
$mock->expects($this->once())
->method('getDataDefinition')
->will($this->returnValue($data_definition->reveal()));
$data_definition->isInternal()
->willReturn($internal)
->shouldBeCalled();
if (!$internal) {
$mock->expects($this->once())
->method('access')
->with('view', $user_context)
->will($this->returnValue($access));
}
return $mock;
}
}
@@ -0,0 +1,286 @@
<?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->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('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->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('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->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
$this->normalizer->denormalize('', DateTimeIso8601::class, NULL, []);
}
}
/**
* Note: Prophecy does not support magic methods. By subclassing and specifying
* an explicit method, Prophecy works.
* @see https://github.com/phpspec/prophecy/issues/338
* @see https://github.com/phpspec/prophecy/issues/34
* @see https://github.com/phpspec/prophecy/issues/80
*/
class DateTimeIso8601NormalizerTestDrupalDateTime extends DrupalDateTime {
public function setTimezone(\DateTimeZone $timezone) {
parent::setTimezone($timezone);
}
}
@@ -0,0 +1,193 @@
<?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->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "Y-m-d\TH:i:sP" (RFC 3339), "Y-m-d\TH:i:sO" (ISO 8601).');
$normalized = '2016/11/06 09:02am GMT';
$this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, []);
}
}
/**
* Note: Prophecy does not support magic methods. By subclassing and specifying
* an explicit method, Prophecy works.
* @see https://github.com/phpspec/prophecy/issues/338
* @see https://github.com/phpspec/prophecy/issues/34
* @see https://github.com/phpspec/prophecy/issues/80
*/
class DateTimeNormalizerTestDrupalDateTime extends DrupalDateTime {
public function setTimezone(\DateTimeZone $timezone) {
parent::setTimezone($timezone);
}
}
@@ -0,0 +1,432 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\serialization\Normalizer\EntityNormalizer;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\EntityNormalizer
* @group serialization
*/
class EntityNormalizerTest extends UnitTestCase {
/**
* The mock entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityFieldManager;
/**
* The mock entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityTypeManager;
/**
* The mock entity type repository.
*
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $entityTypeRepository;
/**
* The mock serializer.
*
* @var \Symfony\Component\Serializer\SerializerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $serializer;
/**
* The entity normalizer.
*
* @var \Drupal\serialization\Normalizer\EntityNormalizer
*/
protected $entityNormalizer;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->entityFieldManager = $this->createMock(EntityFieldManagerInterface::class);
$this->entityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
$this->entityTypeRepository = $this->createMock(EntityTypeRepositoryInterface::class);
$this->entityNormalizer = new EntityNormalizer(
$this->entityTypeManager,
$this->entityTypeRepository,
$this->entityFieldManager
);
}
/**
* Tests the normalize() method.
*
* @covers ::normalize
*/
public function testNormalize() {
$list_item_1 = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
$list_item_2 = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
$definitions = [
'field_1' => $list_item_1,
'field_2' => $list_item_2,
];
$content_entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase')
->disableOriginalConstructor()
->setMethods(['getFields'])
->getMockForAbstractClass();
$content_entity->expects($this->once())
->method('getFields')
->will($this->returnValue($definitions));
$serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')
->disableOriginalConstructor()
->setMethods(['normalize'])
->getMock();
$serializer->expects($this->at(0))
->method('normalize')
->with($list_item_1, 'test_format');
$serializer->expects($this->at(1))
->method('normalize')
->with($list_item_2, 'test_format');
$this->entityNormalizer->setSerializer($serializer);
$this->entityNormalizer->normalize($content_entity, 'test_format');
}
/**
* Tests the denormalize() method with no entity type provided in context.
*
* @covers ::denormalize
*/
public function testDenormalizeWithNoEntityType() {
$this->expectException(UnexpectedValueException::class);
$this->entityNormalizer->denormalize([], 'Drupal\Core\Entity\ContentEntityBase');
}
/**
* Tests the denormalize method with a bundle property.
*
* @covers ::denormalize
*/
public function testDenormalizeWithValidBundle() {
$test_data = [
'key_1' => 'value_1',
'key_2' => 'value_2',
'test_type' => [
['name' => 'test_bundle'],
],
];
$entity_type = $this->createMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->once())
->method('id')
->willReturn('test');
$entity_type->expects($this->once())
->method('hasKey')
->with('bundle')
->will($this->returnValue(TRUE));
$entity_type->expects($this->once())
->method('getKey')
->with('bundle')
->will($this->returnValue('test_type'));
$entity_type->expects($this->once())
->method('entityClassImplements')
->with(FieldableEntityInterface::class)
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('getBundleEntityType')
->will($this->returnValue('test_bundle'));
$entity_type_storage_definition = $this->createMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$entity_type_storage_definition->expects($this->once())
->method('getMainPropertyName')
->will($this->returnValue('name'));
$entity_type_definition = $this->createMock('Drupal\Core\Field\FieldDefinitionInterface');
$entity_type_definition->expects($this->once())
->method('getFieldStorageDefinition')
->will($this->returnValue($entity_type_storage_definition));
$base_definitions = [
'test_type' => $entity_type_definition,
];
$this->entityTypeManager->expects($this->at(0))
->method('getDefinition')
->with('test')
->will($this->returnValue($entity_type));
$this->entityFieldManager->expects($this->at(0))
->method('getBaseFieldDefinitions')
->with('test')
->will($this->returnValue($base_definitions));
$entity_query_mock = $this->createMock('Drupal\Core\Entity\Query\QueryInterface');
$entity_query_mock->expects($this->once())
->method('execute')
->will($this->returnValue(['test_bundle' => 'test_bundle']));
$entity_type_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$entity_type_storage->expects($this->once())
->method('getQuery')
->will($this->returnValue($entity_query_mock));
$this->entityTypeManager->expects($this->at(1))
->method('getStorage')
->with('test_bundle')
->will($this->returnValue($entity_type_storage));
$key_1 = $this->createMock(FieldItemListInterface::class);
$key_2 = $this->createMock(FieldItemListInterface::class);
$entity = $this->createMock(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->createMock('Drupal\Core\Entity\EntityStorageInterface');
// Create should only be called with the bundle property at first.
$expected_test_data = [
'test_type' => 'test_bundle',
];
$storage->expects($this->once())
->method('create')
->with($expected_test_data)
->will($this->returnValue($entity));
$this->entityTypeManager->expects($this->at(2))
->method('getStorage')
->with('test')
->will($this->returnValue($storage));
// 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 a bundle property.
*
* @covers ::denormalize
*/
public function testDenormalizeWithInvalidBundle() {
$test_data = [
'key_1' => 'value_1',
'key_2' => 'value_2',
'test_type' => [
['name' => 'test_bundle'],
],
];
$entity_type = $this->createMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->once())
->method('id')
->willReturn('test');
$entity_type->expects($this->once())
->method('hasKey')
->with('bundle')
->will($this->returnValue(TRUE));
$entity_type->expects($this->once())
->method('getKey')
->with('bundle')
->will($this->returnValue('test_type'));
$entity_type->expects($this->once())
->method('entityClassImplements')
->with(FieldableEntityInterface::class)
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('getBundleEntityType')
->will($this->returnValue('test_bundle'));
$entity_type_storage_definition = $this->createMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$entity_type_storage_definition->expects($this->once())
->method('getMainPropertyName')
->will($this->returnValue('name'));
$entity_type_definition = $this->createMock('Drupal\Core\Field\FieldDefinitionInterface');
$entity_type_definition->expects($this->once())
->method('getFieldStorageDefinition')
->will($this->returnValue($entity_type_storage_definition));
$base_definitions = [
'test_type' => $entity_type_definition,
];
$this->entityTypeManager->expects($this->at(0))
->method('getDefinition')
->with('test')
->will($this->returnValue($entity_type));
$this->entityFieldManager->expects($this->at(0))
->method('getBaseFieldDefinitions')
->with('test')
->will($this->returnValue($base_definitions));
$entity_query_mock = $this->createMock('Drupal\Core\Entity\Query\QueryInterface');
$entity_query_mock->expects($this->once())
->method('execute')
->will($this->returnValue(['test_bundle_other' => 'test_bundle_other']));
$entity_type_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$entity_type_storage->expects($this->once())
->method('getQuery')
->will($this->returnValue($entity_query_mock));
$this->entityTypeManager->expects($this->at(1))
->method('getStorage')
->with('test_bundle')
->will($this->returnValue($entity_type_storage));
$this->expectException(UnexpectedValueException::class);
$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 testDenormalizeWithNoBundle() {
$test_data = [
'key_1' => 'value_1',
'key_2' => 'value_2',
];
$entity_type = $this->createMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->once())
->method('entityClassImplements')
->with(FieldableEntityInterface::class)
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('hasKey')
->with('bundle')
->will($this->returnValue(FALSE));
$entity_type->expects($this->never())
->method('getKey');
$this->entityTypeManager->expects($this->once())
->method('getDefinition')
->with('test')
->will($this->returnValue($entity_type));
$key_1 = $this->createMock(FieldItemListInterface::class);
$key_2 = $this->createMock(FieldItemListInterface::class);
$entity = $this->createMock(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->createMock('Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->once())
->method('create')
->with([])
->will($this->returnValue($entity));
$this->entityTypeManager->expects($this->once())
->method('getStorage')
->with('test')
->will($this->returnValue($storage));
$this->entityFieldManager->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->createMock('Drupal\Core\Entity\EntityTypeInterface');
$entity_type->expects($this->once())
->method('entityClassImplements')
->with(FieldableEntityInterface::class)
->willReturn(FALSE);
$entity_type->expects($this->never())
->method('getKey');
$this->entityTypeManager->expects($this->once())
->method('getDefinition')
->with('test')
->will($this->returnValue($entity_type));
$storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->once())
->method('create')
->with($test_data)
->will($this->returnValue($this->createMock('Drupal\Core\Entity\EntityInterface')));
$this->entityTypeManager->expects($this->once())
->method('getStorage')
->with('test')
->will($this->returnValue($storage));
$this->entityFieldManager->expects($this->never())
->method('getBaseFieldDefinitions');
$this->assertNotNull($this->entityNormalizer->denormalize($test_data, 'Drupal\Core\Entity\ContentEntityBase', NULL, ['entity_type' => 'test']));
}
}
@@ -0,0 +1,506 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\GeneratedUrl;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Url;
use Drupal\locale\StringInterface;
use Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizer;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Serializer;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizer
* @group serialization
*/
class EntityReferenceFieldItemNormalizerTest extends UnitTestCase {
use InternalTypedDataTestTrait;
/**
* The mock serializer.
*
* @var \Symfony\Component\Serializer\SerializerInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $serializer;
/**
* The normalizer under test.
*
* @var \Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizer
*/
protected $normalizer;
/**
* The mock field item.
*
* @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem|\Prophecy\Prophecy\ObjectProphecy
*/
protected $fieldItem;
/**
* The mock entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $entityRepository;
/**
* The mock field definition.
*
* @var \Drupal\Core\Field\FieldDefinitionInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $fieldDefinition;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->entityRepository = $this->prophesize(EntityRepositoryInterface::class);
$this->normalizer = new EntityReferenceFieldItemNormalizer($this->entityRepository->reveal());
$this->serializer = $this->prophesize(Serializer::class);
// Set up the serializer to return an entity property.
$this->serializer->normalize(Argument::cetera())
->willReturn('test');
$this->normalizer->setSerializer($this->serializer->reveal());
$this->fieldItem = $this->prophesize(EntityReferenceItem::class);
$this->fieldItem->getIterator()
->willReturn(new \ArrayIterator(['target_id' => []]));
$this->fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
$this->fieldDefinition->getItemDefinition()
->willReturn($this->prophesize(FieldItemDataDefinition::class)->reveal());
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization() {
$this->assertTrue($this->normalizer->supportsNormalization($this->fieldItem->reveal()));
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* @covers ::supportsDenormalization
*/
public function testSupportsDenormalization() {
$this->assertTrue($this->normalizer->supportsDenormalization([], EntityReferenceItem::class));
$this->assertFalse($this->normalizer->supportsDenormalization([], FieldItemInterface::class));
}
/**
* @covers ::normalize
*/
public function testNormalize() {
$test_url = '/test/100';
$generated_url = (new GeneratedUrl())->setGeneratedUrl($test_url);
$url = $this->prophesize(Url::class);
$url->toString(TRUE)
->willReturn($generated_url);
$entity = $this->prophesize(EntityInterface::class);
$entity->hasLinkTemplate('canonical')
->willReturn(TRUE);
$entity->isNew()
->willReturn(FALSE)
->shouldBeCalled();
$entity->toUrl('canonical')
->willReturn($url)
->shouldBeCalled();
$entity->uuid()
->willReturn('080e3add-f9d5-41ac-9821-eea55b7b42fb')
->shouldBeCalled();
$entity->getEntityTypeId()
->willReturn('test_type')
->shouldBeCalled();
$entity_reference = $this->prophesize(TypedDataInterface::class);
$entity_reference->getValue()
->willReturn($entity->reveal())
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('test_type');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference)
->shouldBeCalled();
$this->fieldItem->getProperties(TRUE)
->willReturn(['target_id' => $this->getTypedDataProperty(FALSE)])
->shouldBeCalled();
$normalized = $this->normalizer->normalize($this->fieldItem->reveal());
$expected = [
'target_id' => 'test',
'target_type' => 'test_type',
'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb',
'url' => $test_url,
];
$this->assertSame($expected, $normalized);
}
public function testNormalizeWithNewEntityReference() {
$test_url = '/test/100';
$generated_url = (new GeneratedUrl())->setGeneratedUrl($test_url);
$url = $this->prophesize(Url::class);
$url->toString(TRUE)
->willReturn($generated_url);
$entity = $this->prophesize(EntityInterface::class);
$entity->hasLinkTemplate('canonical')
->willReturn(TRUE);
$entity->isNew()
->willReturn(TRUE)
->shouldBeCalled();
$entity->uuid()
->willReturn('080e3add-f9d5-41ac-9821-eea55b7b42fb')
->shouldBeCalled();
$entity->getEntityTypeId()
->willReturn('test_type')
->shouldBeCalled();
$entity->toUrl('canonical')
->willReturn($url)
->shouldNotBeCalled();
$entity_reference = $this->prophesize(TypedDataInterface::class);
$entity_reference->getValue()
->willReturn($entity->reveal())
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('test_type');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference)
->shouldBeCalled();
$this->fieldItem->getProperties(TRUE)
->willReturn(['target_id' => $this->getTypedDataProperty(FALSE)])
->shouldBeCalled();
$normalized = $this->normalizer->normalize($this->fieldItem->reveal());
$expected = [
'target_id' => 'test',
'target_type' => 'test_type',
'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb',
];
$this->assertSame($expected, $normalized);
}
/**
* @covers ::normalize
*/
public function testNormalizeWithEmptyTaxonomyTermReference() {
// Override the serializer prophecy from setUp() to return a zero value.
$this->serializer = $this->prophesize(Serializer::class);
// Set up the serializer to return an entity property.
$this->serializer->normalize(Argument::cetera())
->willReturn(0);
$this->normalizer->setSerializer($this->serializer->reveal());
$entity_reference = $this->prophesize(TypedDataInterface::class);
$entity_reference->getValue()
->willReturn(NULL)
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('taxonomy_term');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference)
->shouldBeCalled();
$this->fieldItem->getProperties(TRUE)
->willReturn(['target_id' => $this->getTypedDataProperty(FALSE)])
->shouldBeCalled();
$normalized = $this->normalizer->normalize($this->fieldItem->reveal());
$expected = [
'target_id' => NULL,
];
$this->assertSame($expected, $normalized);
}
/**
* @covers ::normalize
*/
public function testNormalizeWithNoEntity() {
$entity_reference = $this->prophesize(TypedDataInterface::class);
$entity_reference->getValue()
->willReturn(NULL)
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('test_type');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference->reveal())
->shouldBeCalled();
$this->fieldItem->getProperties(TRUE)
->willReturn(['target_id' => $this->getTypedDataProperty(FALSE)])
->shouldBeCalled();
$normalized = $this->normalizer->normalize($this->fieldItem->reveal());
$expected = [
'target_id' => 'test',
];
$this->assertSame($expected, $normalized);
}
/**
* @covers ::denormalize
*/
public function testDenormalizeWithTypeAndUuid() {
$data = [
'target_id' => 'test',
'target_type' => 'test_type',
'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb',
];
$entity = $this->prophesize(FieldableEntityInterface::class);
$entity->id()
->willReturn('test')
->shouldBeCalled();
$this->entityRepository
->loadEntityByUuid($data['target_type'], $data['target_uuid'])
->willReturn($entity)
->shouldBeCalled();
$this->fieldItem->getProperties()->willReturn([
'target_id' => $this->prophesize(IntegerInterface::class),
]);
$this->fieldItem->setValue(['target_id' => 'test'])->shouldBeCalled();
$this->assertDenormalize($data);
}
/**
* @covers ::denormalize
*/
public function testDenormalizeWithUuidWithoutType() {
$data = [
'target_id' => 'test',
'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb',
];
$entity = $this->prophesize(FieldableEntityInterface::class);
$entity->id()
->willReturn('test')
->shouldBeCalled();
$this->entityRepository
->loadEntityByUuid('test_type', $data['target_uuid'])
->willReturn($entity)
->shouldBeCalled();
$this->fieldItem->getProperties()->willReturn([
'target_id' => $this->prophesize(IntegerInterface::class),
]);
$this->fieldItem->setValue(['target_id' => 'test'])->shouldBeCalled();
$this->assertDenormalize($data);
}
/**
* @covers ::denormalize
*/
public function testDenormalizeWithUuidWithIncorrectType() {
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('The field "field_reference" property "target_type" must be set to "test_type" or omitted.');
$data = [
'target_id' => 'test',
'target_type' => 'wrong_type',
'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb',
];
$this->fieldDefinition
->getName()
->willReturn('field_reference')
->shouldBeCalled();
$this->assertDenormalize($data);
}
/**
* @covers ::denormalize
*/
public function testDenormalizeWithTypeWithIncorrectUuid() {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('No "test_type" entity found with UUID "unique-but-none-non-existent" for field "field_reference"');
$data = [
'target_id' => 'test',
'target_type' => 'test_type',
'target_uuid' => 'unique-but-none-non-existent',
];
$this->entityRepository
->loadEntityByUuid($data['target_type'], $data['target_uuid'])
->willReturn(NULL)
->shouldBeCalled();
$this->fieldItem
->getName()
->willReturn('field_reference')
->shouldBeCalled();
$this->assertDenormalize($data);
}
/**
* @covers ::denormalize
*/
public function testDenormalizeWithEmtpyUuid() {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('If provided "target_uuid" cannot be empty for field "test_type".');
$data = [
'target_id' => 'test',
'target_type' => 'test_type',
'target_uuid' => '',
];
$this->fieldItem
->getName()
->willReturn('field_reference')
->shouldBeCalled();
$this->assertDenormalize($data);
}
/**
* @covers ::denormalize
*/
public function testDenormalizeWithId() {
$data = [
'target_id' => 'test',
];
$this->fieldItem->setValue($data)->shouldBeCalled();
$this->assertDenormalize($data);
}
/**
* Asserts denormalization process is correct for give data.
*
* @param array $data
* The data to denormalize.
*/
protected function assertDenormalize(array $data) {
$this->fieldItem->getParent()
->willReturn($this->prophesize(FieldItemListInterface::class)->reveal());
$this->fieldItem->getFieldDefinition()->willReturn($this->fieldDefinition->reveal());
if (!empty($data['target_uuid'])) {
$this->fieldDefinition
->getSetting('target_type')
->willReturn('test_type')
->shouldBeCalled();
}
// Avoid a static method call by returning dummy serialized property data.
$this->fieldDefinition
->getFieldStorageDefinition()
->willReturn()
->shouldBeCalled();
$this->fieldDefinition
->getName()
->willReturn('field_reference')
->shouldBeCalled();
$entity = $this->prophesize(EntityInterface::class);
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity->getEntityType()
->willReturn($entity_type->reveal())
->shouldBeCalled();
$this->fieldItem
->getPluginDefinition()
->willReturn([
'serialized_property_names' => [
'foo' => 'bar',
],
])
->shouldBeCalled();
$this->fieldItem
->getEntity()
->willReturn($entity->reveal())
->shouldBeCalled();
$context = ['target_instance' => $this->fieldItem->reveal()];
$denormalized = $this->normalizer->denormalize($data, EntityReferenceItem::class, 'json', $context);
$this->assertSame($context['target_instance'], $denormalized);
}
/**
* @covers ::constructValue
*/
public function testConstructValueProperties() {
$data = [
'target_id' => 'test',
'target_type' => 'test_type',
'target_uuid' => '080e3add-f9d5-41ac-9821-eea55b7b42fb',
'extra_property' => 'extra_value',
];
$entity = $this->prophesize(FieldableEntityInterface::class);
$entity->id()
->willReturn('test')
->shouldBeCalled();
$this->entityRepository
->loadEntityByUuid($data['target_type'], $data['target_uuid'])
->willReturn($entity)
->shouldBeCalled();
$this->fieldItem->getProperties()->willReturn([
'target_id' => $this->prophesize(IntegerInterface::class),
'extra_property' => $this->prophesize(StringInterface::class),
]);
$this->fieldItem->setValue([
'target_id' => 'test',
'extra_property' => 'extra_value',
])->shouldBeCalled();
$this->assertDenormalize($data);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Trait that provides mocked typed data objects.
*/
trait InternalTypedDataTestTrait {
/**
* Gets a typed data property.
*
* @param bool $internal
* Whether the typed data property is internal.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The typed data property.
*/
protected function getTypedDataProperty($internal = TRUE) {
$definition = $this->prophesize(DataDefinitionInterface::class);
$definition->isInternal()
->willReturn($internal)
->shouldBeCalled();
$definition = $definition->reveal();
$property = $this->prophesize(TypedDataInterface::class);
$property->getDataDefinition()
->willReturn($definition)
->shouldBeCalled();
return $property->reveal();
}
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\serialization\Normalizer\ListNormalizer;
use Drupal\Core\TypedData\Plugin\DataType\ItemList;
use Symfony\Component\Serializer\Serializer;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\ListNormalizer
* @group serialization
*/
class ListNormalizerTest extends UnitTestCase {
/**
* The ListNormalizer instance.
*
* @var \Drupal\serialization\Normalizer\ListNormalizer
*/
protected $normalizer;
/**
* The mock list instance.
*
* @var \Drupal\Core\TypedData\ListInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $list;
/**
* The expected list values to use for testing.
*
* @var array
*/
protected $expectedListValues = ['test', 'test', 'test'];
/**
* The mocked typed data.
*
* @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\Core\TypedData\TypedDataInterface
*/
protected $typedData;
protected function setUp() {
// Mock the TypedDataManager to return a TypedDataInterface mock.
$this->typedData = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
$typed_data_manager = $this->createMock(TypedDataManagerInterface::class);
$typed_data_manager->expects($this->any())
->method('getPropertyInstance')
->will($this->returnValue($this->typedData));
// Set up a mock container as ItemList() will call for the 'typed_data_manager'
// service.
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')
->setMethods(['get'])
->getMock();
$container->expects($this->any())
->method('get')
->with($this->equalTo('typed_data_manager'))
->will($this->returnValue($typed_data_manager));
\Drupal::setContainer($container);
$this->normalizer = new ListNormalizer();
$this->list = new ItemList(new DataDefinition());
$this->list->setValue($this->expectedListValues);
}
/**
* Tests the supportsNormalization() method.
*/
public function testSupportsNormalization() {
$this->assertTrue($this->normalizer->supportsNormalization($this->list));
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* Tests the normalize() method.
*/
public function testNormalize() {
$serializer = $this->prophesize(Serializer::class);
$serializer->normalize($this->typedData, 'json', ['mu' => 'nu'])
->shouldBeCalledTimes(3)
->willReturn('test');
$this->normalizer->setSerializer($serializer->reveal());
$normalized = $this->normalizer->normalize($this->list, 'json', ['mu' => 'nu']);
$this->assertEquals($this->expectedListValues, $normalized);
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\Tests\serialization\Unit\Normalizer\NormalizerBaseTest.
*/
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Tests\UnitTestCase;
use Drupal\serialization\Normalizer\NormalizerBase;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\NormalizerBase
* @group serialization
*/
class NormalizerBaseTest extends UnitTestCase {
/**
* Tests the supportsNormalization method.
*
* @dataProvider providerTestSupportsNormalization
*
* @param bool $expected_return
* The expected boolean return value from supportNormalization.
* @param mixed $data
* The data passed to supportsNormalization.
* @param string $supported_interface_or_class
* (optional) The supported interface or class to set on the normalizer.
*/
public function testSupportsNormalization($expected_return, $data, $supported_interface_or_class = NULL) {
$normalizer_base = $this->getMockForAbstractClass('Drupal\Tests\serialization\Unit\Normalizer\TestNormalizerBase');
if (isset($supported_interface_or_class)) {
$normalizer_base->setSupportedInterfaceOrClass($supported_interface_or_class);
}
$this->assertSame($expected_return, $normalizer_base->supportsNormalization($data));
}
/**
* Data provider for testSupportsNormalization.
*
* @return array
* An array of provider data for testSupportsNormalization.
*/
public function providerTestSupportsNormalization() {
return [
// Something that is not an object should return FALSE immediately.
[FALSE, []],
// An object with no class set should return FALSE.
[FALSE, new \stdClass()],
// Set a supported Class.
[TRUE, new \stdClass(), 'stdClass'],
// Set a supported interface.
[TRUE, new \RecursiveArrayIterator(), 'RecursiveIterator'],
// Set a different class.
[FALSE, new \stdClass(), 'ArrayIterator'],
// Set a different interface.
[FALSE, new \stdClass(), 'RecursiveIterator'],
];
}
}
/**
* Test class for NormalizerBase.
*/
abstract class TestNormalizerBase extends NormalizerBase {
/**
* Sets the protected supportedInterfaceOrClass property.
*
* @param string $supported_interface_or_class
* The class name to set.
*/
public function setSupportedInterfaceOrClass($supported_interface_or_class) {
$this->supportedInterfaceOrClass = $supported_interface_or_class;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\serialization\Normalizer\NullNormalizer;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\NullNormalizer
* @group serialization
*/
class NullNormalizerTest extends UnitTestCase {
/**
* The NullNormalizer instance.
*
* @var \Drupal\serialization\Normalizer\NullNormalizer
*/
protected $normalizer;
/**
* The interface to use in testing.
*
* @var string
*/
protected $interface = 'Drupal\Core\TypedData\TypedDataInterface';
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->normalizer = new NullNormalizer($this->interface);
}
/**
* @covers ::__construct
* @covers ::supportsNormalization
*/
public function testSupportsNormalization() {
$mock = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
$this->assertTrue($this->normalizer->supportsNormalization($mock));
// Also test that an object not implementing TypedDataInterface fails.
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* @covers ::normalize
*/
public function testNormalize() {
$mock = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
$this->assertNull($this->normalizer->normalize($mock));
}
}
@@ -0,0 +1,101 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
use Drupal\Core\TypedData\Plugin\DataType\StringData;
use Drupal\Tests\UnitTestCase;
use Drupal\serialization\Normalizer\PrimitiveDataNormalizer;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\PrimitiveDataNormalizer
* @group serialization
*/
class PrimitiveDataNormalizerTest extends UnitTestCase {
/**
* The TypedDataNormalizer instance.
*
* @var \Drupal\serialization\Normalizer\TypedDataNormalizer
*/
protected $normalizer;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->normalizer = new PrimitiveDataNormalizer();
}
/**
* @covers ::supportsNormalization
* @dataProvider dataProviderPrimitiveData
*/
public function testSupportsNormalization($primitive_data, $expected) {
$this->assertTrue($this->normalizer->supportsNormalization($primitive_data));
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalizationFail() {
// Test that an object not implementing PrimitiveInterface fails.
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* @covers ::normalize
* @dataProvider dataProviderPrimitiveData
*/
public function testNormalize($primitive_data, $expected) {
$this->assertSame($expected, $this->normalizer->normalize($primitive_data));
}
/**
* Data provider for testNormalize().
*/
public function dataProviderPrimitiveData() {
$data = [];
$definition = DataDefinition::createFromDataType('string');
$string = new StringData($definition, 'string');
$string->setValue('test');
$data['string'] = [$string, 'test'];
$definition = DataDefinition::createFromDataType('string');
$string = new StringData($definition, 'string');
$string->setValue(NULL);
$data['string-null'] = [$string, NULL];
$definition = DataDefinition::createFromDataType('integer');
$integer = new IntegerData($definition, 'integer');
$integer->setValue(5);
$data['integer'] = [$integer, 5];
$definition = DataDefinition::createFromDataType('integer');
$integer = new IntegerData($definition, 'integer');
$integer->setValue(NULL);
$data['integer-null'] = [$integer, NULL];
$definition = DataDefinition::createFromDataType('boolean');
$boolean = new BooleanData($definition, 'boolean');
$boolean->setValue(TRUE);
$data['boolean'] = [$boolean, TRUE];
$definition = DataDefinition::createFromDataType('boolean');
$boolean = new BooleanData($definition, 'boolean');
$boolean->setValue(NULL);
$data['boolean-null'] = [$boolean, NULL];
return $data;
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\serialization_test\TimeStampItemNormalizerTraitDeprecatedTestClass;
use Drupal\Tests\UnitTestCase;
/**
* Tests that TimeStampItemNormalizerTrait throws a deprecation error.
*
* @group serialization
* @group legacy
* @coversDefaultClass \Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait
*/
class TimeStampItemNormalizerTraitDeprecatedTest extends UnitTestCase {
/**
* Tests that TimeStampItemNormalizerTrait throws a deprecation error.
*
* @expectedDeprecation Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait is deprecated in Drupal 8.7.0 and will be removed in Drupal 9.0.0. Use \Drupal\serialization\Normalizer\TimestampNormalizer instead.
*/
public function testDeprecated() {
$test = new TimeStampItemNormalizerTraitDeprecatedTestClass();
}
}
@@ -0,0 +1,182 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
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\Serializer;
/**
* 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 {
/**
* @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));
}
/**
* @covers ::normalize
* @see \Drupal\Tests\serialization\Unit\Normalizer\TimestampNormalizerTest
*/
public function testNormalize() {
// 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->getProperties(TRUE)
->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);
// 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(['value' => $timestamp_datetype_normalization, 'format' => \DateTime::RFC3339], $normalized);
}
/**
* @covers ::denormalize
*/
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 get the Timestamp @DataType denormalization set as
// a value, in FieldItemNormalizer::denormalize().
$timestamp_item->setValue(['value' => $timestamp_data_denormalization])
->shouldBeCalled();
// Avoid a static method call by returning dummy serialized property data.
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$timestamp_item
->getFieldDefinition()
->willReturn($field_definition->reveal())
->shouldBeCalled();
$timestamp_item->getPluginDefinition()
->willReturn([
'serialized_property_names' => [
'foo' => 'bar',
],
])
->shouldBeCalled();
$entity = $this->prophesize(EntityInterface::class);
$entity_type = $this->prophesize(EntityTypeInterface::class);
$entity->getEntityType()
->willReturn($entity_type->reveal())
->shouldBeCalled();
$timestamp_item
->getEntity()
->willReturn($entity->reveal())
->shouldBeCalled();
$context = [
'target_instance' => $timestamp_item->reveal(),
'datetime_allowed_formats' => [\DateTime::RFC3339],
];
// 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->assertInstanceOf(TimestampItem::class, $denormalized);
}
/**
* 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;
}
}
@@ -0,0 +1,148 @@
<?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->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('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);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Tests\UnitTestCase;
use Drupal\serialization\Normalizer\TypedDataNormalizer;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\TypedDataNormalizer
* @group serialization
*/
class TypedDataNormalizerTest extends UnitTestCase {
/**
* The TypedDataNormalizer instance.
*
* @var \Drupal\serialization\Normalizer\TypedDataNormalizer
*/
protected $normalizer;
/**
* The mock typed data instance.
*
* @var \Drupal\Core\TypedData\TypedDataInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $typedData;
protected function setUp() {
$this->normalizer = new TypedDataNormalizer();
$this->typedData = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
}
/**
* Tests the supportsNormalization() method.
*/
public function testSupportsNormalization() {
$this->assertTrue($this->normalizer->supportsNormalization($this->typedData));
// Also test that an object not implementing TypedDataInterface fails.
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* Tests the normalize() method.
*/
public function testNormalize() {
$this->typedData->expects($this->once())
->method('getValue')
->will($this->returnValue('test'));
$this->assertEquals('test', $this->normalizer->normalize($this->typedData));
}
}