first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
name: Serialization
type: module
description: 'Provides a service for converting data to and from formats such as JSON and XML.'
package: Web services
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,13 @@
<?php
/**
* @file
* Update functions for the Serialization module.
*/
/**
* Implements hook_update_last_removed().
*/
function serialization_update_last_removed() {
return 8401;
}

View File

@@ -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 .= '<h2>' . t('About') . '</h2>';
$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;
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Post update functions for Serialization module.
*/
/**
* Implements hook_removed_post_updates().
*/
function serialization_removed_post_updates() {
return [
'serialization_post_update_delete_settings' => '10.0.0',
];
}

View File

@@ -0,0 +1,113 @@
services:
_defaults:
autoconfigure: true
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 }
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 }
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 }
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
Drupal\serialization\EntityResolver\ChainEntityResolverInterface: '@serializer.entity_resolver'
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
arguments: ['@serializer', '%serializer.formats%']
serialization.user_route_alter_subscriber:
class: Drupal\serialization\EventSubscriber\UserRouteAlterSubscriber
arguments: ['%serializer.formats%']

View File

@@ -0,0 +1,51 @@
<?php
namespace Drupal\serialization\Encoder;
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 {
/**
* The formats that this Encoder supports.
*
* @var array
*/
protected static $format = ['json', 'ajax'];
/**
* {@inheritdoc}
*/
public function __construct(?JsonEncode $encodingImpl = NULL, ?JsonDecode $decodingImpl = NULL) {
// 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;
$this->encodingImpl = $encodingImpl ?: new JsonEncode([JsonEncode::OPTIONS => $json_encoding_options]);
$this->decodingImpl = $decodingImpl ?: new JsonDecode([JsonDecode::ASSOCIATIVE => TRUE]);
}
/**
* {@inheritdoc}
*/
public function supportsEncoding(string $format, array $context = []): bool {
return in_array($format, static::$format);
}
/**
* {@inheritdoc}
*/
public function supportsDecoding(string $format, array $context = []): bool {
return in_array($format, static::$format);
}
}

View File

@@ -0,0 +1,92 @@
<?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
* The XML encoder.
*/
public function setBaseEncoder($encoder) {
$this->baseEncoder = $encoder;
}
/**
* {@inheritdoc}
*/
public function encode($data, $format, array $context = []): string {
return $this->getBaseEncoder()->encode($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsEncoding(string $format, array $context = []): bool {
return in_array($format, static::$format);
}
/**
* {@inheritdoc}
*/
public function decode($data, $format, array $context = []): mixed {
return $this->getBaseEncoder()->decode($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsDecoding(string $format, array $context = []): bool {
return in_array($format, static::$format);
}
}

View File

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

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\serialization\EntityResolver;
/**
* An interface for delegating an 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);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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\ExceptionEvent;
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\ExceptionEvent $event
* The event to process.
*/
public function on4xx(ExceptionEvent $event) {
/** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
$exception = $event->getThrowable();
$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);
}
}

View File

@@ -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(): array {
$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));
}
}
}
}

View File

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

View File

@@ -0,0 +1,60 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$attributes = [];
// $object will not always match getSupportedTypes().
// @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;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
ComplexDataInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,54 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
return static::getDataWithoutInternals($object->toArray());
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
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]);
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
ConfigEntityInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,41 @@
<?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}
*/
public function normalize($entity, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$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;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
ContentEntityInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,94 @@
<?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}
*/
public function normalize($datetime, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
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 = []): mixed {
// @todo Move the date-only handling out of here in https://www.drupal.org/project/drupal/issues/2958416.
if (isset($context['target_instance'])) {
$field_definition = $context['target_instance']->getFieldDefinition();
}
elseif (isset($context['field_definition'])) {
$field_definition = $context['field_definition'];
}
else {
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]);
$datetime = parent::denormalize($data, $class, $format, $context);
if (!$datetime instanceof \DateTime) {
return $datetime;
}
$datetime->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
DateTimeIso8601::class => TRUE,
];
}
}

View File

@@ -0,0 +1,131 @@
<?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,
];
/**
* 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 = []): array|string|int|float|bool|\ArrayObject|NULL {
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.
->setTimezone($this->getNormalizationTimezone())
->format(\DateTime::RFC3339);
}
/**
* Gets the timezone to be used during normalization.
*
* @see ::normalize
* @see \Drupal\Core\Datetime\DrupalDateTime::prepareTimezone()
*
* @return \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 = []): mixed {
// 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 = $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));
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
DateTimeInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,82 @@
<?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;
/**
* 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, EntityFieldManagerInterface $entity_field_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeRepository = $entity_type_repository;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
$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;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
EntityInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\file\FileInterface;
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;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs an 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 = []): array|string|int|float|bool|\ArrayObject|NULL {
$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
elseif ($entity instanceof FileInterface) {
$values['url'] = $entity->createFileUrl(FALSE);
}
}
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".', $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);
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
EntityReferenceItem::class => TRUE,
];
}
}

View File

@@ -0,0 +1,31 @@
<?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
* The entity reference item.
*
* @return bool
*/
protected function fieldItemReferencesTaxonomyTerm(EntityReferenceItem $field_item) {
return $field_item->getFieldDefinition()->getSetting('target_type') === 'taxonomy_term';
}
}

View File

