first commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Encoder;
|
||||
|
||||
use Symfony\Component\Serializer\Encoder\DecoderInterface;
|
||||
use Symfony\Component\Serializer\Encoder\EncoderInterface;
|
||||
use Symfony\Component\Serializer\Encoder\JsonDecode;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncode;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder as BaseJsonEncoder;
|
||||
|
||||
/**
|
||||
* Adds 'ajax to the supported content types of the JSON encoder'
|
||||
*
|
||||
* @internal
|
||||
* This encoder should not be used directly. Rather, use the `serializer`
|
||||
* service.
|
||||
*/
|
||||
class JsonEncoder extends BaseJsonEncoder implements EncoderInterface, DecoderInterface {
|
||||
|
||||
/**
|
||||
* The formats that this Encoder supports.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $format = ['json', 'ajax'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(JsonEncode $encodingImpl = NULL, JsonDecode $decodingImpl = NULL) {
|
||||
$this->encodingImpl = $encodingImpl ?: $this->getJsonEncode();
|
||||
$this->decodingImpl = $decodingImpl ?: $this->getJsonDecode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a JsonEncode instance.
|
||||
*
|
||||
* @internal this exists to bridge Symfony 3 to Symfony 4, and can be removed
|
||||
* once Drupal requires Symfony 4.2 or higher.
|
||||
*/
|
||||
private function getJsonEncode() {
|
||||
// Encode <, >, ', &, and " for RFC4627-compliant JSON, which may also be
|
||||
// embedded into HTML.
|
||||
// @see \Symfony\Component\HttpFoundation\JsonResponse
|
||||
$json_encoding_options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT;
|
||||
$reflection = new \ReflectionClass(JsonEncode::class);
|
||||
if (array_key_exists('OPTIONS', $reflection->getConstants())) {
|
||||
return new JsonEncode([JsonEncode::OPTIONS => $json_encoding_options]);
|
||||
}
|
||||
return new JsonEncode($json_encoding_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a JsonDecode instance.
|
||||
*
|
||||
* @internal this exists to bridge Symfony 3 to Symfony 4, and can be removed
|
||||
* once Drupal requires Symfony 4.2 or higher.
|
||||
*/
|
||||
private function getJsonDecode() {
|
||||
$reflection = new \ReflectionClass(JsonDecode::class);
|
||||
if (array_key_exists('ASSOCIATIVE', $reflection->getConstants())) {
|
||||
return new JsonDecode([JsonDecode::ASSOCIATIVE => TRUE]);
|
||||
}
|
||||
return new JsonDecode(TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsEncoding($format) {
|
||||
return in_array($format, static::$format);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsDecoding($format) {
|
||||
return in_array($format, static::$format);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Encoder;
|
||||
|
||||
use Symfony\Component\Serializer\Encoder\EncoderInterface;
|
||||
use Symfony\Component\Serializer\Encoder\DecoderInterface;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder as BaseXmlEncoder;
|
||||
use Symfony\Component\Serializer\SerializerAwareInterface;
|
||||
use Symfony\Component\Serializer\SerializerAwareTrait;
|
||||
|
||||
/**
|
||||
* Adds XML support for serializer.
|
||||
*
|
||||
* This acts as a wrapper class for Symfony's XmlEncoder so that it is not
|
||||
* implementing NormalizationAwareInterface, and can be normalized externally.
|
||||
*
|
||||
* @internal
|
||||
* This encoder should not be used directly. Rather, use the `serializer`
|
||||
* service.
|
||||
*/
|
||||
class XmlEncoder implements SerializerAwareInterface, EncoderInterface, DecoderInterface {
|
||||
|
||||
use SerializerAwareTrait;
|
||||
|
||||
/**
|
||||
* The formats that this Encoder supports.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $format = ['xml'];
|
||||
|
||||
/**
|
||||
* An instance of the Symfony XmlEncoder to perform the actual encoding.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\Encoder\XmlEncoder
|
||||
*/
|
||||
protected $baseEncoder;
|
||||
|
||||
/**
|
||||
* Gets the base encoder instance.
|
||||
*
|
||||
* @return \Symfony\Component\Serializer\Encoder\XmlEncoder
|
||||
* The base encoder.
|
||||
*/
|
||||
public function getBaseEncoder() {
|
||||
if (!isset($this->baseEncoder)) {
|
||||
$this->baseEncoder = new BaseXmlEncoder();
|
||||
$this->baseEncoder->setSerializer($this->serializer);
|
||||
}
|
||||
|
||||
return $this->baseEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base encoder instance.
|
||||
*
|
||||
* @param \Symfony\Component\Serializer\Encoder\XmlEncoder $encoder
|
||||
*/
|
||||
public function setBaseEncoder($encoder) {
|
||||
$this->baseEncoder = $encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encode($data, $format, array $context = []) {
|
||||
return $this->getBaseEncoder()->encode($data, $format, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsEncoding($format) {
|
||||
return in_array($format, static::$format);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decode($data, $format, array $context = []) {
|
||||
return $this->getBaseEncoder()->decode($data, $format, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsDecoding($format) {
|
||||
return in_array($format, static::$format);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EntityResolver;
|
||||
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Resolver delegating the entity resolution to a chain of resolvers.
|
||||
*/
|
||||
class ChainEntityResolver implements ChainEntityResolverInterface {
|
||||
|
||||
/**
|
||||
* The concrete resolvers.
|
||||
*
|
||||
* @var \Drupal\serialization\EntityResolver\EntityResolverInterface[]
|
||||
*/
|
||||
protected $resolvers = [];
|
||||
|
||||
/**
|
||||
* Constructs a ChainEntityResolver object.
|
||||
*
|
||||
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface[] $resolvers
|
||||
* The array of concrete resolvers.
|
||||
*/
|
||||
public function __construct(array $resolvers = []) {
|
||||
$this->resolvers = $resolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addResolver(EntityResolverInterface $resolver) {
|
||||
$this->resolvers[] = $resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
|
||||
foreach ($this->resolvers as $resolver) {
|
||||
$resolved = $resolver->resolve($normalizer, $data, $entity_type);
|
||||
if (isset($resolved)) {
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EntityResolver;
|
||||
|
||||
/**
|
||||
* An interface for delegating a entity resolution to a chain of resolvers.
|
||||
*/
|
||||
interface ChainEntityResolverInterface extends EntityResolverInterface {
|
||||
|
||||
/**
|
||||
* Adds an entity resolver.
|
||||
*
|
||||
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface $resolver
|
||||
* The entity resolver to add.
|
||||
*/
|
||||
public function addResolver(EntityResolverInterface $resolver);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EntityResolver;
|
||||
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
interface EntityResolverInterface {
|
||||
|
||||
/**
|
||||
* Returns the local ID of an entity referenced by serialized data.
|
||||
*
|
||||
* Drupal entities are loaded by and internally referenced by a local ID.
|
||||
* Because different websites can use the same local ID to refer to different
|
||||
* entities (e.g., node "1" can be a different node on foo.com and bar.com, or
|
||||
* on example.com and staging.example.com), it is generally unsuitable for use
|
||||
* in hypermedia data exchanges. Instead, UUIDs, URIs, or other globally
|
||||
* unique IDs are preferred.
|
||||
*
|
||||
* This function takes a $data array representing partially deserialized data
|
||||
* for an entity reference, and resolves it to a local entity ID. For example,
|
||||
* depending on the data specification being used, $data might contain a
|
||||
* 'uuid' key, a 'uri' key, a 'href' key, or some other data identifying the
|
||||
* entity, and it is up to the implementor of this interface to resolve that
|
||||
* appropriately for the specification being used.
|
||||
*
|
||||
* @param \Symfony\Component\Serializer\Normalizer\NormalizerInterface $normalizer
|
||||
* The Normalizer which is handling the data.
|
||||
* @param array $data
|
||||
* The data passed into the calling Normalizer.
|
||||
* @param string $entity_type
|
||||
* The type of entity being resolved; e.g., 'node' or 'user'.
|
||||
*
|
||||
* @return string|null
|
||||
* Returns the local entity ID, if found. Otherwise, returns NULL.
|
||||
*/
|
||||
public function resolve(NormalizerInterface $normalizer, $data, $entity_type);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EntityResolver;
|
||||
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Resolves entities from data that contains an entity target ID.
|
||||
*/
|
||||
class TargetIdResolver implements EntityResolverInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
|
||||
if (isset($data['target_id'])) {
|
||||
return $data['target_id'];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EntityResolver;
|
||||
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Interface for extracting UUID from entity reference data when denormalizing.
|
||||
*/
|
||||
interface UuidReferenceInterface extends NormalizerInterface {
|
||||
|
||||
/**
|
||||
* Get the uuid from the data array.
|
||||
*
|
||||
* @param array $data
|
||||
* The data, as was passed into the Normalizer.
|
||||
*
|
||||
* @return string
|
||||
* A UUID.
|
||||
*/
|
||||
public function getUuid($data);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EntityResolver;
|
||||
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Resolves entities from data that contains an entity UUID.
|
||||
*/
|
||||
class UuidResolver implements EntityResolverInterface {
|
||||
|
||||
/**
|
||||
* The entity repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityRepositoryInterface
|
||||
*/
|
||||
protected $entityRepository;
|
||||
|
||||
/**
|
||||
* Constructs a UuidResolver object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository.
|
||||
*/
|
||||
public function __construct(EntityRepositoryInterface $entity_repository) {
|
||||
$this->entityRepository = $entity_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
|
||||
// The normalizer is what knows the specification of the data being
|
||||
// deserialized. If it can return a UUID from that data, and if there's an
|
||||
// entity with that UUID, then return its ID.
|
||||
if (($normalizer instanceof UuidReferenceInterface) && ($uuid = $normalizer->getUuid($data))) {
|
||||
if ($entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid)) {
|
||||
return $entity->id();
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Config\ConfigCrudEvent;
|
||||
use Drupal\Core\Config\ConfigEvents;
|
||||
use Drupal\Core\DrupalKernelInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Config event subscriber to rebuild the container when BC config is saved.
|
||||
*/
|
||||
class BcConfigSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The Drupal Kernel.
|
||||
*
|
||||
* @var \Drupal\Core\DrupalKernelInterface
|
||||
*/
|
||||
protected $kernel;
|
||||
|
||||
/**
|
||||
* BcConfigSubscriber constructor.
|
||||
*
|
||||
* @param \Drupal\Core\DrupalKernelInterface $kernel
|
||||
* The Drupal Kernel.
|
||||
*/
|
||||
public function __construct(DrupalKernelInterface $kernel) {
|
||||
$this->kernel = $kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[ConfigEvents::SAVE][] = 'onConfigSave';
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the service container if serialization BC config gets updated.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigCrudEvent $event
|
||||
*/
|
||||
public function onConfigSave(ConfigCrudEvent $event) {
|
||||
$saved_config = $event->getConfig();
|
||||
|
||||
if ($saved_config->getName() === 'serialization.settings') {
|
||||
if ($event->isChanged('bc_primitives_as_strings') || $event->isChanged('bc_timestamp_normalizer_unix')) {
|
||||
$this->kernel->invalidateContainer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Cache\CacheableResponse;
|
||||
use Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Handles default error responses in serialization formats.
|
||||
*/
|
||||
class DefaultExceptionSubscriber extends HttpExceptionSubscriberBase {
|
||||
|
||||
/**
|
||||
* The serializer.
|
||||
*
|
||||
* @var \Symfony\Component\Serializer\Serializer
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
/**
|
||||
* The available serialization formats.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $serializerFormats = [];
|
||||
|
||||
/**
|
||||
* DefaultExceptionSubscriber constructor.
|
||||
*
|
||||
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
|
||||
* The serializer service.
|
||||
* @param array $serializer_formats
|
||||
* The available serialization formats.
|
||||
*/
|
||||
public function __construct(SerializerInterface $serializer, array $serializer_formats) {
|
||||
$this->serializer = $serializer;
|
||||
$this->serializerFormats = $serializer_formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getHandledFormats() {
|
||||
return $this->serializerFormats;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function getPriority() {
|
||||
// This will fire after the most common HTML handler, since HTML requests
|
||||
// are still more common than HTTP requests. But it has a lower priority
|
||||
// than \Drupal\Core\EventSubscriber\ExceptionJsonSubscriber::on4xx(), so
|
||||
// that this also handles the 'json' format. Then all serialization formats
|
||||
// (::getHandledFormats()) are handled by this exception subscriber, which
|
||||
// results in better consistency.
|
||||
return -70;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all 4xx errors for all serialization failures.
|
||||
*
|
||||
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
|
||||
* The event to process.
|
||||
*/
|
||||
public function on4xx(GetResponseForExceptionEvent $event) {
|
||||
/** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $exception */
|
||||
$exception = $event->getException();
|
||||
$request = $event->getRequest();
|
||||
|
||||
$format = $request->getRequestFormat();
|
||||
$content = ['message' => $exception->getMessage()];
|
||||
$encoded_content = $this->serializer->serialize($content, $format);
|
||||
$headers = $exception->getHeaders();
|
||||
|
||||
// Add the MIME type from the request to send back in the header.
|
||||
$headers['Content-Type'] = $request->getMimeType($format);
|
||||
|
||||
// If the exception is cacheable, generate a cacheable response.
|
||||
if ($exception instanceof CacheableDependencyInterface) {
|
||||
$response = new CacheableResponse($encoded_content, $exception->getStatusCode(), $headers);
|
||||
$response->addCacheableDependency($exception);
|
||||
}
|
||||
else {
|
||||
$response = new Response($encoded_content, $exception->getStatusCode(), $headers);
|
||||
}
|
||||
|
||||
$event->setResponse($response);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Routing\RouteBuildEvent;
|
||||
use Drupal\Core\Routing\RoutingEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Alters user authentication routes to support additional serialization formats.
|
||||
*/
|
||||
class UserRouteAlterSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The available serialization formats.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $serializerFormats = [];
|
||||
|
||||
/**
|
||||
* UserRouteAlterSubscriber constructor.
|
||||
*
|
||||
* @param array $serializer_formats
|
||||
* The available serializer formats.
|
||||
*/
|
||||
public function __construct(array $serializer_formats) {
|
||||
$this->serializerFormats = $serializer_formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[RoutingEvents::ALTER][] = 'onRoutingAlterAddFormats';
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds supported formats to the user authentication HTTP routes.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteBuildEvent $event
|
||||
* The event to process.
|
||||
*/
|
||||
public function onRoutingAlterAddFormats(RouteBuildEvent $event) {
|
||||
$route_names = [
|
||||
'user.login_status.http',
|
||||
'user.login.http',
|
||||
'user.logout.http',
|
||||
'user.pass.http',
|
||||
];
|
||||
$routes = $event->getRouteCollection();
|
||||
foreach ($route_names as $route_name) {
|
||||
if ($route = $routes->get($route_name)) {
|
||||
$formats = explode('|', $route->getRequirement('_format'));
|
||||
$formats = array_unique(array_merge($formats, $this->serializerFormats));
|
||||
$route->setRequirement('_format', implode('|', $formats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Defines the interface for normalizers producing cacheable normalizations.
|
||||
*
|
||||
* @see cache
|
||||
*/
|
||||
interface CacheableNormalizerInterface extends NormalizerInterface {
|
||||
|
||||
/**
|
||||
* Name of key for bubbling cacheability metadata via serialization context.
|
||||
*
|
||||
* @see \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
|
||||
* @see \Symfony\Component\Serializer\SerializerInterface::serialize()
|
||||
* @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::renderResponseBody()
|
||||
*/
|
||||
const SERIALIZATION_CONTEXT_CACHEABILITY = 'cacheability';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\TypedData\ComplexDataInterface;
|
||||
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
|
||||
|
||||
/**
|
||||
* Converts the Drupal entity object structures to a normalized array.
|
||||
*
|
||||
* This is the default Normalizer for entities. All formats that have Encoders
|
||||
* registered with the Serializer in the DIC will be normalized with this
|
||||
* class unless another Normalizer is registered which supersedes it. If a
|
||||
* module wants to use format-specific or class-specific normalization, then
|
||||
* that module can register a new Normalizer and give it a higher priority than
|
||||
* this one.
|
||||
*/
|
||||
class ComplexDataNormalizer extends NormalizerBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = ComplexDataInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
$attributes = [];
|
||||
// $object will not always match $supportedInterfaceOrClass.
|
||||
// @see \Drupal\serialization\Normalizer\EntityNormalizer
|
||||
// Other normalizers that extend this class may only provide $object that
|
||||
// implements \Traversable.
|
||||
if ($object instanceof ComplexDataInterface) {
|
||||
// If there are no properties to normalize, just normalize the value.
|
||||
$object = !empty($object->getProperties(TRUE))
|
||||
? TypedDataInternalPropertiesHelper::getNonInternalProperties($object)
|
||||
: $object->getValue();
|
||||
}
|
||||
/** @var \Drupal\Core\TypedData\TypedDataInterface $property */
|
||||
foreach ($object as $name => $property) {
|
||||
$attributes[$name] = $this->serializer->normalize($property, $format, $context);
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Normalizes/denormalizes Drupal config entity objects into an array structure.
|
||||
*/
|
||||
class ConfigEntityNormalizer extends EntityNormalizer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = ConfigEntityInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
return static::getDataWithoutInternals($object->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
return parent::denormalize(static::getDataWithoutInternals($data), $class, $format, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given data without the internal implementation details.
|
||||
*
|
||||
* @param array $data
|
||||
* The data that is either currently or about to be stored in configuration.
|
||||
*
|
||||
* @return array
|
||||
* The same data, but without internals. Currently, that is only the '_core'
|
||||
* key, which is reserved by Drupal core to handle complex edge cases
|
||||
* correctly. Data in the '_core' key is irrelevant to clients reading
|
||||
* configuration, and is not allowed to be set by clients writing
|
||||
* configuration: it is for Drupal core only, and managed by Drupal core.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2653358
|
||||
*/
|
||||
protected static function getDataWithoutInternals(array $data) {
|
||||
return array_diff_key($data, ['_core' => TRUE]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
|
||||
|
||||
/**
|
||||
* Normalizes/denormalizes Drupal content entities into an array structure.
|
||||
*/
|
||||
class ContentEntityNormalizer extends EntityNormalizer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = ContentEntityInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($entity, $format = NULL, array $context = []) {
|
||||
$context += [
|
||||
'account' => NULL,
|
||||
];
|
||||
|
||||
$attributes = [];
|
||||
/** @var \Drupal\Core\Entity\Entity $entity */
|
||||
foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData()) as $name => $field_items) {
|
||||
if ($field_items->access('view', $context['account'])) {
|
||||
$attributes[$name] = $this->serializer->normalize($field_items, $format, $context);
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
|
||||
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Converts values for the DateTimeIso8601 data type to RFC3339.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DateTimeIso8601Normalizer extends DateTimeNormalizer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $allowedFormats = [
|
||||
'RFC 3339' => \DateTime::RFC3339,
|
||||
'ISO 8601' => \DateTime::ISO8601,
|
||||
// @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416.
|
||||
// RFC3339 only covers combined date and time representations. For date-only
|
||||
// representations, we need to use ISO 8601. There isn't a constant on the
|
||||
// \DateTime class that we can use, so we have to hardcode the format.
|
||||
// @see https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates
|
||||
// @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT
|
||||
'date-only' => 'Y-m-d',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = DateTimeIso8601::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($datetime, $format = NULL, array $context = []) {
|
||||
assert($datetime instanceof DateTimeIso8601);
|
||||
$field_item = $datetime->getParent();
|
||||
// @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416.
|
||||
if ($field_item instanceof DateTimeItem && $field_item->getFieldDefinition()->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
|
||||
$drupal_date_time = $datetime->getDateTime();
|
||||
if ($drupal_date_time === NULL) {
|
||||
return $drupal_date_time;
|
||||
}
|
||||
return $drupal_date_time->format($this->allowedFormats['date-only']);
|
||||
}
|
||||
return parent::normalize($datetime, $format, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
// @todo Move the date-only handling out of here in https://www.drupal.org/project/drupal/issues/2958416.
|
||||
$field_definition = isset($context['target_instance'])
|
||||
? $context['target_instance']->getFieldDefinition()
|
||||
: (isset($context['field_definition']) ? $context['field_definition'] : NULL);
|
||||
if ($field_definition === NULL) {
|
||||
throw new InvalidArgumentException('$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
|
||||
}
|
||||
|
||||
$datetime_type = $field_definition->getSetting('datetime_type');
|
||||
$is_date_only = $datetime_type === DateTimeItem::DATETIME_TYPE_DATE;
|
||||
|
||||
if ($is_date_only) {
|
||||
$context['datetime_allowed_formats'] = array_intersect_key($this->allowedFormats, ['date-only' => TRUE]);
|
||||
$datetime = parent::denormalize($data, $class, $format, $context);
|
||||
if (!$datetime instanceof \DateTime) {
|
||||
return $datetime;
|
||||
}
|
||||
return $datetime->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
|
||||
}
|
||||
|
||||
$context['datetime_allowed_formats'] = array_diff_key($this->allowedFormats, ['date-only' => TRUE]);
|
||||
try {
|
||||
$datetime = parent::denormalize($data, $class, $format, $context);
|
||||
}
|
||||
catch (\UnexpectedValueException $e) {
|
||||
// If denormalization didn't work using any of the actively supported
|
||||
// formats, try again with the BC format too. Explicitly label it as
|
||||
// being deprecated and trigger a deprecation error.
|
||||
$using_deprecated_format = TRUE;
|
||||
$context['datetime_allowed_formats']['backward compatibility — deprecated'] = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
|
||||
$datetime = parent::denormalize($data, $class, $format, $context);
|
||||
}
|
||||
if (!$datetime instanceof \DateTime) {
|
||||
return $datetime;
|
||||
}
|
||||
if (isset($using_deprecated_format)) {
|
||||
@trigger_error('The provided datetime string format (Y-m-d\\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\\TH:i:sP).', E_USER_DEPRECATED);
|
||||
}
|
||||
$datetime->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
|
||||
return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\TypedData\Type\DateTimeInterface;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
/**
|
||||
* Converts values for datetime objects to RFC3339 and from common formats.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface {
|
||||
|
||||
/**
|
||||
* Allowed datetime formats for the denormalizer.
|
||||
*
|
||||
* The list is chosen to be unambiguous and language neutral, but also common
|
||||
* for data interchange.
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
* @see http://php.net/manual/en/datetime.createfromformat.php
|
||||
*/
|
||||
protected $allowedFormats = [
|
||||
'RFC 3339' => \DateTime::RFC3339,
|
||||
'ISO 8601' => \DateTime::ISO8601,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = DateTimeInterface::class;
|
||||
|
||||
/**
|
||||
* The system's date configuration.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
protected $systemDateConfig;
|
||||
|
||||
/**
|
||||
* Constructs a new DateTimeNormalizer instance.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* A config factory for retrieving required config objects.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory) {
|
||||
$this->systemDateConfig = $config_factory->get('system.date');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($datetime, $format = NULL, array $context = []) {
|
||||
assert($datetime instanceof DateTimeInterface);
|
||||
$drupal_date_time = $datetime->getDateTime();
|
||||
if ($drupal_date_time === NULL) {
|
||||
return $drupal_date_time;
|
||||
}
|
||||
return $drupal_date_time
|
||||
// Set an explicit timezone. Otherwise, timestamps may end up being
|
||||
// normalized using the user's preferred timezone. Which would result in
|
||||
// many variations and complex caching.
|
||||
// @see \Drupal\Core\Datetime\DrupalDateTime::prepareTimezone()
|
||||
// @see drupal_get_user_timezone()
|
||||
->setTimezone($this->getNormalizationTimezone())
|
||||
->format(\DateTime::RFC3339);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timezone to be used during normalization.
|
||||
*
|
||||
* @see ::normalize
|
||||
*
|
||||
* @returns \DateTimeZone
|
||||
* The timezone to use.
|
||||
*/
|
||||
protected function getNormalizationTimezone() {
|
||||
$default_site_timezone = $this->systemDateConfig->get('timezone.default');
|
||||
return new \DateTimeZone($default_site_timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
// This only knows how to denormalize datetime strings and timestamps. If
|
||||
// something else is received, let validation constraints handle this.
|
||||
if (!is_string($data) && !is_numeric($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Loop through the allowed formats and create a \DateTime from the
|
||||
// input data if it matches the defined pattern. Since the formats are
|
||||
// unambiguous (i.e., they reference an absolute time with a defined time
|
||||
// zone), only one will ever match.
|
||||
$allowed_formats = isset($context['datetime_allowed_formats'])
|
||||
? $context['datetime_allowed_formats']
|
||||
: $this->allowedFormats;
|
||||
foreach ($allowed_formats as $format) {
|
||||
$date = \DateTime::createFromFormat($format, $data);
|
||||
$errors = \DateTime::getLastErrors();
|
||||
if ($date !== FALSE && empty($errors['errors']) && empty($errors['warnings'])) {
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
||||
$format_strings = [];
|
||||
|
||||
foreach ($allowed_formats as $label => $format) {
|
||||
$format_strings[] = "\"$format\" ($label)";
|
||||
}
|
||||
|
||||
$formats = implode(', ', $format_strings);
|
||||
throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data, $formats));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
/**
|
||||
* Normalizes/denormalizes Drupal entity objects into an array structure.
|
||||
*/
|
||||
class EntityNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
|
||||
|
||||
use FieldableEntityNormalizerTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = EntityInterface::class;
|
||||
|
||||
/**
|
||||
* Constructs an EntityNormalizer object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
|
||||
* The entity type repository.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* The entity field manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeRepositoryInterface $entity_type_repository = NULL, EntityFieldManagerInterface $entity_field_manager = NULL) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
if (!$entity_type_repository) {
|
||||
@trigger_error('The entity_type.repository service must be passed to EntityNormalizer::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
|
||||
$entity_type_repository = \Drupal::service('entity_type.repository');
|
||||
}
|
||||
$this->entityTypeRepository = $entity_type_repository;
|
||||
if (!$entity_field_manager) {
|
||||
@trigger_error('The entity_field.manager service must be passed to EntityNormalizer::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
|
||||
$entity_field_manager = \Drupal::service('entity_field.manager');
|
||||
}
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
$entity_type_id = $this->determineEntityTypeId($class, $context);
|
||||
$entity_type_definition = $this->getEntityTypeDefinition($entity_type_id);
|
||||
|
||||
// The bundle property will be required to denormalize a bundleable
|
||||
// fieldable entity.
|
||||
if ($entity_type_definition->entityClassImplements(FieldableEntityInterface::class)) {
|
||||
// Extract bundle data to pass into entity creation if the entity type uses
|
||||
// bundles.
|
||||
if ($entity_type_definition->hasKey('bundle')) {
|
||||
// Get an array containing the bundle only. This also remove the bundle
|
||||
// key from the $data array.
|
||||
$create_params = $this->extractBundleData($data, $entity_type_definition);
|
||||
}
|
||||
else {
|
||||
$create_params = [];
|
||||
}
|
||||
|
||||
// Create the entity from bundle data only, then apply field values after.
|
||||
$entity = $this->entityTypeManager->getStorage($entity_type_id)->create($create_params);
|
||||
|
||||
$this->denormalizeFieldData($data, $entity, $format, $context);
|
||||
}
|
||||
else {
|
||||
// Create the entity from all data.
|
||||
$entity = $this->entityTypeManager->getStorage($entity_type_id)->create($data);
|
||||
}
|
||||
|
||||
// Pass the names of the fields whose values can be merged.
|
||||
// @todo https://www.drupal.org/node/2456257 remove this.
|
||||
$entity->_restSubmittedFields = array_keys($data);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Adds the file URI to embedded file entities.
|
||||
*/
|
||||
class EntityReferenceFieldItemNormalizer extends FieldItemNormalizer {
|
||||
|
||||
use EntityReferenceFieldItemNormalizerTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = EntityReferenceItem::class;
|
||||
|
||||
/**
|
||||
* The entity repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityRepositoryInterface
|
||||
*/
|
||||
protected $entityRepository;
|
||||
|
||||
/**
|
||||
* Constructs a EntityReferenceFieldItemNormalizer object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository.
|
||||
*/
|
||||
public function __construct(EntityRepositoryInterface $entity_repository) {
|
||||
$this->entityRepository = $entity_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($field_item, $format = NULL, array $context = []) {
|
||||
$values = parent::normalize($field_item, $format, $context);
|
||||
|
||||
$this->normalizeRootReferenceValue($values, $field_item);
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
if ($entity = $field_item->get('entity')->getValue()) {
|
||||
$values['target_type'] = $entity->getEntityTypeId();
|
||||
// Add the target entity UUID to the normalized output values.
|
||||
$values['target_uuid'] = $entity->uuid();
|
||||
|
||||
// Add a 'url' value if there is a reference and a canonical URL. Hard
|
||||
// code 'canonical' here as config entities override the default $rel
|
||||
// parameter value to 'edit-form.
|
||||
if ($entity->hasLinkTemplate('canonical') && !$entity->isNew() && $url = $entity->toUrl('canonical')->toString(TRUE)) {
|
||||
$this->addCacheableDependency($context, $url);
|
||||
$values['url'] = $url->getGeneratedUrl();
|
||||
}
|
||||
// @todo Remove in https://www.drupal.org/project/drupal/issues/2925520
|
||||
// @see \Drupal\hal\Normalizer\FileEntityNormalizer
|
||||
elseif ($entity->getEntityTypeId() === 'file') {
|
||||
$values['url'] = file_create_url($entity->getFileUri());
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function constructValue($data, $context) {
|
||||
if (isset($data['target_uuid'])) {
|
||||
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item */
|
||||
$field_item = $context['target_instance'];
|
||||
if (empty($data['target_uuid'])) {
|
||||
throw new InvalidArgumentException(sprintf('If provided "target_uuid" cannot be empty for field "%s".', $data['target_type'], $data['target_uuid'], $field_item->getName()));
|
||||
}
|
||||
$target_type = $field_item->getFieldDefinition()->getSetting('target_type');
|
||||
if (!empty($data['target_type']) && $target_type !== $data['target_type']) {
|
||||
throw new UnexpectedValueException(sprintf('The field "%s" property "target_type" must be set to "%s" or omitted.', $field_item->getFieldDefinition()->getName(), $target_type));
|
||||
}
|
||||
if ($entity = $this->entityRepository->loadEntityByUuid($target_type, $data['target_uuid'])) {
|
||||
return ['target_id' => $entity->id()] + array_intersect_key($data, $field_item->getProperties());
|
||||
}
|
||||
else {
|
||||
// Unable to load entity by uuid.
|
||||
throw new InvalidArgumentException(sprintf('No "%s" entity found with UUID "%s" for field "%s".', $data['target_type'], $data['target_uuid'], $field_item->getName()));
|
||||
}
|
||||
}
|
||||
return parent::constructValue($data, $context);
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
|
||||
|
||||
/**
|
||||
* Converts empty reference values for entity reference items.
|
||||
*/
|
||||
trait EntityReferenceFieldItemNormalizerTrait {
|
||||
|
||||
protected function normalizeRootReferenceValue(&$values, EntityReferenceItem $field_item) {
|
||||
// @todo Generalize for all tree-structured entity types.
|
||||
if ($this->fieldItemReferencesTaxonomyTerm($field_item) && empty($values['target_id'])) {
|
||||
$values['target_id'] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a field item references a taxonomy term.
|
||||
*
|
||||
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function fieldItemReferencesTaxonomyTerm(EntityReferenceItem $field_item) {
|
||||
return $field_item->getFieldDefinition()->getSetting('target_type') === 'taxonomy_term';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\FieldItemInterface;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
/**
|
||||
* Denormalizes field item object structure by updating the entity field values.
|
||||
*/
|
||||
class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
|
||||
|
||||
use FieldableEntityNormalizerTrait;
|
||||
use SerializedColumnNormalizerTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = FieldItemInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
if (!isset($context['target_instance'])) {
|
||||
throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
|
||||
}
|
||||
|
||||
if ($context['target_instance']->getParent() == NULL) {
|
||||
throw new InvalidArgumentException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Field\FieldItemInterface $field_item */
|
||||
$field_item = $context['target_instance'];
|
||||
$this->checkForSerializedStrings($data, $class, $field_item);
|
||||
|
||||
$field_item->setValue($this->constructValue($data, $context));
|
||||
return $field_item;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
/**
|
||||
* Denormalizes data to Drupal field values.
|
||||
*
|
||||
* This class simply calls denormalize() on the individual FieldItems. The
|
||||
* FieldItem normalizers are responsible for setting the field values for each
|
||||
* item.
|
||||
*
|
||||
* @see \Drupal\serialization\Normalizer\FieldItemNormalizer.
|
||||
*/
|
||||
class FieldNormalizer extends ListNormalizer implements DenormalizerInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = FieldItemListInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
if (!isset($context['target_instance'])) {
|
||||
throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
|
||||
}
|
||||
|
||||
if ($context['target_instance']->getParent() == NULL) {
|
||||
throw new InvalidArgumentException('The field passed in via $context[\'target_instance\'] must have a parent set.');
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
|
||||
$items = $context['target_instance'];
|
||||
$item_class = $items->getItemDefinition()->getClass();
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new UnexpectedValueException(sprintf('Field values for "%s" must use an array structure', $items->getName()));
|
||||
}
|
||||
|
||||
foreach ($data as $item_data) {
|
||||
// Create a new item and pass it as the target for the unserialization of
|
||||
// $item_data. All items in field should have removed before this method
|
||||
// was called.
|
||||
// @see \Drupal\serialization\Normalizer\ContentEntityNormalizer::denormalize().
|
||||
$context['target_instance'] = $items->appendItem();
|
||||
$this->serializer->denormalize($item_data, $item_class, $format, $context);
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldItemInterface;
|
||||
use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* A trait for providing fieldable entity normalization/denormalization methods.
|
||||
*
|
||||
* @todo Move this into a FieldableEntityNormalizer in Drupal 9. This is a trait
|
||||
* used in \Drupal\serialization\Normalizer\EntityNormalizer to maintain BC.
|
||||
* @see https://www.drupal.org/node/2834734
|
||||
*/
|
||||
trait FieldableEntityNormalizerTrait {
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
|
||||
*/
|
||||
protected $entityTypeRepository;
|
||||
|
||||
/**
|
||||
* Determines the entity type ID to denormalize as.
|
||||
*
|
||||
* @param string $class
|
||||
* The entity type class to be denormalized to.
|
||||
* @param array $context
|
||||
* The serialization context data.
|
||||
*
|
||||
* @return string
|
||||
* The entity type ID.
|
||||
*/
|
||||
protected function determineEntityTypeId($class, $context) {
|
||||
// Get the entity type ID while letting context override the $class param.
|
||||
return !empty($context['entity_type']) ? $context['entity_type'] : $this->getEntityTypeRepository()->getEntityTypeFromClass($class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity type definition.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to load the definition for.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface
|
||||
* The loaded entity type definition.
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
|
||||
*/
|
||||
protected function getEntityTypeDefinition($entity_type_id) {
|
||||
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition */
|
||||
// Get the entity type definition.
|
||||
$entity_type_definition = $this->getEntityTypeManager()->getDefinition($entity_type_id, FALSE);
|
||||
|
||||
// Don't try to create an entity without an entity type id.
|
||||
if (!$entity_type_definition) {
|
||||
throw new UnexpectedValueException(sprintf('The specified entity type "%s" does not exist. A valid entity type is required for denormalization', $entity_type_id));
|
||||
}
|
||||
|
||||
return $entity_type_definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes the bundle property so entity creation can use it.
|
||||
*
|
||||
* @param array $data
|
||||
* The data being denormalized.
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition
|
||||
* The entity type definition.
|
||||
*
|
||||
* @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
|
||||
*
|
||||
* @return string
|
||||
* The valid bundle name.
|
||||
*/
|
||||
protected function extractBundleData(array &$data, EntityTypeInterface $entity_type_definition) {
|
||||
$bundle_key = $entity_type_definition->getKey('bundle');
|
||||
// Get the base field definitions for this entity type.
|
||||
$base_field_definitions = $this->getEntityFieldManager()->getBaseFieldDefinitions($entity_type_definition->id());
|
||||
|
||||
// Get the ID key from the base field definition for the bundle key or
|
||||
// default to 'value'.
|
||||
$key_id = isset($base_field_definitions[$bundle_key]) ? $base_field_definitions[$bundle_key]->getFieldStorageDefinition()->getMainPropertyName() : 'value';
|
||||
|
||||
// Normalize the bundle if it is not explicitly set.
|
||||
$bundle_value = isset($data[$bundle_key][0][$key_id]) ? $data[$bundle_key][0][$key_id] : (isset($data[$bundle_key]) ? $data[$bundle_key] : NULL);
|
||||
// Unset the bundle from the data.
|
||||
unset($data[$bundle_key]);
|
||||
|
||||
// Get the bundle entity type from the entity type definition.
|
||||
$bundle_type_id = $entity_type_definition->getBundleEntityType();
|
||||
$bundle_types = $bundle_type_id ? $this->getEntityTypeManager()->getStorage($bundle_type_id)->getQuery()->execute() : [];
|
||||
|
||||
// Make sure a bundle has been provided.
|
||||
if (!is_string($bundle_value)) {
|
||||
throw new UnexpectedValueException(sprintf('Could not determine entity type bundle: "%s" field is missing.', $bundle_key));
|
||||
}
|
||||
|
||||
// Make sure the submitted bundle is a valid bundle for the entity type.
|
||||
if ($bundle_types && !in_array($bundle_value, $bundle_types)) {
|
||||
throw new UnexpectedValueException(sprintf('"%s" is not a valid bundle type for denormalization.', $bundle_value));
|
||||
}
|
||||
|
||||
return [$bundle_key => $bundle_value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes entity data by denormalizing each field individually.
|
||||
*
|
||||
* @param array $data
|
||||
* The data to denormalize.
|
||||
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
|
||||
* The fieldable entity to set field values for.
|
||||
* @param string $format
|
||||
* The serialization format.
|
||||
* @param array $context
|
||||
* The context data.
|
||||
*/
|
||||
protected function denormalizeFieldData(array $data, FieldableEntityInterface $entity, $format, array $context) {
|
||||
foreach ($data as $field_name => $field_data) {
|
||||
$field_item_list = $entity->get($field_name);
|
||||
|
||||
// Remove any values that were set as a part of entity creation (e.g
|
||||
// uuid). If the incoming field data is set to an empty array, this will
|
||||
// also have the effect of emptying the field in REST module.
|
||||
$field_item_list->setValue([]);
|
||||
$field_item_list_class = get_class($field_item_list);
|
||||
|
||||
if ($field_data) {
|
||||
// The field instance must be passed in the context so that the field
|
||||
// denormalizer can update field values for the parent entity.
|
||||
$context['target_instance'] = $field_item_list;
|
||||
$this->serializer->denormalize($field_data, $field_item_list_class, $format, $context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entity type repository.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeRepositoryInterface
|
||||
* The entity type repository.
|
||||
*/
|
||||
protected function getEntityTypeRepository() {
|
||||
if (!$this->entityTypeRepository) {
|
||||
@trigger_error('The entityTypeRepository property must be set on the FieldEntityNormalizerTrait, it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
|
||||
$this->entityTypeRepository = \Drupal::service('entity_type.repository');
|
||||
}
|
||||
return $this->entityTypeRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entity field manager.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
* The entity field manager.
|
||||
*/
|
||||
protected function getEntityFieldManager() {
|
||||
if (!$this->entityFieldManager) {
|
||||
@trigger_error('The entityFieldManager property must be set on the FieldEntityNormalizerTrait, it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
|
||||
$this->entityFieldManager = \Drupal::service('entity_field.manager');
|
||||
}
|
||||
return $this->entityFieldManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entity type manager.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
* The entity type manager.
|
||||
*/
|
||||
protected function getEntityTypeManager() {
|
||||
if (!$this->entityTypeManager) {
|
||||
@trigger_error('The entityTypeManager property must be set on the FieldEntityNormalizerTrait, it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
|
||||
$this->entityTypeManager = \Drupal::service('entity_type.manager');
|
||||
}
|
||||
return $this->entityTypeManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the field item value using the incoming data.
|
||||
*
|
||||
* Most normalizers that extend this class can simply use this method to
|
||||
* construct the denormalized value without having to override denormalize()
|
||||
* and reimplementing its validation logic or its call to set the field value.
|
||||
*
|
||||
* It's recommended to not override this and instead provide a (de)normalizer
|
||||
* at the DataType level.
|
||||
*
|
||||
* @param mixed $data
|
||||
* The incoming data for this field item.
|
||||
* @param array $context
|
||||
* The context passed into the Normalizer.
|
||||
*
|
||||
* @return mixed
|
||||
* The value to use in Entity::setValue().
|
||||
*/
|
||||
protected function constructValue($data, $context) {
|
||||
$field_item = $context['target_instance'];
|
||||
|
||||
// Get the property definitions.
|
||||
assert($field_item instanceof FieldItemInterface);
|
||||
$field_definition = $field_item->getFieldDefinition();
|
||||
$item_definition = $field_definition->getItemDefinition();
|
||||
assert($item_definition instanceof FieldItemDataDefinitionInterface);
|
||||
$property_definitions = $item_definition->getPropertyDefinitions();
|
||||
|
||||
$serialized_property_names = $this->getCustomSerializedPropertyNames($field_item);
|
||||
$denormalize_property = function ($property_name, $property_value, $property_value_class, $context) use ($serialized_property_names) {
|
||||
if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
|
||||
return $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
|
||||
}
|
||||
else {
|
||||
if (in_array($property_name, $serialized_property_names, TRUE)) {
|
||||
$property_value = serialize($property_value);
|
||||
}
|
||||
return $property_value;
|
||||
}
|
||||
};
|
||||
|
||||
if (!is_array($data)) {
|
||||
$property_value = $data;
|
||||
$property_name = $item_definition->getMainPropertyName();
|
||||
$property_value_class = $property_definitions[$property_name]->getClass();
|
||||
return $denormalize_property($property_name, $property_value, $property_value_class, $context);
|
||||
}
|
||||
|
||||
$data_internal = [];
|
||||
if (!empty($property_definitions)) {
|
||||
foreach ($property_definitions as $property_name => $property_definition) {
|
||||
// Not every property is required to be sent.
|
||||
if (!array_key_exists($property_name, $data)) {
|
||||
continue;
|
||||
}
|
||||
$property_value = $data[$property_name];
|
||||
$property_value_class = $property_definition->getClass();
|
||||
$data_internal[$property_name] = $denormalize_property($property_name, $property_value, $property_value_class, $context);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$data_internal = $data;
|
||||
}
|
||||
|
||||
return $data_internal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\TypedData\ListInterface;
|
||||
|
||||
/**
|
||||
* Converts list objects to arrays.
|
||||
*
|
||||
* Ordinarily, this would be handled automatically by Serializer, but since
|
||||
* there is a TypedDataNormalizer and the Field class extends TypedData, any
|
||||
* Field will be handled by that Normalizer instead of being traversed. This
|
||||
* class ensures that TypedData classes that also implement ListInterface are
|
||||
* traversed instead of simply returning getValue().
|
||||
*/
|
||||
class ListNormalizer extends NormalizerBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = ListInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
$attributes = [];
|
||||
foreach ($object as $fieldItem) {
|
||||
$attributes[] = $this->serializer->normalize($fieldItem, $format, $context);
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Component\Render\MarkupInterface;
|
||||
|
||||
/**
|
||||
* Normalizes MarkupInterface objects into a string.
|
||||
*/
|
||||
class MarkupNormalizer extends NormalizerBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = MarkupInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
return (string) $object;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Symfony\Component\Serializer\SerializerAwareInterface;
|
||||
use Symfony\Component\Serializer\SerializerAwareTrait;
|
||||
|
||||
/**
|
||||
* Base class for Normalizers.
|
||||
*/
|
||||
abstract class NormalizerBase implements SerializerAwareInterface, CacheableNormalizerInterface {
|
||||
|
||||
use SerializerAwareTrait;
|
||||
|
||||
/**
|
||||
* The interface or class that this Normalizer supports.
|
||||
*
|
||||
* @var string|array
|
||||
*/
|
||||
protected $supportedInterfaceOrClass;
|
||||
|
||||
/**
|
||||
* List of formats which supports (de-)normalization.
|
||||
*
|
||||
* @var string|string[]
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supportsNormalization($data, $format = NULL) {
|
||||
// If we aren't dealing with an object or the format is not supported return
|
||||
// now.
|
||||
if (!is_object($data) || !$this->checkFormat($format)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$supported = (array) $this->supportedInterfaceOrClass;
|
||||
|
||||
return (bool) array_filter($supported, function ($name) use ($data) {
|
||||
return $data instanceof $name;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization()
|
||||
*
|
||||
* This class doesn't implement DenormalizerInterface, but most of its child
|
||||
* classes do, so this method is implemented at this level to reduce code
|
||||
* duplication.
|
||||
*/
|
||||
public function supportsDenormalization($data, $type, $format = NULL) {
|
||||
// If the format is not supported return now.
|
||||
if (!$this->checkFormat($format)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$supported = (array) $this->supportedInterfaceOrClass;
|
||||
|
||||
$subclass_check = function ($name) use ($type) {
|
||||
return (class_exists($name) || interface_exists($name)) && is_subclass_of($type, $name, TRUE);
|
||||
};
|
||||
|
||||
return in_array($type, $supported) || array_filter($supported, $subclass_check);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided format is supported by this normalizer.
|
||||
*
|
||||
* @param string $format
|
||||
* The format to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the format is supported, FALSE otherwise. If no format is
|
||||
* specified this will return TRUE.
|
||||
*/
|
||||
protected function checkFormat($format = NULL) {
|
||||
if (!isset($format) || !isset($this->format)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return in_array($format, (array) $this->format, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cacheability if applicable.
|
||||
*
|
||||
* @param array $context
|
||||
* Context options for the normalizer.
|
||||
* @param $data
|
||||
* The data that might have cacheability information.
|
||||
*/
|
||||
protected function addCacheableDependency(array $context, $data) {
|
||||
if ($data instanceof CacheableDependencyInterface && isset($context[static::SERIALIZATION_CONTEXT_CACHEABILITY])) {
|
||||
$context[static::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
/**
|
||||
* Null normalizer.
|
||||
*/
|
||||
class NullNormalizer extends NormalizerBase {
|
||||
|
||||
/**
|
||||
* Constructs a NullNormalizer object.
|
||||
*
|
||||
* @param string|array $supported_interface_of_class
|
||||
* The supported interface(s) or class(es).
|
||||
*/
|
||||
public function __construct($supported_interface_of_class) {
|
||||
$this->supportedInterfaceOrClass = $supported_interface_of_class;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\FieldItemInterface;
|
||||
use Drupal\Core\TypedData\PrimitiveInterface;
|
||||
|
||||
/**
|
||||
* Converts primitive data objects to their casted values.
|
||||
*/
|
||||
class PrimitiveDataNormalizer extends NormalizerBase {
|
||||
|
||||
use SerializedColumnNormalizerTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = PrimitiveInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
$parent = $object->getParent();
|
||||
if ($parent instanceof FieldItemInterface && $object->getValue()) {
|
||||
$serialized_property_names = $this->getCustomSerializedPropertyNames($parent);
|
||||
if (in_array($object->getName(), $serialized_property_names, TRUE)) {
|
||||
return unserialize($object->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// Typed data casts NULL objects to their empty variants, so for example
|
||||
// the empty string ('') for string type data, or 0 for integer typed data.
|
||||
// In a better world with typed data implementing algebraic data types,
|
||||
// getCastedValue would return NULL, but as typed data is not aware of real
|
||||
// optional values on the primitive level, we implement our own optional
|
||||
// value normalization here.
|
||||
return $object->getValue() === NULL ? NULL : $object->getCastedValue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Field\FieldItemInterface;
|
||||
|
||||
/**
|
||||
* A trait providing methods for serialized columns.
|
||||
*/
|
||||
trait SerializedColumnNormalizerTrait {
|
||||
|
||||
/**
|
||||
* Checks if there is a serialized string for a column.
|
||||
*
|
||||
* @param mixed $data
|
||||
* The field item data to denormalize.
|
||||
* @param string $class
|
||||
* The expected class to instantiate.
|
||||
* @param \Drupal\Core\Field\FieldItemInterface $field_item
|
||||
* The field item.
|
||||
*/
|
||||
protected function checkForSerializedStrings($data, $class, FieldItemInterface $field_item) {
|
||||
// Require specialized denormalizers for fields with 'serialize' columns.
|
||||
// Note: this cannot be checked in ::supportsDenormalization() because at
|
||||
// that time we only have the field item class. ::hasSerializeColumn()
|
||||
// must be able to call $field_item->schema(), which requires a field
|
||||
// storage definition. To determine that, the entity type and bundle
|
||||
// must be known, which is contextual information that the Symfony
|
||||
// serializer does not pass to ::supportsDenormalization().
|
||||
if (!is_array($data)) {
|
||||
$data = [$field_item->getDataDefinition()->getMainPropertyName() => $data];
|
||||
}
|
||||
if ($this->dataHasStringForSerializeColumn($field_item, $data)) {
|
||||
$field_name = $field_item->getParent() ? $field_item->getParent()->getName() : $field_item->getName();
|
||||
throw new \LogicException(sprintf('The generic FieldItemNormalizer cannot denormalize string values for "%s" properties of the "%s" field (field item class: %s).', implode('", "', $this->getSerializedPropertyNames($field_item)), $field_name, $class));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the data contains string value for serialize column.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldItemInterface $field_item
|
||||
* The field item.
|
||||
* @param array $data
|
||||
* The data being denormalized.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if there is a string value for serialize column, otherwise FALSE.
|
||||
*/
|
||||
protected function dataHasStringForSerializeColumn(FieldItemInterface $field_item, array $data) {
|
||||
foreach ($this->getSerializedPropertyNames($field_item) as $property_name) {
|
||||
if (isset($data[$property_name]) && is_string($data[$property_name])) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the names of all serialized properties.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldItemInterface $field_item
|
||||
* The field item.
|
||||
*
|
||||
* @return string[]
|
||||
* The property names for serialized properties.
|
||||
*/
|
||||
protected function getSerializedPropertyNames(FieldItemInterface $field_item) {
|
||||
$field_storage_definition = $field_item->getFieldDefinition()->getFieldStorageDefinition();
|
||||
|
||||
if ($custom_property_names = $this->getCustomSerializedPropertyNames($field_item)) {
|
||||
return $custom_property_names;
|
||||
}
|
||||
|
||||
$field_storage_schema = $field_item->schema($field_storage_definition);
|
||||
// If there are no columns then there are no serialized properties.
|
||||
if (!isset($field_storage_schema['columns'])) {
|
||||
return [];
|
||||
}
|
||||
$serialized_columns = array_filter($field_storage_schema['columns'], function ($column_schema) {
|
||||
return isset($column_schema['serialize']) && $column_schema['serialize'] === TRUE;
|
||||
});
|
||||
return array_keys($serialized_columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the names of all properties the plugin treats as serialized data.
|
||||
*
|
||||
* This allows the field storage definition or entity type to provide a
|
||||
* setting for serialized properties. This can be used for fields that
|
||||
* handle serialized data themselves and do not rely on the serialized schema
|
||||
* flag.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldItemInterface $field_item
|
||||
* The field item.
|
||||
*
|
||||
* @return string[]
|
||||
* The property names for serialized properties.
|
||||
*/
|
||||
protected function getCustomSerializedPropertyNames(FieldItemInterface $field_item) {
|
||||
if ($field_item instanceof PluginInspectionInterface) {
|
||||
$definition = $field_item->getPluginDefinition();
|
||||
$serialized_fields = $field_item->getEntity()->getEntityType()->get('serialized_field_property_names');
|
||||
$field_name = $field_item->getFieldDefinition()->getName();
|
||||
if (is_array($serialized_fields) && isset($serialized_fields[$field_name]) && is_array($serialized_fields[$field_name])) {
|
||||
return $serialized_fields[$field_name];
|
||||
}
|
||||
if (isset($definition['serialized_property_names']) && is_array($definition['serialized_property_names'])) {
|
||||
return $definition['serialized_property_names'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||
|
||||
@trigger_error(__NAMESPACE__ . '\TimeStampItemNormalizerTrait is deprecated in Drupal 8.7.0 and will be removed in Drupal 9.0.0. Use \Drupal\serialization\Normalizer\TimestampNormalizer instead.', E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* A trait for TimestampItem normalization functionality.
|
||||
*
|
||||
* @deprecated in 8.7.0, use \Drupal\serialization\Normalizer\TimestampNormalizer instead.
|
||||
*/
|
||||
trait TimeStampItemNormalizerTrait {
|
||||
|
||||
/**
|
||||
* Allowed timestamps formats for the denormalizer.
|
||||
*
|
||||
* The denormalizer allows deserialization to timestamps from three
|
||||
* different formats. Validation of the input data and creation of the
|
||||
* numerical timestamp value is handled with \DateTime::createFromFormat().
|
||||
* The list is chosen to be unambiguous and language neutral, but also common
|
||||
* for data interchange.
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
* @see http://php.net/manual/datetime.createfromformat.php
|
||||
*/
|
||||
protected $allowedFormats = [
|
||||
'UNIX timestamp' => 'U',
|
||||
'ISO 8601' => \DateTime::ISO8601,
|
||||
'RFC 3339' => \DateTime::RFC3339,
|
||||
];
|
||||
|
||||
/**
|
||||
* Processes normalized timestamp values to add a formatted date and format.
|
||||
*
|
||||
* @param array $normalized
|
||||
* The normalized field data to process.
|
||||
* @return array
|
||||
* The processed data.
|
||||
*/
|
||||
protected function processNormalizedValues(array $normalized) {
|
||||
// Use a RFC 3339 timestamp with the time zone set to UTC to replace the
|
||||
// timestamp value.
|
||||
$date = new \DateTime();
|
||||
$date->setTimestamp($normalized['value']);
|
||||
$date->setTimezone(new \DateTimeZone('UTC'));
|
||||
$normalized['value'] = $date->format(\DateTime::RFC3339);
|
||||
// 'format' is not a property on TimestampItem fields. This is present to
|
||||
// assist consumers of this data.
|
||||
$normalized['format'] = \DateTime::RFC3339;
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function constructValue($data, $context) {
|
||||
// Loop through the allowed formats and create a TimestampItem from the
|
||||
// input data if it matches the defined pattern. Since the formats are
|
||||
// unambiguous (i.e., they reference an absolute time with a defined time
|
||||
// zone), only one will ever match.
|
||||
$timezone = new \DateTimeZone('UTC');
|
||||
|
||||
// First check for a provided format.
|
||||
if (!empty($data['format']) && in_array($data['format'], $this->allowedFormats)) {
|
||||
$date = \DateTime::createFromFormat($data['format'], $data['value'], $timezone);
|
||||
return ['value' => $date->getTimestamp()];
|
||||
}
|
||||
// Otherwise, loop through formats.
|
||||
else {
|
||||
foreach ($this->allowedFormats as $format) {
|
||||
if (($date = \DateTime::createFromFormat($format, $data['value'], $timezone)) !== FALSE) {
|
||||
return ['value' => $date->getTimestamp()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$format_strings = [];
|
||||
|
||||
foreach ($this->allowedFormats as $label => $format) {
|
||||
$format_strings[] = "\"$format\" ($label)";
|
||||
}
|
||||
|
||||
$formats = implode(', ', $format_strings);
|
||||
throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data['value'], $formats));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
|
||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
|
||||
|
||||
/**
|
||||
* Converts values for TimestampItem to and from common formats.
|
||||
*
|
||||
* Overrides FieldItemNormalizer to use \Drupal\serialization\Normalizer\TimestampNormalizer
|
||||
*
|
||||
* Overrides FieldItemNormalizer to
|
||||
* - during normalization, add the 'format' key to assist consumers
|
||||
* - during denormalization, use \Drupal\serialization\Normalizer\TimestampNormalizer
|
||||
*/
|
||||
class TimestampItemNormalizer extends FieldItemNormalizer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = TimestampItem::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
return parent::normalize($object, $format, $context) + [
|
||||
// 'format' is not a property on Timestamp objects. This is present to
|
||||
// assist consumers of this data.
|
||||
'format' => \DateTime::RFC3339,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function constructValue($data, $context) {
|
||||
if (!empty($data['format'])) {
|
||||
$context['datetime_allowed_formats'] = [$data['format']];
|
||||
}
|
||||
return ['value' => $this->serializer->denormalize($data['value'], Timestamp::class, NULL, $context)];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
|
||||
|
||||
/**
|
||||
* Converts values for the Timestamp data type to and from common formats.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* Note that \Drupal\Core\TypedData\Plugin\DataType\Timestamp::getDateTime()
|
||||
* explicitly sets a default timezone of UTC. This ensures the string
|
||||
* representation generated by DateTimeNormalizer::normalize() is also in UTC.
|
||||
*/
|
||||
class TimestampNormalizer extends DateTimeNormalizer {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $allowedFormats = [
|
||||
'UNIX timestamp' => 'U',
|
||||
'ISO 8601' => \DateTime::ISO8601,
|
||||
'RFC 3339' => \DateTime::RFC3339,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = Timestamp::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getNormalizationTimezone() {
|
||||
return new \DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function denormalize($data, $class, $format = NULL, array $context = []) {
|
||||
$denormalized = parent::denormalize($data, $class, $format, $context);
|
||||
return $denormalized->getTimestamp();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Normalizer;
|
||||
|
||||
use Drupal\Core\TypedData\TypedDataInterface;
|
||||
|
||||
/**
|
||||
* Converts typed data objects to arrays.
|
||||
*/
|
||||
class TypedDataNormalizer extends NormalizerBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $supportedInterfaceOrClass = TypedDataInterface::class;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function normalize($object, $format = NULL, array $context = []) {
|
||||
$this->addCacheableDependency($context, $object);
|
||||
$value = $object->getValue();
|
||||
// Support for stringable value objects: avoid numerous custom normalizers.
|
||||
if (is_object($value) && method_exists($value, '__toString')) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
|
||||
/**
|
||||
* Adds services tagged 'normalizer' and 'encoder' to the Serializer.
|
||||
*/
|
||||
class RegisterEntityResolversCompilerPass implements CompilerPassInterface {
|
||||
|
||||
/**
|
||||
* Adds services to the Serializer.
|
||||
*
|
||||
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
|
||||
* The container to process.
|
||||
*/
|
||||
public function process(ContainerBuilder $container) {
|
||||
$definition = $container->getDefinition('serializer.entity_resolver');
|
||||
$resolvers = [];
|
||||
|
||||
// Retrieve registered Normalizers and Encoders from the container.
|
||||
foreach ($container->findTaggedServiceIds('entity_resolver') as $id => $attributes) {
|
||||
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
|
||||
$resolvers[$priority][] = new Reference($id);
|
||||
}
|
||||
|
||||
// Add the registered concrete EntityResolvers to the ChainEntityResolver.
|
||||
foreach ($this->sort($resolvers) as $resolver) {
|
||||
$definition->addMethodCall('addResolver', [$resolver]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts by priority.
|
||||
*
|
||||
* Order services from highest priority number to lowest (reverse sorting).
|
||||
*
|
||||
* @param array $services
|
||||
* A nested array keyed on priority number. For each priority number, the
|
||||
* value is an array of Symfony\Component\DependencyInjection\Reference
|
||||
* objects, each a reference to a normalizer or encoder service.
|
||||
*
|
||||
* @return array
|
||||
* A flattened array of Reference objects from $services, ordered from high
|
||||
* to low priority.
|
||||
*/
|
||||
protected function sort($services) {
|
||||
$sorted = [];
|
||||
krsort($services);
|
||||
|
||||
// Flatten the array.
|
||||
foreach ($services as $a) {
|
||||
$sorted = array_merge($sorted, $a);
|
||||
}
|
||||
|
||||
return $sorted;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization;
|
||||
|
||||
use Drupal\Core\Config\BootstrapConfigStorageFactory;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
|
||||
/**
|
||||
* Adds services tagged 'normalizer' and 'encoder' to the Serializer.
|
||||
*/
|
||||
class RegisterSerializationClassesCompilerPass implements CompilerPassInterface {
|
||||
|
||||
/**
|
||||
* Adds services to the Serializer.
|
||||
*
|
||||
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
|
||||
* The container to process.
|
||||
*/
|
||||
public function process(ContainerBuilder $container) {
|
||||
$definition = $container->getDefinition('serializer');
|
||||
|
||||
// Retrieve registered Normalizers and Encoders from the container.
|
||||
foreach ($container->findTaggedServiceIds('normalizer') as $id => $attributes) {
|
||||
// The 'serializer' service is the public API: mark normalizers private.
|
||||
$container->getDefinition($id)->setPublic(FALSE);
|
||||
|
||||
// If there is a BC key present, pass this to determine if the normalizer
|
||||
// should be skipped.
|
||||
if (isset($attributes[0]['bc']) && $this->normalizerBcSettingIsEnabled($attributes[0]['bc'], $attributes[0]['bc_config_name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
|
||||
$normalizers[$priority][] = new Reference($id);
|
||||
}
|
||||
foreach ($container->findTaggedServiceIds('encoder') as $id => $attributes) {
|
||||
// The 'serializer' service is the public API: mark encoders private.
|
||||
$container->getDefinition($id)->setPublic(FALSE);
|
||||
|
||||
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
|
||||
$encoders[$priority][] = new Reference($id);
|
||||
}
|
||||
|
||||
// Add the registered Normalizers and Encoders to the Serializer.
|
||||
if (!empty($normalizers)) {
|
||||
$definition->replaceArgument(0, $this->sort($normalizers));
|
||||
}
|
||||
if (!empty($encoders)) {
|
||||
$definition->replaceArgument(1, $this->sort($encoders));
|
||||
}
|
||||
|
||||
// Find all serialization formats known.
|
||||
$formats = [];
|
||||
$format_providers = [];
|
||||
foreach ($container->findTaggedServiceIds('encoder') as $service_id => $attributes) {
|
||||
$format = $attributes[0]['format'];
|
||||
$formats[] = $format;
|
||||
|
||||
if ($provider_tag = $container->getDefinition($service_id)->getTag('_provider')) {
|
||||
$format_providers[$format] = $provider_tag[0]['provider'];
|
||||
}
|
||||
}
|
||||
$container->setParameter('serializer.formats', $formats);
|
||||
$container->setParameter('serializer.format_providers', $format_providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a normalizer BC setting is disabled or not.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function normalizerBcSettingIsEnabled($key, $config_name) {
|
||||
$settings = BootstrapConfigStorageFactory::get()->read($config_name);
|
||||
return !empty($settings[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts by priority.
|
||||
*
|
||||
* Order services from highest priority number to lowest (reverse sorting).
|
||||
*
|
||||
* @param array $services
|
||||
* A nested array keyed on priority number. For each priority number, the
|
||||
* value is an array of Symfony\Component\DependencyInjection\Reference
|
||||
* objects, each a reference to a normalizer or encoder service.
|
||||
*
|
||||
* @return array
|
||||
* A flattened array of Reference objects from $services, ordered from high
|
||||
* to low priority.
|
||||
*/
|
||||
protected function sort($services) {
|
||||
$sorted = [];
|
||||
krsort($services);
|
||||
|
||||
// Flatten the array.
|
||||
foreach ($services as $a) {
|
||||
$sorted = array_merge($sorted, $a);
|
||||
}
|
||||
|
||||
return $sorted;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* Serialization dependency injection container.
|
||||
*/
|
||||
class SerializationServiceProvider implements ServiceProviderInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function register(ContainerBuilder $container) {
|
||||
// Add a compiler pass for adding Normalizers and Encoders to Serializer.
|
||||
$container->addCompilerPass(new RegisterSerializationClassesCompilerPass());
|
||||
// Add a compiler pass for adding concrete Resolvers to chain Resolver.
|
||||
$container->addCompilerPass(new RegisterEntityResolversCompilerPass());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\serialization\Tests;
|
||||
|
||||
@trigger_error(__NAMESPACE__ . '\NormalizerTestBase is deprecated for removal before Drupal 9.0.0. Use \Drupal\Tests\serialization\Kernel\NormalizerTestBase instead.', E_USER_DEPRECATED);
|
||||
|
||||
use Drupal\Tests\serialization\Kernel\NormalizerTestBase as SerializationNormalizerTestBase;
|
||||
|
||||
/**
|
||||
* Helper base class to set up some test fields for serialization testing.
|
||||
*
|
||||
* @deprecated in drupal:8.?.? and is removed from drupal:9.0.0.
|
||||
* Use \Drupal\Tests\serialization\Kernel\NormalizerTestBase instead.
|
||||
*/
|
||||
abstract class NormalizerTestBase extends SerializationNormalizerTestBase {}
|
||||
Reference in New Issue
Block a user