diff --git a/src/Plugin/migrate_plus/data_parser/Json.php b/src/Plugin/migrate_plus/data_parser/Json.php index c94444b..e63a9f9 100755 --- a/src/Plugin/migrate_plus/data_parser/Json.php +++ b/src/Plugin/migrate_plus/data_parser/Json.php @@ -30,17 +30,26 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa protected $iterator; /** + * Tracks retrieved URLs. + * + * @var array + */ + protected $checked = []; + + /** * Retrieves the JSON data and returns it as an array. * * @param string $url * URL of a JSON feed. + * @param $item_selector + * Selector within the data content at which useful data is found. * * @return array * The selected data to be iterated. * * @throws \GuzzleHttp\Exception\RequestException */ - protected function getSourceData($url) { + protected function getSourceData($url, $item_selector = '') { $response = $this->getDataFetcherPlugin()->getResponseContent($url); // Convert objects to associative arrays. @@ -54,18 +63,16 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa } // Backwards-compatibility for depth selection. - if (is_int($this->itemSelector)) { + if (is_int($item_selector)) { return $this->selectByDepth($source_data); } // Otherwise, we're using xpath-like selectors. $selectors = explode('/', trim($this->itemSelector, '/')); foreach ($selectors as $selector) { - if (!empty($selector)) { - $source_data = $source_data[$selector]; - } + $source_data = $source_data[$selector]; } - return $source_data; + return static::select($source_data, $item_selector); } /** @@ -104,7 +111,7 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa */ protected function openSourceUrl($url) { // (Re)open the provided URL. - $source_data = $this->getSourceData($url); + $source_data = $this->getSourceData($url, $this->itemSelector); $this->iterator = new \ArrayIterator($source_data); return TRUE; } @@ -115,12 +122,12 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa protected function fetchNextRow() { $current = $this->iterator->current(); if ($current) { + if (!empty($this->configuration['list'])) { + $current = $this->getRemoteItemData($current, $this->configuration['list']); + } foreach ($this->fieldSelectors() as $field_name => $selector) { $field_data = $current; - $field_selectors = explode('/', trim($selector, '/')); - foreach ($field_selectors as $field_selector) { - $field_data = $field_data[$field_selector]; - } + $field_data = static::select($field_data, $selector); $this->currentItem[$field_name] = $field_data; } if (!empty($this->configuration['include_raw_data'])) { @@ -130,4 +137,106 @@ class Json extends DataParserPluginBase implements ContainerFactoryPluginInterfa } } + /** + * Handle "list" mode in the source data. + * + * A list-enabled source will contain data which uniquely identifies a remote + * object which is the intended item for import. + * + * @param array|bool $options + * In cases where the list mode is simply activated, the row of data will be + * treated as a simple URI to be followed. As an array, there are several + * specific options that will be applied. + * - selector: Operates as the "field selector" for which value to + * use within the data to derive a URI. + * - remote_item_selector: Similar to item_selector in the source + * configuration, but used to extract the correct data from the remote + * source. + * - uri_template: URI Template to slot ID values into a coherent URL. + */ + protected function getRemoteItemData($data, $options = TRUE) { + // Lookup relevant data within the row. + if (is_array($data) && !empty($options['selector'])) { + $data = static::select($data, $options['selector']); + } + + // Construct the URI at which the data for the specific object may be found. + if (isset($options['uri_template'])) { + $uri = static::getTemplatedUri($options['uri_template'], $data); + } + else { + $uri = $data; + } + + $item_selector = isset($options['remote_item_selector']) ? $options['remote_item_selector'] : $this->itemSelector; + + // Provide some minimum feedback that this specific data value was + // previously targeted for retrieval. + if (isset($this->checked[$uri . $item_selector])) { + \Drupal::logger('migrate')->notice('The URI %uri was previously retrieved for selector %selector.', [ + '%uri' => $uri, + '%selector' => $item_selector, + ]); + } + $this->checked[$uri . $item_selector] = TRUE; + $item_data = $this->getSourceData($uri, $item_selector); + + return $item_data; + } + + /** + * Generate a URI based on data and a URI template. + * + * @param string $template + * URI template. If it lacks curly-brace delimited items and $variables + * is scalar the $variables value will be appended to the template. + * Otherwise named elements on $variables will be replaced. + * @param mixed $variables + * If a scalar this will be appended to the template. If an array, named + * elements will be used as data source for replacement in the template. + * + * @return string + * Processed link template. + * + * @todo find a dedicated facility for link template logic. + */ + protected static function getTemplatedUri($template, $variables) { + if (strpos($template, '{') === FALSE && is_scalar($variables)) { + return $template . $variables; + } + elseif (strpos($template, '{') !== FALSE && is_array($variables)) { + foreach ($variables as $key => $value) { + if (is_scalar($value)) { + $pattern = preg_quote('{' . $key . '}'); + $template = preg_replace('/' . $pattern . '/', $value, $template); + } + } + } + + return $template; + } + + /** + * Provide xpath-like dereferencing of objects from an array. + * + * @param array $data + * The data from which we extract our value. + * @param string $selector + * Selector in the format of "one/0/id" + * + * @return mixed + * Value found in the data for the provided selector. + */ + protected static function select(array $data = [], $selector) { + $selector = trim($selector, '/'); + if (!empty($selector)) { + $field_selectors = explode('/', $selector); + foreach ($field_selectors as $field_selector) { + $data = $data[$field_selector]; + } + } + + return $data; + } + }