@@ -0,0 +1,46 @@
<?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}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
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;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
FieldItemInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,61 @@
<?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}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
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;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
FieldItemListInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,255 @@
<?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. The bundle information will be removed.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition
* The entity type definition.
*
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
* If the bundle value is invalid or the bundle type is ineligible.
*
* @return array
* An array containing a single $bundle_key => $bundle_value pair.
*/
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 = $data[$bundle_key][0][$key_id] ?? ($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()->accessCheck(TRUE)->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() {
return $this->entityTypeRepository;
}
/**
* Returns the entity field manager.
*
* @return \Drupal\Core\Entity\EntityFieldManagerInterface
* The entity field manager.
*/
protected function getEntityFieldManager() {
return $this->entityFieldManager;
}
/**
* Returns the entity type manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The entity type manager.
*/
protected function getEntityTypeManager() {
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 re-implementing 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;
}
}

View File

@@ -0,0 +1,38 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$attributes = [];
foreach ($object as $fieldItem) {
$attributes[] = $this->serializer->normalize($fieldItem, $format, $context);
}
return $attributes;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
ListInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Component\Render\MarkupInterface;
/**
* Normalizes MarkupInterface objects into a string.
*/
class MarkupNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
return (string) $object;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
MarkupInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,124 @@
<?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;
/**
* List of formats which supports (de-)normalization.
*
* @var string|string[]
*/
protected $format;
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, ?string $format = NULL, array $context = []): bool {
// 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;
}
if (property_exists($this, 'supportedInterfaceOrClass')) {
@trigger_error('Defining ' . static::class . '::supportedInterfaceOrClass property is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
$supported = (array) $this->supportedInterfaceOrClass;
}
else {
$supported = array_keys($this->getSupportedTypes($format));
}
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. Therefore, this method is implemented at this level to reduce
* code duplication.
*/
public function supportsDenormalization($data, string $type, ?string $format = NULL, array $context = []): bool {
// If the format is not supported return now.
if (!$this->checkFormat($format)) {
return FALSE;
}
if (property_exists($this, 'supportedInterfaceOrClass')) {
@trigger_error('Defining ' . static::class . '::supportedInterfaceOrClass property is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
$supported = (array) $this->supportedInterfaceOrClass;
}
else {
$supported = array_keys($this->getSupportedTypes($format));
}
$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);
}
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
'*' => FALSE,
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\serialization\Normalizer;
/**
* Null normalizer.
*/
class NullNormalizer extends NormalizerBase {
/**
* The interface or class that this Normalizer supports.
*
* @var string[]
*/
protected array $supportedTypes = ['*' => FALSE];
/**
* 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->supportedTypes = [$supported_interface_of_class => TRUE];
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return $this->supportedTypes;
}
}

View File

@@ -0,0 +1,57 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
// Add cacheability if applicable.
$this->addCacheableDependency($context, $object);
$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();
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
PrimitiveInterface::class => TRUE,
];
}
}

View File

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

View File

@@ -0,0 +1,49 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
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)];
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
TimestampItem::class => TRUE,
];
}
}

View File

@@ -0,0 +1,51 @@
<?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 function getNormalizationTimezone() {
return new \DateTimeZone('UTC');
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
$denormalized = parent::denormalize($data, $class, $format, $context);
return $denormalized->getTimestamp();
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
Timestamp::class => TRUE,
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Converts typed data objects to arrays.
*/
class TypedDataNormalizer extends NormalizerBase {
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$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;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use getSupportedTypes() instead. See https://www.drupal.org/node/3359695', E_USER_DEPRECATED);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
TypedDataInterface::class => TRUE,
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\serialization;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services tagged 'entity_resolver' 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 = $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) {
krsort($services);
return array_merge(...$services);
}
}

View File

@@ -0,0 +1,81 @@
<?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 RegisterSerializationClassesCompilerPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*
* phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
* @return void
*/
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);
$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 = $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);
}
/**
* 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) {
krsort($services);
return array_merge(...$services);
}
}

View File

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

View File

@@ -0,0 +1,10 @@
name: 'Entity serialization test support'
type: module
description: 'Provides test support for entity serialization tests.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

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

View File

@@ -0,0 +1,10 @@
name: 'FieldItem normalization test support'
type: module
description: 'Provides test support for fieldItem normalization test support.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
serializer.normalizer.silly_field_item:
class: Drupal\field_normalization_test\Normalization\TextItemSillyNormalizer
tags:
# The priority must be higher than serialization.normalizer.field_item.
- { name: normalizer , priority: 9 }

View File

@@ -0,0 +1,38 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$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;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [TextItemBase::class => TRUE];
}
}

View File

@@ -0,0 +1,38 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
return $object->getValue() ? '👍' : '👎';
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []): mixed {
if (!in_array($data, ['👍', '👎'], TRUE)) {
throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
}
return $data === '👍';
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [BooleanData::class => TRUE];
}
}

View File

@@ -0,0 +1,10 @@
name: 'Test @DataType normalizer'
type: module
description: 'Provides test support for @DataType-level normalization.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

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

View File

@@ -0,0 +1,48 @@
<?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}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$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;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [BooleanItem::class => TRUE];
}
}

View File

@@ -0,0 +1,10 @@
name: 'Test @FieldType normalizer'
type: module
description: 'Provides test support for @FieldType-level normalization.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

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

View File

@@ -0,0 +1,10 @@
name: Serialization test module
type: module
description: "Support module for serialization tests."
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

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

View File

@@ -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 = []): string {
// @see \Drupal\serialization_test\SerializationTestNormalizer::normalize().
return 'Normalized by ' . $data['normalized_by'] . ', Encoded by SerializationTestEncoder';
}
/**
* {@inheritdoc}
*/
public function supportsEncoding(string $format, array $context = []): bool {
return static::$format === $format;
}
}

