diff --git a/core/modules/hal/hal.info b/core/modules/hal/hal.info new file mode 100644 index 0000000..ec43ea8 --- /dev/null +++ b/core/modules/hal/hal.info @@ -0,0 +1,6 @@ +name = HAL (Hypertext Application Language) +description = Serializes entities using HAL. +package = Core +core = 8.x +dependencies[] = rest +dependencies[] = serialization diff --git a/core/modules/hal/hal.module b/core/modules/hal/hal.module new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/core/modules/hal/hal.module @@ -0,0 +1 @@ +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..d782512 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/HalBundle.php @@ -0,0 +1,46 @@ +register('serializer.normalizer.entity_reference_item.hal', 'Drupal\hal\Normalizer\EntityReferenceItemNormalizer') + ->addTag('normalizer', array('priority' => $priority)); + $container->register('serializer.normalizer.field_item.hal', 'Drupal\hal\Normalizer\FieldItemNormalizer') + ->addTag('normalizer', array('priority' => $priority)); + $container->register('serializer.normalizer.field.hal', 'Drupal\hal\Normalizer\FieldNormalizer') + ->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..a1a6d51 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php @@ -0,0 +1,103 @@ + 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) { + if ($property->getName() == 'id') { + continue; + } + $normalized_property = $this->serializer->normalize($property, $format, $context); + $normalized = NestedArray::mergeDeep($normalized, $normalized_property); + } + + // Only add the curies array if there are curies in the link relations. + $link_relations = array_keys($normalized['_links']); + if (isset($normalized['_embedded'])) { + $link_relations = array_merge($link_relations, array_keys($normalized['_embedded'])); + } + foreach ($link_relations as $link_relation) { + if (strpos($link_relation, ':') == TRUE) { + $normalized['_links']['curies'][] = array( + // @todo Make this configurable. + 'href' => url('relations') . '/{rel}', + 'name' => 'site', + 'templated' => TRUE, + ); + break; + } + } + + return $normalized; + } + + /** + * Constructs the entity URI. + * + * @param $entity + * The entity. + * + * @return string + * The entity URI. + */ + protected function getEntityUri($entity) { + // @todo Remove this conditional once entities are converted to EntityNG. + if ($entity instanceof EntityNG) { + $uri_info = $entity->uri(); + return url($uri_info['path']); + } + } + +} 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..46170b5 --- /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(); + // @todo Introduce a RelationLinkManager to get the CURIE. + $field_curie = "site:$field_name"; + return array( + '_links' => array( + $field_curie => array($link), + ), + '_embedded' => array( + $field_curie => 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..4be97f7 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php @@ -0,0 +1,43 @@ +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..c87941e --- /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(); + + $expected_array = array( + '_links' => array( + 'curies' => array( + array( + 'href' => '/relations', + 'name' => 'site', + 'templated' => true, + ), + ), + 'self' => array( + 'href' => $this->getUri($entity), + ), + 'type' => array( + 'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)), + ), + 'site:user_id' => array( + array( + 'href' => NULL, + 'lang' => 'de', + ), + ), + 'site:field_test_entity_reference' => array( + array( + 'href' => $this->getUri($target_entity_de), + 'lang' => 'de', + ), + array( + 'href' => $this->getUri($target_entity_en), + 'lang' => 'en', + ), + ), + ), + '_embedded' => array( + 'site:user_id' => array( + array( + 'href' => NULL, + 'lang' => 'de', + ), + ), + 'site:field_test_entity_reference' => array( + array( + '_links' => array( + 'self' => array( + 'href' => $this->getUri($target_entity_de), + ), + 'type' => array( + 'href' =>url('rest/types/entity_test/entity_test', array('absolute' => TRUE)), + ), + ), + 'uuid' => array( + array( + 'value' => $target_entity_de->uuid(), + ), + ), + 'lang' => 'de', + ), + array( + '_links' => array( + 'self' => array( + 'href' => $this->getUri($target_entity_en), + ), + 'type' => array( + 'href' => url('rest/types/entity_test/entity_test', array('absolute' => TRUE)), + ), + ), + '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']['site:field_test_entity_reference'], $expected_array['_embedded']['site:field_test_entity_reference'], 'Entity reference field is normalized.'); + $this->assertEqual($normalized['_links']['site:field_test_entity_reference'], $expected_array['_links']['site:field_test_entity_reference'], 'Links are added for entity reference field.'); + } + + protected function getUri($entity) { + $entity_uri_info = $entity->uri(); + return url($entity_uri_info['path']); + } + +} 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..b577f20 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizerTestBase.php @@ -0,0 +1,91 @@ +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..7f739c6 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManager.php @@ -0,0 +1,33 @@ +typeLinkManager = $type_link_manager; + } + + /** + * Implements \Drupal\rest\LinkManager\TypeLinkManagerInterface::getTypeUri(). + */ + public function getTypeUri($entity_type, $bundle) { + return $this->typeLinkManager->getTypeUri($entity_type, $bundle); + } +} 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..30f5d25 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/LinkManager/LinkManagerInterface.php @@ -0,0 +1,23 @@ +cache = $cache; + } + + /** + * 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/types/$entity_type/$bundle", array('absolute' => TRUE)); + } + + /** + * Get the array of type links. + * + * @return array + * An array of typed data ids (entity_type and bundle) keyed by + * corresponding type URI. + */ + public function getTypes() { + $cid = 'rest:links:types'; + $cache = $this->cache->get($cid); + if (!$cache) { + $this->writeCache(); + $cache = $this->cache->get($cid); + } + return $cache->data; + } + + /** + * Writes the cache of type links. + */ + protected function writeCache() { + $data = array(); + + // Type URIs correspond to bundles. Iterate through the bundles to get the + // URI and data for them. + $entity_info = entity_get_info(); + foreach (entity_get_bundles() as $entity_type => $bundles) { + $entity_type_info = $entity_info[$entity_type]; + $reflection = new \ReflectionClass($entity_type_info['class']); + // Only content entities are supported currently. + // @todo Consider supporting config entities. + if ($reflection->implementsInterface('\Drupal\Core\Config\Entity\ConfigEntityInterface')) { + continue; + } + foreach ($bundles as $bundle => $bundle_info) { + // Get a type URI for the bundle. + $bundle_uri = $this->getTypeUri($entity_type, $bundle); + $data[$bundle_uri] = array( + 'entity_type' => $entity_type, + 'bundle' => $bundle, + ); + } + } + // These URIs only change when entity info changes, so cache it permanently + // and only clear it when entity_info is cleared. + $this->cache->set('rest:links:types', $data, CacheBackendInterface::CACHE_PERMANENT, array('entity_info' => 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')); + $container->register('rest.link_manager.type', 'Drupal\rest\LinkManager\TypeLinkManager') + ->addArgument(new Reference('cache.cache')); } }