Issue #1924220 by linclark: Support serialization in hal+json.

8.0.x
Dries 2013-03-13 13:09:05 -04:00
parent 11632e296b
commit b59ad36f0b
20 changed files with 907 additions and 0 deletions

View File

@ -237,6 +237,9 @@ Filter module
Forum module
- Lee Rowlands 'larowlan' http://drupal.org/user/395439
Hypertext Application Language (HAL) module
- Lin Clark 'linclark' http://drupal.org/user/396253
Help module
- ?

View File

@ -0,0 +1,7 @@
name: 'HAL (Hypertext Application Language)'
description: 'Serializes entities using HAL'
package: Core
core: 8.x
dependencies:
- rest
- serialization

View File

@ -0,0 +1,6 @@
<?php
/**
* @file
* Drupal-required module file for HAL module.
*/

View File

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\hal\JsonEncoder.
*/
namespace Drupal\hal\Encoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder as SymfonyJsonEncoder;
/**
* Encodes HAL data in JSON.
*
* Simply respond to application/hal+json requests using the JSON encoder.
*/
class JsonEncoder extends SymfonyJsonEncoder {
/**
* The formats that this Encoder supports.
*
* @var string
*/
protected $format = 'hal_json';
/**
* Overrides \Symfony\Component\Serializer\Encoder\JsonEncoder::supportsEncoding()
*/
public function supportsEncoding($format) {
return $format == $this->format;
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* @file
* Contains \Drupal\hal\HalBundle.
*/
namespace Drupal\hal;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* HAL dependency injection container.
*/
class HalBundle extends Bundle {
/**
* Overrides \Symfony\Component\HttpKernel\Bundle\Bundle::build().
*/
public function build(ContainerBuilder $container) {
$priority = 10;
$container->register('serializer.normalizer.entity_reference_item.hal', 'Drupal\hal\Normalizer\EntityReferenceItemNormalizer')
->addMethodCall('setLinkManager', array(new Reference('rest.link_manager')))
->addTag('normalizer', array('priority' => $priority));
$container->register('serializer.normalizer.field_item.hal', 'Drupal\hal\Normalizer\FieldItemNormalizer')
->addMethodCall('setLinkManager', array(new Reference('rest.link_manager')))
->addTag('normalizer', array('priority' => $priority));
$container->register('serializer.normalizer.field.hal', 'Drupal\hal\Normalizer\FieldNormalizer')
->addMethodCall('setLinkManager', array(new Reference('rest.link_manager')))
->addTag('normalizer', array('priority' => $priority));
$container->register('serializer.normalizer.entity.hal', 'Drupal\hal\Normalizer\EntityNormalizer')
->addMethodCall('setLinkManager', array(new Reference('rest.link_manager')))
->addTag('normalizer', array('priority' => $priority));
$container->register('serializer.encoder.hal', 'Drupal\hal\Encoder\JsonEncoder')
->addTag('encoder', array(
'priority' => $priority,
'format' => array(
'hal_json' => 'HAL (JSON)',
),
));
$container->register('hal.subscriber', 'Drupal\hal\HalSubscriber')
->addTag('event_subscriber');
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\hal\HalSubscriber.
*/
namespace Drupal\hal;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Subscribes to the kernel request event to add HAL media types.
*/
class HalSubscriber implements EventSubscriberInterface {
/**
* Registers HAL formats with the Request class.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The event to process.
*/
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
$request->setFormat('hal_json', 'application/hal+json');
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequest', 40);
return $events;
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* @file
* Contains \Drupal\hal\Normalizer\EntityNormalizer.
*/
namespace Drupal\hal\Normalizer;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityNG;
/**
* Converts the Drupal entity object structure to a HAL array structure.
*/
class EntityNormalizer extends NormalizerBase {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\EntityInterface';
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
public function normalize($entity, $format = NULL, array $context = array()) {
// Create the array of normalized properties, starting with the URI.
$normalized = array(
'_links' => array(
'self' => array(
'href' => $this->getEntityUri($entity),
),
'type' => array(
'href' => $this->linkManager->getTypeUri($entity->entityType(), $entity->bundle()),
),
),
);
// If the properties to use were specified, only output those properties.
// Otherwise, output all properties except internal ID.
if (isset($context['included_fields'])) {
foreach ($context['included_fields'] as $property_name) {
$properties[] = $entity->get($property_name);
}
}
else {
$properties = $entity->getProperties();
}
foreach ($properties as $property) {
// In some cases, Entity API will return NULL array items. Ensure this is
// a real property and that it is not the internal id.
if (!is_object($property) || $property->getName() == 'id') {
continue;
}
$normalized_property = $this->serializer->normalize($property, $format, $context);
$normalized = NestedArray::mergeDeep($normalized, $normalized_property);
}
return $normalized;
}
/**
* Constructs the entity URI.
*
* @param $entity
* The entity.
*
* @return string
* The entity URI.
*/
protected function getEntityUri($entity) {
$uri_info = $entity->uri();
return url($uri_info['path'], array('absolute' => TRUE));
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\hal\Normalizer\EntityReferenceItemNormalizer.
*/
namespace Drupal\hal\Normalizer;
/**
* Converts the Drupal entity reference item object to HAL array structure.
*/
class EntityReferenceItemNormalizer extends FieldItemNormalizer {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\Type\EntityReferenceItem';
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
public function normalize($field_item, $format = NULL, array $context = array()) {
$target_entity = $field_item->get('entity')->getValue();
// If the parent entity passed in a langcode, unset it before normalizing
// the target entity. Otherwise, untranslatable fields of the target entity
// will include the langcode.
$langcode = isset($context['langcode']) ? $context['langcode'] : NULL;
unset($context['langcode']);
$context['included_fields'] = array('uuid');
// Normalize the target entity.
$embedded = $this->serializer->normalize($target_entity, $format, $context);
$link = $embedded['_links']['self'];
// If the field is translatable, add the langcode to the link relation
// object. This does not indicate the language of the target entity.
if ($langcode) {
$embedded['lang'] = $link['lang'] = $langcode;
}
// The returned structure will be recursively merged into the normalized
// entity so that the items are properly added to the _links and _embedded
// objects.
$field_name = $field_item->getParent()->getName();
$entity = $field_item->getRoot();
$field_uri = $this->linkManager->getRelationUri($entity->entityType(), $entity->bundle(), $field_name);
return array(
'_links' => array(
$field_uri => array($link),
),
'_embedded' => array(
$field_uri => array($embedded),
),
);
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\hal\Normalizer\FieldItemNormalizer.
*/
namespace Drupal\hal\Normalizer;
/**
* Converts the Drupal field item object structure to HAL array structure.
*/
class FieldItemNormalizer extends NormalizerBase {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\FieldItemInterface';
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
public function normalize($field_item, $format = NULL, array $context = array()) {
$values = $field_item->getPropertyValues();
if (isset($context['langcode'])) {
$values['lang'] = $context['langcode'];
}
// The values are wrapped in an array, and then wrapped in another array
// keyed by field name so that field items can be merged by the
// FieldNormalizer. This is necessary for the EntityReferenceItemNormalizer
// to be able to place values in the '_links' array.
$field = $field_item->getParent();
return array(
$field->getName() => array($values),
);
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\hal\Normalizer\FieldNormalizer.
*/
namespace Drupal\hal\Normalizer;
use Drupal\Component\Utility\NestedArray;
/**
* Converts the Drupal field structure to HAL array structure.
*/
class FieldNormalizer extends NormalizerBase {
/**
* The interface or class that this Normalizer supports.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\Field\FieldInterface';
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
*/
public function normalize($field, $format = NULL, array $context = array()) {
$normalized_field_items = array();
$entity = $field->getParent();
$field_name = $field->getName();
$field_definition = $entity->getPropertyDefinition($field_name);
// If this field is not translatable, it can simply be normalized without
// separating it into different translations.
if (empty($field_definition['translatable'])) {
$normalized_field_items = $this->normalizeFieldItems($field, $format, $context);
}
// Otherwise, the languages have to be extracted from the entity and passed
// in to the field item normalizer in the context. The langcode is appended
// to the field item values.
else {
foreach ($entity->getTranslationLanguages() as $lang) {
$context['langcode'] = $lang->langcode == 'und' ? LANGUAGE_DEFAULT : $lang->langcode;
$translation = $entity->getTranslation($lang->langcode);
$translated_field = $translation->get($field_name);
$normalized_field_items = array_merge($normalized_field_items, $this->normalizeFieldItems($translated_field, $format, $context));
}
}
// Merge deep so that links set in entity reference normalizers are merged
// into the links property.
$normalized = NestedArray::mergeDeepArray($normalized_field_items);
return $normalized;
}
/**
* Helper function to normalize field items.
*
* @param \Drupal\Core\Entity\Field\FieldInterface $field
* The field object.
* @param string $format
* The format.
* @param array $context
* The context array.
*
* @return array
* The array of normalized field items.
*/
protected function normalizeFieldItems($field, $format, $context) {
$normalized_field_items = array();
if (!$field->isEmpty()) {
foreach ($field as $field_item) {
$normalized_field_items[] = $this->serializer->normalize($field_item, $format, $context);
}
}
return $normalized_field_items;
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* @file
* Contains \Drupal\hal\Normalizer\NormalizerBase.
*/
namespace Drupal\hal\Normalizer;
use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase;
/**
* Base class for Normalizers.
*/
abstract class NormalizerBase extends SerializationNormalizerBase {
/**
* The formats that the Normalizer can handle.
*
* @var array
*/
protected $formats = array('hal_json');
/**
* The hypermedia link manager.
*
* @var \Drupal\rest\LinkManager\LinkManager
*/
protected $linkManager;
/**
* Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::supportsNormalization().
*/
public function supportsNormalization($data, $format = NULL) {
return in_array($format, $this->formats) && parent::supportsNormalization($data, $format);
}
/**
* Sets the link manager.
*
* The link manager determines the hypermedia type and relation links which
* correspond to different bundles and fields.
*
* @param \Drupal\rest\LinkManager\LinkManager $link_manager
*/
public function setLinkManager($link_manager) {
$this->linkManager = $link_manager;
}
}

View File

@ -0,0 +1,179 @@
<?php
/**
* @file
* Contains \Drupal\hal\Tests\NormalizeTest.
*/
namespace Drupal\hal\Tests;
/**
* Test the HAL normalizer.
*/
class NormalizeTest extends NormalizerTestBase {
public static function getInfo() {
return array(
'name' => 'Normalize Test',
'description' => 'Test that entities can be normalized in HAL.',
'group' => 'HAL',
);
}
/**
* Tests the normalize function.
*/
public function testNormalize() {
$target_entity_de = entity_create('entity_test', (array('langcode' => 'de', 'field_test_entity_reference' => NULL)));
$target_entity_de->save();
$target_entity_en = entity_create('entity_test', (array('langcode' => 'en', 'field_test_entity_reference' => NULL)));
$target_entity_en->save();
// Create a German entity.
$values = array(
'langcode' => 'de',
'name' => $this->randomName(),
'user_id' => 1,
'field_test_text' => array(
'value' => $this->randomName(),
'format' => 'full_html',
),
'field_test_entity_reference' => array(
'target_id' => $target_entity_de->id(),
),
);
// Array of translated values.
$translation_values = array(
'name' => $this->randomName(),
'field_test_entity_reference' => array(
'target_id' => $target_entity_en->id(),
)
);
$entity = entity_create('entity_test', $values);
$entity->save();
// Add an English value for name and entity reference properties.
$entity->getTranslation('en')->set('name', array(0 => array('value' => $translation_values['name'])));
$entity->getTranslation('en')->set('field_test_entity_reference', array(0 => $translation_values['field_test_entity_reference']));
$entity->save();
$type_uri = url('rest/type/entity_test/entity_test', array('absolute' => TRUE));
$relation_uri = url('rest/relation/entity_test/entity_test/field_test_entity_reference', array('absolute' => TRUE));
$expected_array = array(
'_links' => array(
'curies' => array(
array(
'href' => '/relations',
'name' => 'site',
'templated' => true,
),
),
'self' => array(
'href' => $this->getEntityUri($entity),
),
'type' => array(
'href' => $type_uri,
),
$relation_uri => array(
array(
'href' => $this->getEntityUri($target_entity_de),
'lang' => 'de',
),
array(
'href' => $this->getEntityUri($target_entity_en),
'lang' => 'en',
),
),
),
'_embedded' => array(
$relation_uri => array(
array(
'_links' => array(
'self' => array(
'href' => $this->getEntityUri($target_entity_de),
),
'type' => array(
'href' => $type_uri,
),
),
'uuid' => array(
array(
'value' => $target_entity_de->uuid(),
),
),
'lang' => 'de',
),
array(
'_links' => array(
'self' => array(
'href' => $this->getEntityUri($target_entity_en),
),
'type' => array(
'href' => $type_uri,
),
),
'uuid' => array(
array(
'value' => $target_entity_en->uuid(),
),
),
'lang' => 'en',
),
),
),
'uuid' => array(
array(
'value' => $entity->uuid(),
),
),
'langcode' => array(
array(
'value' => 'de',
),
),
'name' => array(
array(
'value' => $values['name'],
'lang' => 'de',
),
array(
'value' => $translation_values['name'],
'lang' => 'en',
),
),
'field_test_text' => array(
array(
'value' => $values['field_test_text']['value'],
'format' => $values['field_test_text']['format'],
),
),
);
$normalized = $this->container->get('serializer')->normalize($entity, $this->format);
$this->assertEqual($normalized['_links']['self'], $expected_array['_links']['self'], 'self link placed correctly.');
// @todo Test curies.
// @todo Test type.
$this->assertFalse(isset($normalized['id']), 'Internal id is not exposed.');
$this->assertEqual($normalized['uuid'], $expected_array['uuid'], 'Non-translatable fields is normalized.');
$this->assertEqual($normalized['name'], $expected_array['name'], 'Translatable field with multiple language values is normalized.');
$this->assertEqual($normalized['field_test_text'], $expected_array['field_test_text'], 'Field with properties is normalized.');
$this->assertEqual($normalized['_embedded'][$relation_uri], $expected_array['_embedded'][$relation_uri], 'Entity reference field is normalized.');
$this->assertEqual($normalized['_links'][$relation_uri], $expected_array['_links'][$relation_uri], 'Links are added for entity reference field.');
}
/**
* Constructs the entity URI.
*
* @param $entity
* The entity.
*
* @return string
* The entity URI.
*/
protected function getEntityUri($entity) {
$entity_uri_info = $entity->uri();
return url($entity_uri_info['path'], array('absolute' => TRUE));
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\hal\Tests\NormalizerTestBase.
*/
namespace Drupal\hal\Tests;
use Drupal\Core\Language\Language;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Test the HAL normalizer.
*/
abstract class NormalizerTestBase extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test', 'entity_reference', 'field', 'field_sql_storage', 'hal', 'language', 'rest', 'serialization', 'system', 'text', 'user');
/**
* The format being tested.
*
* @var string
*/
protected $format = 'hal_json';
/**
* Overrides \Drupal\simpletest\DrupalUnitTestBase::setup().
*/
function setUp() {
parent::setUp();
$this->installSchema('system', array('variable', 'url_alias'));
$this->installSchema('field', array('field_config', 'field_config_instance'));
$this->installSchema('user', array('users'));
$this->installSchema('language', array('language'));
$this->installSchema('entity_test', array('entity_test'));
// Add English as a language.
$english = new Language(array(
'langcode' => 'en',
'name' => 'English',
));
language_save($english);
// Add German as a language.
$german = new Language(array(
'langcode' => 'de',
'name' => 'Deutsch',
));
language_save($german);
// Create the test text field.
$field = array(
'field_name' => 'field_test_text',
'type' => 'text',
'cardinality' => 1,
'translatable' => FALSE,
);
field_create_field($field);
$instance = array(
'entity_type' => 'entity_test',
'field_name' => 'field_test_text',
'bundle' => 'entity_test',
);
field_create_instance($instance);
// Create the test entity reference field.
$field = array(
'translatable' => TRUE,
'settings' => array(
'target_type' => 'entity_test',
),
'field_name' => 'field_test_entity_reference',
'type' => 'entity_reference',
);
field_create_field($field);
$instance = array(
'entity_type' => 'entity_test',
'field_name' => 'field_test_entity_reference',
'bundle' => 'entity_test',
);
field_create_instance($instance);
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\rest\LinkManager\LinkManager.
*/
namespace Drupal\rest\LinkManager;
class LinkManager implements LinkManagerInterface {
/**
* The type link manager.
*
* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface
*/
protected $typeLinkManager;
/**
* The relation link manager.
*
* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface
*/
protected $relationLinkManager;
/**
* Constructor.
*
* @param \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_link_manager
* Manager for handling bundle URIs.
* @param \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_link_manager
* Manager for handling bundle URIs.
*/
public function __construct(TypeLinkManagerInterface $type_link_manager, RelationLinkManagerInterface $relation_link_manager) {
$this->typeLinkManager = $type_link_manager;
$this->relationLinkManager = $relation_link_manager;
}
/**
* Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeUri().
*/
public function getTypeUri($entity_type, $bundle) {
return $this->typeLinkManager->getTypeUri($entity_type, $bundle);
}
/**
* Implements \Drupal\rest\LinkManager\RelationLinkManagerInterface::getRelationUri().
*/
public function getRelationUri($entity_type, $bundle, $field_name) {
return $this->relationLinkManager->getRelationUri($entity_type, $bundle, $field_name);
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* @file
* Contains \Drupal\rest\LinkManager\LinkManagerInterface
*/
namespace Drupal\rest\LinkManager;
/**
* Interface implemented by link managers.
*
* There are no explicit methods on the manager interface. Instead link managers
* broker the interactions of the different components, and therefore must
* implement each component interface, which is enforced by this interface
* extending all of the component ones.
*
* While a link manager may directly implement these interface methods with
* custom logic, it is expected to be more common for plugin managers to proxy
* the method invocations to the respective components.
*/
interface LinkManagerInterface extends TypeLinkManagerInterface, RelationLinkManagerInterface {
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\rest\LinkManager\RelationLinkManager.
*/
namespace Drupal\rest\LinkManager;
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.
*
* @return array
* The URI that identifies this field.
*/
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));
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @file
* Contains \Drupal\rest\LinkManager\RelationLinkManagerInterface.
*/
namespace Drupal\rest\LinkManager;
interface RelationLinkManagerInterface {
/**
* Gets the URI that corresponds to a field.
*
* @param string $entity_type
* The bundle's entity type.
* @param string $bundle
* The bundle name.
* @param string $field_name
* The field name.
*
* @return string
* The corresponding URI for the field.
*/
public function getRelationUri($entity_type, $bundle, $field_name);
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\rest\LinkManager\TypeLinkManager.
*/
namespace Drupal\rest\LinkManager;
class TypeLinkManager implements TypeLinkManagerInterface {
/**
* Get a type link for a bundle.
*
* @param string $entity_type
* The bundle's entity type.
* @param string $bundle
* The name of the bundle.
*
* @return array
* The URI that identifies this bundle.
*/
public function getTypeUri($entity_type, $bundle) {
// @todo Make the base path configurable.
return url("rest/type/$entity_type/$bundle", array('absolute' => TRUE));
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @file
* Contains \Drupal\rest\LinkManager\TypeLinkManagerInterface.
*/
namespace Drupal\rest\LinkManager;
interface TypeLinkManagerInterface {
/**
* Gets the URI that corresponds to a bundle.
*
* When using hypermedia formats, this URI can be used to indicate which
* bundle the data represents. Documentation about required and optional
* fields can also be provided at this URI.
*
* @param $entity_type
* The bundle's entity type.
* @param $bundle
* The bundle name.
*
* @return string
* The corresponding URI for the bundle.
*/
public function getTypeUri($entity_type, $bundle);
}

View File

@ -32,5 +32,11 @@ class RestBundle extends Bundle {
$container->register('access_check.rest.csrf', 'Drupal\rest\Access\CSRFAccessCheck')
->addTag('access_check');
$container->register('rest.link_manager', 'Drupal\rest\LinkManager\LinkManager')
->addArgument(new Reference('rest.link_manager.type'))
->addArgument(new Reference('rest.link_manager.relation'));
$container->register('rest.link_manager.type', 'Drupal\rest\LinkManager\TypeLinkManager');
$container->register('rest.link_manager.relation', 'Drupal\rest\LinkManager\RelationLinkManager');
}
}