View File

@@ -0,0 +1,43 @@
<?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';
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL {
$normalized = (array) $object;
// Add identifying value that can be used to verify that the expected
// normalizer was invoked.
$normalized['normalized_by'] = 'SerializationTestNormalizer';
return $normalized;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, ?string $format = NULL, array $context = []): bool {
return static::$format === $format;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
\stdClass::class => TRUE,
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\serialization\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for serialization.
*
* @group serialization
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,390 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\serialization\Kernel;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\entity_test\Entity\EntityTestComputedField;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Datetime\DateTimePlus;
use Drupal\entity_test\Entity\EntitySerializedField;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\filter\Entity\FilterFormat;
use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
/**
* Tests that entities can be serialized to supported core formats.
*
* @group serialization
*/
class EntitySerializationTest extends NormalizerTestBase {
/**
* Modules to install.
*
* @var array
*/
protected 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';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
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']);
}
/**
* Tests the normalize function.
*/
public function testNormalize(): void {
$expected = [
'id' => [
['value' => 1],
],
'uuid' => [
['value' => $this->entity->uuid()],
],
'langcode' => [
['value' => 'en'],
],
'name' => [
['value' => $this->values['name']],
],
'type' => [
['value' => 'entity_test_mulrev'],
],
'created' => [
[
'value' => (new \DateTime())->setTimestamp((int) $this->entity->get('created')->value)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'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->assertEquals([], array_diff_key($normalized, $expected), 'No unexpected data is added to the normalized array.');
}
/**
* Tests user normalization with some default access controls overridden.
*
* @see entity_serialization_test.module
*/
public function testUserNormalize(): void {
// 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->assertSame([NULL], $normalized['pass'], '"pass" value is normalized to [NULL]');
}
/**
* Tests entity serialization for core's formats by a registered Serializer.
*/
public function testSerialize(): void {
// 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->assertSame($expected, $actual, 'Entity serializes to JSON when "json" is requested.');
$actual = $this->serializer->serialize($normalized, 'json');
$this->assertSame($expected, $actual, 'A normalized array serializes to JSON when "json" is requested');
// Test 'ajax'.
$actual = $this->serializer->serialize($this->entity, 'ajax');
$this->assertSame($expected, $actual, 'Entity serializes to JSON when "ajax" is requested.');
$actual = $this->serializer->serialize($normalized, 'ajax');
$this->assertSame($expected, $actual, '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 = [
'value' => DateTimePlus::createFromTimestamp($this->entity->created->value, 'UTC')->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
];
$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->assertSame($expected, $actual);
$actual = $this->serializer->serialize($normalized, 'xml');
$this->assertSame($expected, $actual);
}
/**
* Tests denormalization of an entity.
*/
public function testDenormalize(): void {
$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->assertSame($this->entity->getEntityTypeId(), $denormalized->getEntityTypeId(), 'Expected entity type found.');
$this->assertSame($this->entity->bundle(), $denormalized->bundle(), 'Expected entity bundle found.');
$this->assertSame($this->entity->uuid(), $denormalized->uuid(), 'Expected entity UUID found.');
}
}
/**
* Tests denormalizing serialized columns.
*/
public function testDenormalizeSerializedItem(): void {
$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(): void {
$entity = EntitySerializedField::create(['serialized_text' => serialize(['Hello world!'])]);
$normalized = $this->serializer->normalize($entity);
$this->assertEquals(['Hello world!'], $normalized['serialized_text'][0]['value']);
$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(): void {
$entity = EntitySerializedField::create(['serialized_long' => serialize(['Hello world!'])]);
$normalized = $this->serializer->normalize($entity);
$this->assertEquals(['Hello world!'], $normalized['serialized_long'][0]['value']);
$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(): void {
$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(): void {
$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(): void {
$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);
}
/**
* Tests normalizing cacheable computed field.
*/
public function testCacheableComputedField(): void {
$context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY] = new CacheableMetadata();
$entity = EntityTestComputedField::create();
$normalized = $this->serializer->normalize($entity, NULL, $context);
$this->assertEquals('computed test cacheable string field', $normalized['computed_test_cacheable_string_field'][0]['value']);
$this->assertInstanceOf(CacheableDependencyInterface::class, $context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]);
// See \Drupal\entity_test\Plugin\Field\ComputedTestCacheableStringItemList::computeValue().
$this->assertEquals($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->getCacheContexts(), ['url.query_args:computed_test_cacheable_string_field']);
$this->assertEquals($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->getCacheTags(), ['field:computed_test_cacheable_string_field']);
$this->assertEquals($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->getCacheMaxAge(), 800);
}
}

View File

@@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
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}
*/
protected 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(): void {
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(): void {
$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(): void {
$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): void {
// Asserts the entity contains the value we set.
$this->assertFalse($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->assertFalse($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->assertTrue($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 static 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 XML normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'xml',
],
];
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
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}
*/
protected 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(): void {
parent::setUp();
$this->serializer = \Drupal::service('serializer');
$this->typedDataManager = \Drupal::typedDataManager();
}
/**
* Tests whether map data can be normalized.
*/
public function testMapNormalize(): void {
$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);
}
/**
* Tests whether map data with properties can be normalized.
*/
public function testMapWithPropertiesNormalize(): void {
$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;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
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
*/
protected static $modules = [
'serialization',
'system',
'field',
'entity_test',
'text',
'filter',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test_mulrev');
$this->installEntitySchema('user');
$this->installConfig(['field']);
\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();
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
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
*/
protected static $modules = ['serialization', 'serialization_test'];
/**
* The serializer service to test.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->serializer = $this->container->get('serializer');
}
/**
* Confirms that modules can register normalizers and encoders.
*/
public function testSerializerComponentRegistration(): void {
$object = new \stdClass();
$format = 'serialization_test';
$expected = 'Normalized by SerializationTestNormalizer, Encoded by SerializationTestEncoder';
// Ensure the serialization invokes the expected normalizer and encoder.
$this->assertSame($expected, $this->serializer->serialize($object, $format));
// 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) {
// Expected exception; just continue testing.
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
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(): void {
$container = new ContainerBuilder();
$serializer_definition = new Definition(Serializer::class, [[], []]);
$serializer_definition->setPublic(TRUE);
$container->setDefinition('serializer', $serializer_definition);
$encoder_1_definition = new Definition('TestClass');
$encoder_1_definition->addTag('encoder', ['format' => 'xml']);
$encoder_1_definition->addTag('_provider', ['provider' => 'test_provider_a']);
$encoder_1_definition->setPublic(TRUE);
$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']);
$encoder_2_definition->setPublic(TRUE);
$container->setDefinition('encoder_2', $encoder_2_definition);
$normalizer_1_definition = new Definition('TestClass');
$normalizer_1_definition->addTag('normalizer');
$normalizer_1_definition->setPublic(TRUE);
$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'], $container->getParameter('serializer.formats'));
$this->assertEquals(['xml' => 'test_provider_a', 'json' => 'test_provider_a'], $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($normalizer_1_definition->isPublic());
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
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(): void {
$encoder = new JsonEncoder();
$this->assertTrue($encoder->supportsEncoding('json'));
$this->assertTrue($encoder->supportsEncoding('ajax'));
$this->assertFalse($encoder->supportsEncoding('xml'));
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
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'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->baseEncoder = $this->createMock(BaseXmlEncoder::class);
$this->encoder = new XmlEncoder();
$this->encoder->setBaseEncoder($this->baseEncoder);
}
/**
* Tests the supportsEncoding() method.
*/
public function testSupportsEncoding(): void {
$this->assertTrue($this->encoder->supportsEncoding('xml'));
$this->assertFalse($this->encoder->supportsEncoding('json'));
}
/**
* Tests the supportsDecoding() method.
*/
public function testSupportsDecoding(): void {
$this->assertTrue($this->encoder->supportsDecoding('xml'));
$this->assertFalse($this->encoder->supportsDecoding('json'));
}
/**
* Tests the encode() method.
*/
public function testEncode(): void {
$this->baseEncoder->expects($this->once())
->method('encode')
->with($this->testArray, 'test', [])
->willReturn('test');
$this->assertEquals('test', $this->encoder->encode($this->testArray, 'test'));
}
/**
* Tests the decode() method.
*/
public function testDecode(): void {
$this->baseEncoder->expects($this->once())
->method('decode')
->with('test', 'test', [])
->willReturn($this->testArray);
$this->assertEquals($this->testArray, $this->encoder->decode('test', 'test'));
}
/**
* @covers ::getBaseEncoder
*/
public function testDefaultEncoderHasSerializer(): void {
// The serializer should be set on the Drupal encoder, which should then
// set it on our default encoder.
$encoder = new XmlEncoder();
$serializer = new Serializer([new GetSetMethodNormalizer()]);
$encoder->setSerializer($serializer);
$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';
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
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 object
*/
protected $testData;
/**
* A test entity type.
*
* @var string
*/
protected $testEntityType = 'test_type';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->testNormalizer = $this->createMock('Symfony\Component\Serializer\Normalizer\NormalizerInterface');
$this->testData = new \stdClass();
}
/**
* Tests the resolve method with no matching resolvers.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithNoneResolved(): void {
$resolvers = [
$this->createEntityResolverMock(),
$this->createEntityResolverMock(),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertNull($resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Tests the resolve method with no matching resolvers, using addResolver.
*
* @covers ::addResolver
* @covers ::resolve
*/
public function testResolverWithNoneResolvedUsingAddResolver(): void {
$resolver = new ChainEntityResolver();
$resolver->addResolver($this->createEntityResolverMock());
$resolver->addResolver($this->createEntityResolverMock());
$this->assertNull($resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Tests the resolve method with a matching resolver first.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithFirstResolved(): void {
$resolvers = [
$this->createEntityResolverMock(10),
$this->createEntityResolverMock(NULL, FALSE),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertSame(10, $resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Tests the resolve method with a matching resolver last.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithLastResolved(): void {
$resolvers = [
$this->createEntityResolverMock(),
$this->createEntityResolverMock(10),
];
$resolver = new ChainEntityResolver($resolvers);
$this->assertSame(10, $resolver->resolve($this->testNormalizer, $this->testData, $this->testEntityType));
}
/**
* Tests the resolve method where one resolver returns 0.
*
* @covers ::__construct
* @covers ::resolve
*/
public function testResolverWithResolvedToZero(): void {
$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)
->willReturn($return);
}
else {
$mock->expects($this->never())
->method('resolve');
}
return $mock;
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
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(): void {
parent::setUp();
$this->entityRepository = $this->createMock(EntityRepositoryInterface::class);
$this->resolver = new UuidResolver($this->entityRepository);
}
/**
* Tests resolve() with a class using the incorrect interface.
*/
public function testResolveNotInInterface(): void {
$this->entityRepository->expects($this->never())
->method('loadEntityByUuid');
$normalizer = $this->createMock('Symfony\Component\Serializer\Normalizer\NormalizerInterface');
$this->assertNull($this->resolver->resolve($normalizer, [], 'test_type'));
}
/**
* Tests resolve() with a class using the correct interface but no UUID.
*/
public function testResolveNoUuid(): void {
$this->entityRepository->expects($this->never())
->method('loadEntityByUuid');
$normalizer = $this->createMock('Drupal\serialization\EntityResolver\UuidReferenceInterface');
$normalizer->expects($this->once())
->method('getUuid')
->with([])
->willReturn(NULL);
$this->assertNull($this->resolver->resolve($normalizer, [], 'test_type'));
}
/**
* Tests resolve() with correct interface but no matching entity for the UUID.
*/
public function testResolveNoEntity(): void {
$uuid = '392eab92-35c2-4625-872d-a9dab4da008e';
$this->entityRepository->expects($this->once())
->method('loadEntityByUuid')
->with('test_type')
->willReturn(NULL);
$normalizer = $this->createMock('Drupal\serialization\EntityResolver\UuidReferenceInterface');
$normalizer->expects($this->once())
->method('getUuid')
->with([])
->willReturn($uuid);
$this->assertNull($this->resolver->resolve($normalizer, [], 'test_type'));
}
/**
* Tests resolve() when a UUID corresponds to an entity.
*/
public function testResolveWithEntity(): void {
$uuid = '392eab92-35c2-4625-872d-a9dab4da008e';
$entity = $this->createMock('Drupal\Core\Entity\EntityInterface');
$entity->expects($this->once())
->method('id')
->willReturn(1);
$this->entityRepository->expects($this->once())
->method('loadEntityByUuid')
->with('test_type', $uuid)
->willReturn($entity);
$normalizer = $this->createMock('Drupal\serialization\EntityResolver\UuidReferenceInterface');
$normalizer->expects($this->once())
->method('getUuid')
->with([])
->willReturn($uuid);
$this->assertSame(1, $this->resolver->resolve($normalizer, [], 'test_type'));
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
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\ExceptionEvent;
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(): void {
$kernel = $this->prophesize(HttpKernelInterface::class);
$request = Request::create('/test');
$request->setRequestFormat('json');
$e = new MethodNotAllowedHttpException(['POST', 'PUT'], 'test message');
$event = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::MAIN_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'));
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
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(): void {
parent::setUp();
$this->normalizer = new ComplexDataNormalizer();
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization(): void {
$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()));
}
/**
* Tests normalizing complex data.
*
* @covers ::normalize
*/
public function testNormalizeComplexData(): void {
$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);
}
/**
* Tests normalize() where $object does not implement ComplexDataInterface.
*
* Normalizers extending ComplexDataNormalizer may have a different supported
* class.
*
* @covers ::normalize
*/
public function testNormalizeNonComplex(): void {
$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 {
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
\stdClass::class => TRUE,
];
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
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(): void {
$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')
->willReturn($test_export_properties);
$this->assertSame(['test' => 'test'], $normalizer->normalize($config_entity));
}
/**
* @covers ::denormalize
*/
public function testDenormalize(): void {
$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 type 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'));
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\serialization\Unit\Normalizer;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\serialization\Normalizer\ContentEntityNormalizer;
use Drupal\Tests\Core\Entity\ContentEntityBaseMockableClass;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Symfony\Component\Serializer\Serializer;
/**
* @coversDefaultClass \Drupal\serialization\Normalizer\ContentEntityNormalizer
* @group serialization
*/
class ContentEntityNormalizerTest extends UnitTestCase {
/**
* The mock serializer.
*
* @var \Symfony\Component\Serializer\Serializer|\Prophecy\Prophecy\ObjectProphecy
*/
protected $serializer;
/**
* The normalizer under test.
*
* @var \Drupal\serialization\Normalizer\ContentEntityNormalizer
*/
protected $contentEntityNormalizer;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::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->prophesize(Serializer::class);
$this->contentEntityNormalizer->setSerializer($this->serializer->reveal());
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization(): void {
$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(): void {
$this->serializer->normalize(Argument::type(FieldItemListInterface::class),
'test_format', ['account' => NULL])->willReturn('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(): void {
$mock_account = $this->createMock('Drupal\Core\Session\AccountInterface');
$context = [
'account' => $mock_account,
];
$this->serializer->normalize(Argument::type(FieldItemListInterface::class),
'test_format', $context)->willReturn('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
* The properties the will be returned.
*
* @return \PHPUnit\Framework\MockObject\MockObject
*/
public function createMockForContentEntity($definitions) {
$content_entity_mock = $this->getMockBuilder(ContentEntityBaseMockableClass::class)
->disableOriginalConstructor()
->onlyMethods(['getTypedData'])
->getMock();
$typed_data = $this->prophesize(ComplexDataInterface::class);
$typed_data->getProperties(TRUE)
->willReturn($definitions)
->shouldBeCalled();
$content_entity_mock->expects($this->any())
->method('getTypedData')
->willReturn($typed_data->reveal());
return $content_entity_mock;
}
/**
* Creates a mock field list item.
*
* @param bool $access
* The value that access() will return.
* @param bool $internal
* The value that isInternal() will return.
* @param \Drupal\Core\Session\AccountInterface $user_context
* The user context used for the access check.
*
* @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')
->willReturn($data_definition->reveal());
$data_definition->isInternal()
->willReturn($internal)
->shouldBeCalled();
if (!$internal) {
$mock->expects($this->once())
->method('access')
->with('view', $user_context)
->willReturn($access);
}
return $mock;
}
}

View File

@@ -0,0 +1,275 @@
<?php
declare(strict_types=1);
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(): void {
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(): void {
$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(): void {
$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): void {
$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): void {
$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 static 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): void {
$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 static 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 bad data for the date-only case.
*
* @covers ::denormalize
*/
public function testDenormalizeDateOnlyException(): void {
$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(): void {
$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).');
$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(): void {
$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, []);
}
}
/**
* Provides a test class for testing DrupalDateTime.
*
* Note: Prophecy does not support magic methods. By subclassing and specifying
* an explicit method, Prophecy works.
* @see https://github.com/phpspec/prophecy/issues/338
* @see https://github.com/phpspec/prophecy/issues/34
* @see https://github.com/phpspec/prophecy/issues/80
*/
class DateTimeIso8601NormalizerTestDrupalDateTime extends DrupalDateTime {
public function setTimezone(\DateTimeZone $timezone) {
parent::setTimezone($timezone);
}
}

View File

@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
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(): void {
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(): void {
$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(): void {
$this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), DateTimeInterface::class));
}
/**
* @covers ::normalize
*/
public function testNormalize(): void {
$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(): void {
$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): void {
$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 static 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): void {
$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 static 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(): void {
$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, []);
}
}
/**
* Provides a test class for testing DrupalDateTime.
*
* Note: Prophecy does not support magic methods. By subclassing and specifying
* an explicit method, Prophecy works.
* @see https://github.com/phpspec/prophecy/issues/338
* @see https://github.com/phpspec/prophecy/issues/34
* @see https://github.com/phpspec/prophecy/issues/80
*/
class DateTimeNormalizerTestDrupalDateTime extends DrupalDateTime {
public function setTimezone(\DateTimeZone $timezone) {
parent::setTimezone($timezone);
}
}

View File

@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
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\Core\Entity\ContentEntityBaseMockableClass;
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(): void {
parent::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(): void {
$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(ContentEntityBaseMockableClass::class)
->disableOriginalConstructor()
->onlyMethods(['getFields'])
->getMock();
$content_entity->expects($this->once())
->method('getFields')
->willReturn($definitions);
$serializer = $this->prophesize('Symfony\Component\Serializer\Serializer');
$serializer->normalize($list_item_1, 'test_format', [])->shouldBeCalled();
$serializer->normalize($list_item_2, 'test_format', [])->shouldBeCalled();
$this->entityNormalizer->setSerializer($serializer->reveal());
$this->entityNormalizer->normalize($content_entity, 'test_format');
}
/**
* Tests the denormalize() method with no entity type provided in context.
*
* @covers ::denormalize
*/
public function testDenormalizeWithNoEntityType(): void {
$this->expectException(UnexpectedValueException::class);
$this->entityNormalizer->denormalize([], ContentEntityBaseMockableClass::class);
}
/**
* Tests the denormalize method with a bundle property.
*
* @covers ::denormalize
*/
public function testDenormalizeWithValidBundle(): void {
$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')
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('getKey')
->with('bundle')
->willReturn('test_type');
$entity_type->expects($this->once())
->method('entityClassImplements')
->with(FieldableEntityInterface::class)
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('getBundleEntityType')
->willReturn('test_bundle');
$entity_type_storage_definition = $this->createMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$entity_type_storage_definition->expects($this->once())
->method('getMainPropertyName')
->willReturn('name');
$entity_type_definition = $this->createMock('Drupal\Core\Field\FieldDefinitionInterface');
$entity_type_definition->expects($this->once())
->method('getFieldStorageDefinition')
->willReturn($entity_type_storage_definition);
$base_definitions = [
'test_type' => $entity_type_definition,
];
$this->entityTypeManager->expects($this->once())
->method('getDefinition')
->with('test')
->willReturn($entity_type);
$this->entityFieldManager->expects($this->once())
->method('getBaseFieldDefinitions')
->with('test')
->willReturn($base_definitions);
$entity_query_mock = $this->createMock('Drupal\Core\Entity\Query\QueryInterface');
$entity_query_mock->expects($this->once())
->method('accessCheck')
->with(TRUE)
->willReturn($entity_query_mock);
$entity_query_mock->expects($this->once())
->method('execute')
->willReturn(['test_bundle' => 'test_bundle']);
$entity_type_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$entity_type_storage->expects($this->once())
->method('getQuery')
->willReturn($entity_query_mock);
$key_1 = $this->createMock(FieldItemListInterface::class);
$key_2 = $this->createMock(FieldItemListInterface::class);
$entity = $this->createMock(ContentEntityBaseMockableClass::class);
$entity->expects($this->exactly(2))
->method('get')
->willReturnMap([
['key_1', $key_1],
['key_2', $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)
->willReturn($entity);
$this->entityTypeManager->expects($this->exactly(2))
->method('getStorage')
->willReturnMap([
['test_bundle', $entity_type_storage],
['test', $storage],
]);
// Setup expectations for the serializer. This will be called for each field
// item.
$serializer = $this->prophesize('Symfony\Component\Serializer\Serializer');
$serializer->denormalize('value_1', get_class($key_1), NULL, ['target_instance' => $key_1, 'entity_type' => 'test'])
->willReturn(NULL)
->shouldBeCalled();
$serializer->denormalize('value_2', get_class($key_2), NULL, ['target_instance' => $key_2, 'entity_type' => 'test'])
->willReturn(NULL)
->shouldBeCalled();
$this->entityNormalizer->setSerializer($serializer->reveal());
$this->assertNotNull($this->entityNormalizer->denormalize($test_data, ContentEntityBaseMockableClass::class, NULL, ['entity_type' => 'test']));
}
/**
* Tests the denormalize method with a bundle property.
*
* @covers ::denormalize
*/
public function testDenormalizeWithInvalidBundle(): void {
$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')
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('getKey')
->with('bundle')
->willReturn('test_type');
$entity_type->expects($this->once())
->method('entityClassImplements')
->with(FieldableEntityInterface::class)
->willReturn(TRUE);
$entity_type->expects($this->once())
->method('getBundleEntityType')
->willReturn('test_bundle');
$entity_type_storage_definition = $this->createMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$entity_type_storage_definition->expects($this->once())
->method('getMainPropertyName')
->willReturn('name');
$entity_type_definition = $this->createMock('Drupal\Core\Field\FieldDefinitionInterface');
$entity_type_definition->expects($this->once())
->method('getFieldStorageDefinition')
->willReturn($entity_type_storage_definition);
$base_definitions = [
'test_type' => $entity_type_definition,
];
$this->entityTypeManager->expects($this->once())
->method('getDefinition')
->with('test')
->willReturn($entity_type);
$this->entityFieldManager->expects($this->once())
->method('getBaseFieldDefinitions')
->with('test')
->willReturn($base_definitions);
$entity_query_mock = $this->createMock('Drupal\Core\Entity\Query\QueryInterface');
$entity_query_mock->expects($this->once())
->method('accessCheck')
->with(TRUE)
->willReturn($entity_query_mock);
$entity_query_mock->expects($this->once())
->method('execute')
->willReturn(['test_bundle_other' => 'test_bundle_other']);
$entity_type_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$entity_type_storage->expects($this->once())
->method('getQuery')
->willReturn($entity_query_mock);
$this->entityTypeManager->expects($this->once())
->method('getStorage')
->with('test_bundle')
->willReturn($entity_type_storage);
$this->expectException(UnexpectedValueException::class);
$this->entityNormalizer->denormalize($test_data, ContentEntityBaseMockableClass::class, NULL, ['entity_type' => 'test']);
}
/**
* Tests the denormalize method with no bundle defined.
*
* @covers ::denormalize
*/
public function testDenormalizeWithNoBundle(): void {
$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')
->willReturn(FALSE);
$entity_type->expects($this->never())
->method('getKey');
$this->entityTypeManager->expects($this->once())
->method('getDefinition')
->with('test')
->willReturn($entity_type);
$key_1 = $this->createMock(FieldItemListInterface::class);
$key_2 = $this->createMock(FieldItemListInterface::class);
$entity = $this->createMock(ContentEntityBaseMockableClass::class);
$entity->expects($this->exactly(2))
->method('get')
->willReturnMap([
['key_1', $key_1],
['key_2', $key_2],
]);
$storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->once())
->method('create')
->with([])
->willReturn($entity);
$this->entityTypeManager->expects($this->once())
->method('getStorage')
->with('test')
->willReturn($storage);
$this->entityFieldManager->expects($this->never())
->method('getBaseFieldDefinitions');
// Setup expectations for the serializer. This will be called for each field
// item.
$serializer = $this->prophesize('Symfony\Component\Serializer\Serializer');
$serializer->denormalize('value_1', get_class($key_1), NULL, ['target_instance' => $key_1, 'entity_type' => 'test'])
->willReturn(NULL)
->shouldBeCalled();
$serializer->denormalize('value_2', get_class($key_2), NULL, ['target_instance' => $key_2, 'entity_type' => 'test'])
->willReturn(NULL)
->shouldBeCalled();
$this->entityNormalizer->setSerializer($serializer->reveal());
$this->assertNotNull($this->entityNormalizer->denormalize($test_data, ContentEntityBaseMockableClass::class, NULL, ['entity_type' => 'test']));
}
/**
* Tests the denormalize method with no bundle defined.
*
* @covers ::denormalize
*/
public function testDenormalizeWithNoFieldableEntityType(): void {
$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')
->willReturn($entity_type);
$storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->once())
->method('create')
->with($test_data)
->willReturn($this->createMock(ContentEntityBaseMockableClass::class));
$this->entityTypeManager->expects($this->once())
->method('getStorage')
->with('test')
->willReturn($storage);
$this->entityFieldManager->expects($this->never())
->method('getBaseFieldDefinitions');
$this->assertNotNull($this->entityNormalizer->denormalize($test_data, ContentEntityBaseMockableClass::class, NULL, ['entity_type' => 'test']));
}
}

View File

@@ -0,0 +1,512 @@
<?php
declare(strict_types=1);
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(): void {
parent::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(): void {
$this->assertTrue($this->normalizer->supportsNormalization($this->fieldItem->reveal()));
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* @covers ::supportsDenormalization
*/
public function testSupportsDenormalization(): void {
$this->assertTrue($this->normalizer->supportsDenormalization([], EntityReferenceItem::class));
$this->assertFalse($this->normalizer->supportsDenormalization([], FieldItemInterface::class));
}
/**
* @covers ::normalize
*/
public function testNormalize(): void {
$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(): void {
$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(): void {
// 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(): void {
$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(): void {
$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(): void {
$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(): void {
$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(): void {
$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 testDenormalizeWithEmptyUuid(): void {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('If provided "target_uuid" cannot be empty for field "field_reference".');
$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(): void {
$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.
*
* @internal
*/
protected function assertDenormalize(array $data): void {
$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(): void {
$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);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
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();
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
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;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::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')
->willReturn($this->typedData);
// Set up a mock container as ItemList() will call for the 'typed_data_manager'
// service.
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')
->onlyMethods(['get'])
->getMock();
$container->expects($this->any())
->method('get')
->with($this->equalTo('typed_data_manager'))
->willReturn($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(): void {
$this->assertTrue($this->normalizer->supportsNormalization($this->list));
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
/**
* Tests the normalize() method.
*/
public function testNormalize(): void {
$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);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
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_types
* (optional) The supported interface or class to set on the normalizer.
*/
public function testSupportsNormalization($expected_return, $data, $supported_types = NULL): void {
$normalizer_base = $this->getMockForAbstractClass('Drupal\Tests\serialization\Unit\Normalizer\TestNormalizerBase');
if (isset($supported_types)) {
$normalizer_base->setSupportedTypes($supported_types);
}
$this->assertSame($expected_return, $normalizer_base->supportsNormalization($data));
}
/**
* Data provider for testSupportsNormalization.
*
* @return array
* An array of provider data for testSupportsNormalization.
*/
public static 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 {
/**
* The interface or class that this Normalizer supports.
*
* @var string[]
*/
protected array $supportedTypes = ['*' => FALSE];
/**
* Sets the supported types.
*
* @param string $supported_types
* The class name to set.
*/
public function setSupportedTypes($supported_types): void {
$this->supportedTypes = [$supported_types => FALSE];
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return $this->supportedTypes;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
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(): void {
parent::setUp();
$this->normalizer = new NullNormalizer($this->interface);
}
/**
* @covers ::__construct
* @covers ::supportsNormalization
*/
public function testSupportsNormalization(): void {
$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(): void {
$mock = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
$this->assertNull($this->normalizer->normalize($mock));
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
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(): void {
parent::setUp();
$this->normalizer = new PrimitiveDataNormalizer();
}
/**
* @covers ::supportsNormalization
* @dataProvider dataProviderPrimitiveData
*/
public function testSupportsNormalization($primitive_data, $expected): void {
$this->assertTrue($this->normalizer->supportsNormalization($primitive_data));
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalizationFail(): void {
// 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): void {
$this->assertSame($expected, $this->normalizer->normalize($primitive_data));
}
/**
* Data provider for testNormalize().
*/
public static 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;
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
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(): void {
parent::setUp();
$this->normalizer = new TimestampItemNormalizer();
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization(): void {
$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(): void {
$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(): void {
// 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(): void {
$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;
}
}

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
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(): void {
parent::setUp();
$this->normalizer = new TimestampNormalizer($this->prophesize(ConfigFactoryInterface::class)->reveal());
$this->data = $this->prophesize(Timestamp::class);
}
/**
* @covers ::supportsNormalization
*/
public function testSupportsNormalization(): void {
$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(): void {
$this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), Timestamp::class));
}
/**
* @covers ::normalize
*/
public function testNormalize(): void {
$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): void {
$denormalized = $this->normalizer->denormalize($normalized, Timestamp::class, NULL, []);
$this->assertSame($expected, $denormalized);
}
/**
* Data provider for testDenormalizeValidFormats.
*
* @return array
*/
public static 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(): void {
$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, []);
}
}
/**
* Provides a test class for testing DrupalDateTime.
*
* 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);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
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;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->normalizer = new TypedDataNormalizer();
$this->typedData = $this->createMock('Drupal\Core\TypedData\TypedDataInterface');
}
/**
* Tests the supportsNormalization() method.
*/
public function testSupportsNormalization(): void {
$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(): void {
$this->typedData->expects($this->once())
->method('getValue')
->willReturn('test');
$this->assertEquals('test', $this->normalizer->normalize($this->typedData));
}
}