diff --git a/external_entities.services.yml b/external_entities.services.yml index 8f790cd2d49c7e94f6b2a38b02f810ca22236e93..32c4bd44b6aaf030b1e22c997e58b7e7606b8ad6 100644 --- a/external_entities.services.yml +++ b/external_entities.services.yml @@ -34,3 +34,7 @@ services: arguments: ['@entity_type.manager'] tags: - { name: route_processor_outbound } + serialization.external_entities_api_json: + class: \Drupal\external_entities\Serialization\JsonApi + tags: + - { name: external_entity_response_decoder } diff --git a/src/Plugin/ExternalEntities/StorageClient/JsonApi.php b/src/Plugin/ExternalEntities/StorageClient/JsonApi.php index 23a3376e876d346042fc15737bf8cd828dcddc6e..16b3402e46383a013d64755649c60b3481f32a8f 100644 --- a/src/Plugin/ExternalEntities/StorageClient/JsonApi.php +++ b/src/Plugin/ExternalEntities/StorageClient/JsonApi.php @@ -67,7 +67,7 @@ class JsonApi extends RestClient { ], 'response_format' => [ '#type' => 'hidden', - '#default_value' => 'json', + '#default_value' => 'external_entities_api_json', ], 'data_path' => [ '#type' => NULL, @@ -90,7 +90,7 @@ class JsonApi extends RestClient { ], 'pager' => [ '#type' => NULL, - // Drupal uses a 50 elements per bage basis so we go for 1 query per + // Drupal uses a 50 elements per page basis so we go for 1 query per // page. 50 is the JSON:API default limit anyway. 'default_limit' => [ '#type' => 'hidden', @@ -347,16 +347,49 @@ class JsonApi extends RestClient { $result = parent::load($id); - // Check for "included". - if (!empty($result[1])) { - // JSONPath returned an array of object so we got "extra" from "included". - $included = array_slice($result, 1); - $result = $result[0]; - $result['included'] = $included; - } static::$cachedData[$this->configuration['endpoint']][$id] = $result; return $result; } + protected function resolveRelationships(&$result): void + { + // Collect all includes and index them by type and id. + $includedRegistry = []; + foreach ($result['included'] as $included_entry) { + $included_id = ($included_entry['type'] ?? NULL) . ':' . $included_entry['id']; + $includedRegistry[$included_id] = $included_entry; + } + $this->_resolveRelationsRecursive($result['relationships'], $includedRegistry); + } + + protected function _resolveRelationsRecursive(&$relationships, $includedRegistry): void + { + foreach ($relationships as $field => $relationship) { + if (!empty($relationship['data'])) { + // Relationships can be single or multi-value. + if (isset($relationship['data']['id'])) { + $this->_mapRelationshipData($relationships[$field]['data'], $includedRegistry); + } else { + foreach ($relationship['data'] as $i => $data) { + $this->_mapRelationshipData($relationships[$field]['data'][$i], $includedRegistry); + } + } + } + } + } + + protected function _mapRelationshipData(&$data, array $includedRegistry): void + { + if (isset($data['id'])) { + $included_id = ($data['type'] ?? NULL) . ':' . $data['id']; + if (isset($includedRegistry[$included_id])) { + $data['included'] = $includedRegistry[$included_id]; + } + if (!empty($data['relationships'])) { + $this->_resolveRelationsRecursive($data['relationships'], $includedRegistry); + } + } + } + } diff --git a/src/Serialization/JsonApi.php b/src/Serialization/JsonApi.php new file mode 100644 index 0000000000000000000000000000000000000000..282983bf5a9fd7e5bcef73b2e3cb61d5589c74be --- /dev/null +++ b/src/Serialization/JsonApi.php @@ -0,0 +1,96 @@ +<?php + +namespace Drupal\external_entities\Serialization; + +use Drupal\Component\Serialization\Json; + +/** + * Denormalizes the json:api response + * + * This is handled via a Serialization handler as this is the first opportunity + * to handle raw responses fetched by the RestClient. Which means the processing + * applies to _all_ types of requests single & multiple load scenarios. + * + * The denormalization is necessary because json:api normalizes data references + * into the "included" property which is outside the actual data set. + * However, jsonpath does not provide a sane way to access this included data + * bits from the result item itself. + * So this recursive handling will put _all_ included data into each result as + * well as adds the related included data to the relationships referencing the + * data. + * Allowing relative simple jsonPath expressions like: + * $.relationships.field_taxonomy_terms.data.included.attributes.field_body + * + * @TODO See if there's any way the existing jsonapi denormalizer can be used: + * \Drupal\jsonapi\Serializer\Serializer::denormalize() + */ +class JsonApi extends Json { + + public static function getFileExtension() + { + return 'external_entities_api_json'; + } + + /** + * Denormalizes the json:api response + * + * @param $string + * + * @return mixed + */ + public static function decode($string) + { + $decoded_data = parent::decode($string); + + if (!empty($decoded_data['data']) && !empty($decoded_data['included'])) { + foreach ($decoded_data['data'] as $i => $result) { + $decoded_data['data'][$i]['included'] = $decoded_data['included']; + if (!empty($result['relationships'])) { + static::resolveRelationships($decoded_data['data'][$i]); + } + } + } + + return $decoded_data; + } + + protected static function resolveRelationships(&$result): void + { + // Collect all includes and index them by type and id. + $includedRegistry = []; + foreach ($result['included'] as $included_entry) { + $included_id = ($included_entry['type'] ?? NULL) . ':' . $included_entry['id']; + $includedRegistry[$included_id] = $included_entry; + } + static::_resolveRelationsRecursive($result['relationships'], $includedRegistry); + } + + protected static function _resolveRelationsRecursive(&$relationships, $includedRegistry): void + { + foreach ($relationships as $field => $relationship) { + if (!empty($relationship['data'])) { + // Relationships can be single or multi-value. + if (isset($relationship['data']['id'])) { + static::_mapRelationshipData($relationships[$field]['data'], $includedRegistry); + } else { + foreach ($relationship['data'] as $i => $data) { + static::_mapRelationshipData($relationships[$field]['data'][$i], $includedRegistry); + } + } + } + } + } + + protected static function _mapRelationshipData(&$data, array $includedRegistry): void + { + if (isset($data['id'])) { + $included_id = ($data['type'] ?? NULL) . ':' . $data['id']; + if (isset($includedRegistry[$included_id])) { + $data['included'] = $includedRegistry[$included_id]; + } + if (!empty($data['relationships'])) { + static::_resolveRelationsRecursive($data['relationships'], $includedRegistry); + } + } + } +}