diff --git a/core/modules/hal/hal.info b/core/modules/hal/hal.info new file mode 100644 index 0000000..17c302e --- /dev/null +++ b/core/modules/hal/hal.info @@ -0,0 +1,5 @@ +name = HAL (Hypertext Application Language) +description = Serializes entities using HAL. +package = Core +core = 8.x +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..ad817dd --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/HalBundle.php @@ -0,0 +1,44 @@ +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') + ->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..666e42e --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityNormalizer.php @@ -0,0 +1,60 @@ + array( + 'curies' => array( + array( + // @todo Make this configurable. + 'href' => url('relations') . '/{rel}', + 'name' => 'site', + 'templated' => TRUE, + ), + ), + 'self' => array( + 'href' => $entity_wrapper->getUri(), + ), + 'type' => array( + 'href' => $entity_wrapper->getTypeUri(), + ), + ), + ); + + $properties = $entity->getProperties(); + foreach ($properties as $property) { + $normalized_property = $this->serializer->normalize($property, $format, $context); + $normalized = NestedArray::mergeDeep($normalized, $normalized_property); + } + + return $normalized; + } + +} 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..9b35dc2 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityReferenceItemNormalizer.php @@ -0,0 +1,62 @@ +get('entity')->getValue(); + $entity_wrapper = new EntityWrapper($target_entity); + + // Set up the values for _link and _embedded and add the language if one + // was passed in. + $target_entity_uri = $entity_wrapper->getUri(); + $link = array( + 'href' => $target_entity_uri, + ); + $target_entity_uuid = $entity_wrapper->getUuid(); + $embedded = array( + '_links' => array( + 'self' => $target_entity_uri, + ), + 'uuid' => $target_entity_uuid, + ); + if (isset($context['langcode'])) { + $embedded['lang'] = $link['lang'] = $context['langcode']; + } + + // This 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(); + $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/EntityWrapper.php b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityWrapper.php new file mode 100644 index 0000000..841b443 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/EntityWrapper.php @@ -0,0 +1,58 @@ +entity = $entity; + } + + public function getUri() { + // @todo Remove this conditional once entities are converted to EntityNG. + if ($this->entity instanceof EntityNG) { + $uri_info = $this->entity->uri(); + return url($uri_info['path']); + } + } + + public function getTypeUri() { + $entity_type = $this->entity->entityType(); + $bundle = $this->entity->bundle(); + return url("types/$entity_type/$bundle"); + } + + public function getUuid() { + // @todo Remove this conditional once entities are converted to EntityNG. + if ($this->entity instanceof EntityNG) { + return $this->entity->uuid(); + } + } + +} 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..e6e3777 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/FieldNormalizer.php @@ -0,0 +1,66 @@ +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->normalizeField($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->normalizeField($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; + } + + protected function normalizeField($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..8a42ff3 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Normalizer/NormalizerBase.php @@ -0,0 +1,31 @@ +formats) && parent::supportsNormalization($data, $format); + } + +} 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..17cf2a4 --- /dev/null +++ b/core/modules/hal/lib/Drupal/hal/Tests/NormalizeTest.php @@ -0,0 +1,161 @@ + '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' => '/types/entity_test/entity_test', + ), + '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' => $this->getUri($target_entity_de), + ), + 'uuid' => $target_entity_de->uuid(), + 'lang' => 'de', + ), + array( + '_links' => array( + 'self' => $this->getUri($target_entity_en), + ), + 'uuid' => $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($expected_array['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..8d1e8e1 --- /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); + } + +}