diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 169c785..13e5b52 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -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 - ? diff --git a/core/modules/hal/hal.info.yml b/core/modules/hal/hal.info.yml new file mode 100644 index 0000000..ac48857 --- /dev/null +++ b/core/modules/hal/hal.info.yml @@ -0,0 +1,7 @@ +name: 'HAL (Hypertext Application Language)' +description: 'Serializes entities using HAL' +package: Core +core: 8.x +dependencies: + - rest + - serialization \ No newline at end of file diff --git a/core/modules/hal/hal.module b/core/modules/hal/hal.module new file mode 100644 index 0000000..72e9b86 --- /dev/null +++ b/core/modules/hal/hal.module @@ -0,0 +1,6 @@ +format; + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/HalBundle.php b/core/modules/hal/lib/Drupal/hal/HalBundle.php new file mode 100644 index 0000000..bb1e2dc --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/HalBundle.php @@ -0,0 +1,49 @@ +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'); + } +} diff --git a/core/modules/hal/lib/Drupal/hal/HalSubscriber.php b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php new file mode 100644 index 0000000..93a70bb --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/HalSubscriber.php @@ -0,0 +1,41 @@ +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; + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php new file mode 100644 index 0000000..0613d2a --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php @@ -0,0 +1,78 @@ + 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)); + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php new file mode 100644 index 0000000..b56c2bb --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php @@ -0,0 +1,60 @@ +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), + ), + ); + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php new file mode 100644 index 0000000..b06a90f --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldItemNormalizer.php @@ -0,0 +1,41 @@ +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), + ); + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php new file mode 100644 index 0000000..d088bca --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php @@ -0,0 +1,79 @@ +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; + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php new file mode 100644 index 0000000..d159f1f --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php @@ -0,0 +1,50 @@ +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; + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php b/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php new file mode 100644 index 0000000..848b19a --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php @@ -0,0 +1,179 @@ + '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)); + } + +} diff --git a/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php new file mode 100644 index 0000000..224586d --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php @@ -0,0 +1,89 @@ +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); + } + +} diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php new file mode 100644 index 0000000..3f5b947 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php @@ -0,0 +1,51 @@ +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); + } +} diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php new file mode 100644 index 0000000..60e5629 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php @@ -0,0 +1,23 @@ + TRUE)); + } + +} diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/RelationLinkManagerInterface.php b/core/modules/rest/lib/Drupal/rest/LinkManager/RelationLinkManagerInterface.php new file mode 100644 index 0000000..bd28432 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/RelationLinkManagerInterface.php @@ -0,0 +1,26 @@ + TRUE)); + } + +} diff --git a/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php new file mode 100644 index 0000000..ba4dc3c --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/TypeLinkManagerInterface.php @@ -0,0 +1,28 @@ +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'); } }