Commit 1c295885 authored by webchick's avatar webchick

Issue #1880424 by linclark, effulgentsia: Handle entity references on import.

parent 6fa6c772
......@@ -5,6 +5,7 @@ services:
- { name: normalizer, priority: 10 }
calls:
- [setLinkManager, ['@rest.link_manager']]
- [setEntityResolver, ['@serializer.entity_resolver']]
serializer.normalizer.field_item.hal:
class: Drupal\hal\Normalizer\FieldItemNormalizer
tags:
......
......@@ -80,7 +80,6 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
$typed_data_ids = $this->getTypedDataIds($data['_links']['type']);
$entity = entity_create($typed_data_ids['entity_type'], array('langcode' => $langcode, 'type' => $typed_data_ids['bundle']));
// @todo Handle data in _links and _embedded, http://drupal.org/node/1880424
// Get links and remove from data array.
$links = $data['_links'];
unset($data['_links']);
......@@ -91,6 +90,15 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
unset($data['_embedded']);
}
// Flatten the embedded values.
foreach ($embedded as $relation => $field) {
$field_ids = $this->linkManager->getRelationInternalIds($relation);
if (!empty($field_ids)) {
$field_name = $field_ids['field_name'];
$data[$field_name] = $field;
}
}
// Iterate through remaining items in data array. These should all
// correspond to fields.
foreach ($data as $field_name => $field_data) {
......@@ -166,5 +174,4 @@ protected function getTypedDataIds($types) {
return $typed_data_ids;
}
}
......@@ -7,10 +7,12 @@
namespace Drupal\hal\Normalizer;
use Drupal\serialization\EntityResolver\UuidReferenceInterface;
/**
* Converts the Drupal entity reference item object to HAL array structure.
*/
class EntityReferenceItemNormalizer extends FieldItemNormalizer {
class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidReferenceInterface {
/**
* The interface or class that this Normalizer supports.
......@@ -58,10 +60,29 @@ public function normalize($field_item, $format = NULL, array $context = array())
}
/**
* Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize()
* Overrides \Drupal\hal\Normalizer\FieldItemNormalizer::constructValue().
*/
public function denormalize($data, $class, $format = NULL, array $context = array()) {
// @todo Implement this in http://drupal.org/node/1880424
protected function constructValue($data, $context) {
$field_item = $context['target_instance'];
$field_definition = $field_item->getDefinition();
$target_type = $field_definition['settings']['target_type'];
if ($id = $this->entityResolver->resolve($this, $data, $target_type)) {
return array('target_id' => $id);
}
return NULL;
}
/**
* Implements \Drupal\serialization\EntityResolver\UuidReferenceInterface::getUuid().
*/
public function getUuid($data) {
if (isset($data['uuid'])) {
$uuid = $data['uuid'];
if (is_array($uuid)) {
$uuid = reset($uuid);
}
return $uuid;
}
}
}
......@@ -63,10 +63,25 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
}
}
$field_item->setValue($data);
$field_item->setValue($this->constructValue($data, $context));
return $field_item;
}
/**
* Build the field item value using the incoming data.
*
* @param $data
* The incoming data for this field item.
* @param $context
* The context passed into the Normalizer.
*
* @return mixed
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
return $data;
}
/**
* Get a translated version of the field item instance.
*
......
......@@ -7,6 +7,7 @@
namespace Drupal\hal\Normalizer;
use Drupal\serialization\EntityResolver\EntityResolverInterface;
use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
......@@ -22,6 +23,13 @@ abstract class NormalizerBase extends SerializationNormalizerBase implements Den
*/
protected $formats = array('hal_json');
/**
* The entity resolver.
*
* @var \Drupal\serialization\EntityResolver\EntityResolverInterface
*/
protected $entityResolver;
/**
* The hypermedia link manager.
*
......@@ -64,4 +72,15 @@ public function setLinkManager($link_manager) {
$this->linkManager = $link_manager;
}
/**
* Sets the entity resolver.
*
* The entity resolver is used to
*
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface $entity_resolver
*/
public function setEntityResolver(EntityResolverInterface $entity_resolver) {
$this->entityResolver = $entity_resolver;
}
}
......@@ -128,7 +128,7 @@ function setUp() {
new FieldItemNormalizer(),
new FieldNormalizer(),
);
$link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('cache')), new RelationLinkManager());
$link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('cache')), new RelationLinkManager(new MemoryBackend('cache')));
foreach ($normalizers as $normalizer) {
$normalizer->setLinkManager($link_manager);
}
......
......@@ -55,4 +55,11 @@ public function getTypeInternalIds($type_uri) {
public function getRelationUri($entity_type, $bundle, $field_name) {
return $this->relationLinkManager->getRelationUri($entity_type, $bundle, $field_name);
}
/**
* Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationInternalIds().
*/
public function getRelationInternalIds($relation_uri) {
return $this->relationLinkManager->getRelationInternalIds($relation_uri);
}
}
......@@ -7,24 +7,87 @@
namespace Drupal\rest\LinkManager;
use Drupal\Core\Cache\CacheBackendInterface;
class RelationLinkManager implements RelationLinkManagerInterface{
/**
* Get a relation link for the field.
*
* @param string $entity_type
* The bundle's entity type.
* @param string $bundle
* The name of the bundle.
* @param string $field_name
* The name of the field.
* @var \Drupal\Core\Cache\CacheBackendInterface;
*/
protected $cache;
/**
* Constructor.
*
* @return array
* The URI that identifies this field.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache of relation URIs and their associated Typed Data IDs.
*/
public function __construct(CacheBackendInterface $cache) {
$this->cache = $cache;
}
/**
* Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationUri().
*/
public function getRelationUri($entity_type, $bundle, $field_name) {
// @todo Make the base path configurable.
return url("rest/relation/$entity_type/$bundle/$field_name", array('absolute' => TRUE));
}
/**
* Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationInternalIds().
*/
public function getRelationInternalIds($relation_uri) {
$relations = $this->getRelations();
if (isset($relations[$relation_uri])) {
return $relations[$relation_uri];
}
return FALSE;
}
/**
* Get the array of relation links.
*
* Any field can be handled as a relation simply by changing how it is
* normalized. Therefore, there is no prior knowledge that can be used here
* to determine which fields to assign relation URIs. Instead, each field,
* even primitives, are given a relation URI. It is up to the caller to
* determine which URIs to use.
*
* @return array
* An array of typed data ids (entity_type, bundle, and field name) keyed
* by corresponding relation URI.
*/
public function getRelations() {
$cid = 'rest:links:relations';
$cache = $this->cache->get($cid);
if (!$cache) {
$this->writeCache();
$cache = $this->cache->get($cid);
}
return $cache->data;
}
/**
* Writes the cache of relation links.
*/
protected function writeCache() {
$data = array();
foreach (field_info_fields() as $field_info) {
foreach ($field_info['bundles'] as $entity_type => $bundles) {
foreach ($bundles as $bundle) {
$relation_uri = $this->getRelationUri($entity_type, $bundle, $field_info['field_name']);
$data[$relation_uri] = array(
'entity_type' => $entity_type,
'bundle' => $bundle,
'field_name' => $field_info['field_name'],
);
}
}
}
// These URIs only change when field info changes, so cache it permanently
// and only clear it when field_info is cleared.
$this->cache->set('rest:links:relations', $data, CacheBackendInterface::CACHE_PERMANENT, array('field_info' => TRUE));
}
}
......@@ -19,3 +19,4 @@ services:
arguments: ['@cache.cache']
rest.link_manager.relation:
class: Drupal\rest\LinkManager\RelationLinkManager
arguments: ['@cache.cache']
<?php
/**
* @file
* Contains \Drupal\serialization\EntityResolver\ChainEntityResolver
*/
namespace Drupal\serialization\EntityResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Resolver delegating the entity resolution to a chain of resolvers.
*/
class ChainEntityResolver implements EntityResolverInterface {
/**
* The concrete resolvers.
*
* @var array
*/
protected $resolvers;
/**
* Constructor.
*
* @param array $resolvers
* The array of concrete resolvers.
*/
public function __construct(array $resolvers = array()) {
$this->resolvers = $resolvers;
}
/**
* Implements \Drupal\serialization\EntityResolver\EntityResolverInterface::resolve().
*/
public function resolve(NormalizerInterface $normalizer, $data, $entity_type) {
foreach ($this->resolvers as $resolver) {
if ($resolved = $resolver->resolve($normalizer, $data, $entity_type)) {
return $resolved;
}
}
return NULL;
}
}
<?php
/**
* @file
* Contains \Drupal\serialization\EntityResolver\EntityResolverInterface
*/
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);
}
<?php
/**
* @file
* Contains \Drupal\serialization\EntityResolver\UuidReferenceInterface
*/
namespace Drupal\serialization\EntityResolver;
/**
* Interface for extracting UUID from entity reference data when denormalizing.
*/
interface UuidReferenceInterface {
/**
* 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);
}
<?php
/**
* @file
* Contains \Drupal\serialization\EntityResolver\UuidResolver
*/
namespace Drupal\serialization\EntityResolver;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Resolves entities from data that contains an entity UUID.
*/
class UuidResolver implements EntityResolverInterface {
/**
* Implements \Drupal\serialization\EntityResolver\EntityResolverInterface::resolve().
*/
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 = entity_load_by_uuid($entity_type, $uuid)) {
return $entity->id();
}
}
return NULL;
}
}
<?php
/**
* @file
* Contains \Drupal\serialization\RegisterEntityResolversCompilerPass.
*/
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');
// 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.
if (!empty($resolvers)) {
$definition->replaceArgument(0, $this->sort($resolvers));
}
}
/**
* 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 = array();
krsort($services);
// Flatten the array.
foreach ($services as $a) {
$sorted = array_merge($sorted, $a);
}
return $sorted;
}
}
......@@ -21,5 +21,7 @@ class SerializationBundle extends Bundle {
public function build(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());
}
}
<?php
/**
* @file
* Contains \Drupal\serialization\Tests\EntityResolverTest.
*/
namespace Drupal\serialization\Tests;
class EntityResolverTest extends NormalizerTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_reference', 'hal', 'rest');
/**
* The format being tested.
*
* @var string
*/
protected $format = 'hal_json';
public static function getInfo() {
return array(
'name' => 'Entity resolver tests',
'description' => 'Tests that entities references can be resolved.',
'group' => 'Serialization',
);
}
protected function setUp() {
parent::setUp();
// Create the test field.
$field = array(
'settings' => array(
'target_type' => 'entity_test_mulrev',
),
'field_name' => 'field_test_entity_reference',
'type' => 'entity_reference',
);
field_create_field($field);
// Create the test field instance.
$instance = array(
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_entity_reference',
'bundle' => 'entity_test_mulrev',
);
field_create_instance($instance);
}
/**
* Test that fields referencing UUIDs can be denormalized.
*/
function testUuidEntityResolver() {
// Create an entity to get the UUID from.
$entity = entity_create('entity_test_mulrev', array('type' => 'entity_test_mulrev'));
$entity->set('name', 'foobar');
$entity->set('field_test_entity_reference', array(array('target_id' => 1)));
$entity->save();
$field_uri = url('rest/relation/entity_test_mulrev/entity_test_mulrev/field_test_entity_reference', array('absolute' => TRUE));
$data = array(
'_links' => array(
'type' => array(
'href' => url('rest/type/entity_test_mulrev/entity_test_mulrev', array('absolute' => TRUE)),
),
$field_uri => array(
array(
'href' => url('entity/entity_test_mulrev/' . $entity->id()),
),
),
),
'_embedded' => array(
$field_uri => array(
array(
'_links' => array(
'self' => url('entity/entity_test_mulrev/' . $entity->id()),
),
'uuid' => array(
array(
'value' => $entity->uuid(),
),
),
),
),
),
);
$denormalized = $this->container->get('serializer')->denormalize($data, 'Drupal\entity_test\Plugin\Core\Entity\EntityTestMulRev', $this->format);
$field_value = $denormalized->get('field_test_entity_reference')->getValue();
$this->assertEqual($field_value[0]['target_id'], 1, 'Entity reference resolved using UUID.');
}
}
......@@ -16,14 +16,7 @@
/**
* Tests entity normalization and serialization of supported core formats.
*/
class EntitySerializationTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('serialization', 'system', 'entity', 'field', 'entity_test', 'text', 'field_sql_storage');
class EntitySerializationTest extends NormalizerTestBase {
/**
* The test values.
......@@ -57,27 +50,6 @@ public static function getInfo() {
protected function setUp() {
parent::setUp();
$this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_property_revision', 'entity_test_mulrev_property_data'));
// Auto-create a field for testing.
field_create_field(array(
'field_name' => 'field_test_text',
'type' => 'text',
'cardinality' => 1,
'translatable' => FALSE,
));
$instance = array(
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_text',
'bundle' => 'entity_test_mulrev',
'label' => 'Test text-field',
'widget' => array(
'type' => 'text_textfield',
'weight' => 0,
),
);
field_create_instance($instance);
// Create a test entity to serialize.
$this->values = array(
'name' => $this->randomName(),
......
<?php
/**
* @file
*
*/
namespace Drupal\serialization\Tests;
use Drupal\simpletest\DrupalUnitTestBase;
abstract class NormalizerTestBase extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('serialization', 'system', 'entity', 'field', 'entity_test', 'text', 'field_sql_storage');
protected function setUp() {
parent::setUp();
$this->installSchema('entity_test', array('entity_test_mulrev', 'entity_test_mulrev_property_revision', 'entity_test_mulrev_property_data'));
$this->installSchema('system', array('url_alias'));
// Auto-create a field for testing.
field_create_field(array(
'field_name' => 'field_test_text',
'type' => 'text',
'cardinality' => 1,
'translatable' => FALSE,
));
$instance = array(
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_text',
'bundle' => 'entity_test_mulrev',
'label' => 'Test text-field',
'widget' => array(
'type' => 'text_textfield',
'weight' => 0,
),
);
field_create_instance($instance);
}
}