diff --git a/modules/migrate_preview/migrate_preview.info.yml b/modules/migrate_preview/migrate_preview.info.yml new file mode 100644 index 0000000..f272aa5 --- /dev/null +++ b/modules/migrate_preview/migrate_preview.info.yml @@ -0,0 +1,8 @@ +name: 'Migrate preview' +description: 'Displays a preview of a migration.' +core: 8.x +type: module +package: 'Migration' +dependencies: + - drupal:migrate + - migrate_tools:migrate_tools diff --git a/modules/migrate_preview/migrate_preview.links.task.yml b/modules/migrate_preview/migrate_preview.links.task.yml new file mode 100644 index 0000000..82d05cb --- /dev/null +++ b/modules/migrate_preview/migrate_preview.links.task.yml @@ -0,0 +1,5 @@ +entity.migration.overview_preview: + title: Preview + route_name: entity.migration.preview + parent_id: entity.migration.overview + weight: 2.5 diff --git a/modules/migrate_preview/migrate_preview.routing.yml b/modules/migrate_preview/migrate_preview.routing.yml new file mode 100644 index 0000000..8fdcded --- /dev/null +++ b/modules/migrate_preview/migrate_preview.routing.yml @@ -0,0 +1,14 @@ +entity.migration.preview: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/preview' + defaults: + _controller: '\Drupal\migrate_preview\Controller\MigrationController::preview' + _title: 'Preview' + _migrate_group: true + requirements: + _permission: 'administer migrations' + options: + parameters: + migration: + type: entity:migration + migration_group: + type: entity:migration_group diff --git a/modules/migrate_preview/src/Controller/MigrationController.php b/modules/migrate_preview/src/Controller/MigrationController.php new file mode 100644 index 0000000..f7da7b6 --- /dev/null +++ b/modules/migrate_preview/src/Controller/MigrationController.php @@ -0,0 +1,195 @@ +migrationPluginManager = $migration_plugin_manager; + $this->currentRouteMatch = $currentRouteMatch; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.migration'), + $container->get('current_route_match') + ); + } + + /** + * Displays a preview of the source. + * + * @param \Drupal\migrate_plus\Entity\MigrationGroupInterface $migration_group + * The migration group. + * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration + * The $migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function preview(MigrationGroupInterface $migration_group, MigrationInterface $migration) { + $migration_plugin = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); + $migrateMessage = new MigrateMessage(); + $executable = new MigratePreviewExecutable($migration_plugin, $migrateMessage, [ + 'limit' => 10, + 'update' => TRUE, + 'force' => TRUE, + ]); + + // Compose header. + $header = []; + $source = $migration_plugin->getSourcePlugin(); + foreach ($source->fields($migration_plugin) as $machine_name => $description) { + $header[$machine_name] = Xss::filterAdmin($description); + } + + $migrate_rows = $executable->preview(); + return [ + 'source' => $this->buildTable($header, $migrate_rows, 'source'), + 'destination' => $this->buildTable($header, $migrate_rows, 'destination'), + ]; + } + + /** + * Builds a table from the given result. + * + * @param array $headers + * The expected headers. + * @param \Drupal\migrate\Row[] $migrate_rows + * A list of migrate rows. + * + * @return array + * The rows for in the table. + */ + protected function buildTable(array $headers, array $migrate_rows, $method = 'source') { + if (empty($migrate_rows)) { + return [ + '#plain_text' => $this->t('No data.'), + ]; + } + + // Add keys from first item as additional headers. + switch ($method) { + case 'source': + $item = reset($migrate_rows)->getSource(); + break; + + case 'destination': + $item = reset($migrate_rows)->getRawDestination(); + break; + } + $keys = array_merge(array_keys($headers), array_keys($item)); + foreach (array_keys($item) as $key) { + if (!isset($headers[$key])) { + $headers[$key] = Xss::filterAdmin($key); + } + } + + $rows = []; + $index = 0; + foreach ($migrate_rows as $migrate_row) { + switch ($method) { + case 'source': + $row = $migrate_row->getSource(); + break; + + case 'destination': + $row = $migrate_row->getRawDestination(); + break; + } + $row += array_fill_keys($keys, NULL); + + foreach ($keys as $column) { + $rows[$index][$column] = $this->buildValue($row[$column]); + } + + $index++; + } + + return [ + '#type' => 'container', + '#attributes' => [ + 'style' => 'overflow: scroll;', + ], + 'table' => [ + '#type' => 'table', + '#header' => $headers, + ] + $rows, + ]; + } + + protected function buildValue($value) { + $row_value = [ + '#plain_text' => $value, + ]; + + if (is_string($value) && strlen($value) > 255) { + $value = substr($value, 0, 255) . '...'; + } + + if (is_scalar($value)) { + $row_value['#plain_text'] = $value; + } + elseif (is_array($value)) { + foreach ($value as $value_index => &$subvalue) { + if (is_string($subvalue)) { + if (strlen($subvalue) > 255) { + $subvalue = substr($subvalue, 0, 255) . '...'; + } + } + if (!is_scalar($subvalue)) { + $subvalue = print_r($subvalue, TRUE); + $value[$value_index] = $this->buildValue($subvalue)['#plain_text']; + } + } + + $row_value = [ + '#theme' => 'item_list', + '#items' => $value, + ]; + } + + return $row_value; + } + +} diff --git a/modules/migrate_preview/src/MigratePreviewExecutable.php b/modules/migrate_preview/src/MigratePreviewExecutable.php new file mode 100644 index 0000000..2689ce2 --- /dev/null +++ b/modules/migrate_preview/src/MigratePreviewExecutable.php @@ -0,0 +1,128 @@ +source)) { + $this->source = $this->migration->getSourcePlugin(); + } + return $this->source; + } + + /** + * Get the ID map from the current migration. + * + * @return \Drupal\migrate\Plugin\MigrateIdMapInterface + * The ID map. + */ + protected function getIdMap() { + return $this->migration->getIdMap(); + } + + /** + * + */ + public function preview() { + $this->getEventDispatcher()->dispatch(MigrateEvents::PRE_IMPORT, new MigrateImportEvent($this->migration, $this->message)); + + // Knock off migration if the requirements haven't been met. + try { + $this->migration->checkRequirements(); + } + catch (RequirementsException $e) { + $this->message->display( + $this->t( + 'Migration @id did not meet the requirements. @message @requirements', + [ + '@id' => $this->migration->id(), + '@message' => $e->getMessage(), + '@requirements' => $e->getRequirementsString(), + ] + ), + 'error' + ); + + return []; + } + + $source = $this->getSource(); + $id_map = $this->getIdMap(); + $id_map->prepareUpdate(); + $this->migration->set('requirements', []); + + try { + $source->rewind(); + } + catch (\Exception $e) { + $this->message->display( + $this->t('Migration failed with source plugin exception: @e', ['@e' => $e->getMessage()]), 'error'); + return []; + } + + $rows = []; + while ($source->valid()) { + $row = $source->current(); + $this->sourceIdValues = $row->getSourceIdValues(); + + try { + $this->processRow($row); + $rows[] = $row; + $this->itemLimitCounter++; + + if ($this->itemLimit && ($this->itemLimitCounter) >= $this->itemLimit) { + break; + } + } + catch (MigrateException $e) { + // @todo show message to user. + //$this->getIdMap()->saveIdMapping($row, [], $e->getStatus()); + //$this->saveMessage($e->getMessage(), $e->getLevel()); + } + catch (MigrateSkipRowException $e) { + // @todo show message to user. +// if ($e->getSaveToMap()) { +// $id_map->saveIdMapping($row, [], MigrateIdMapInterface::STATUS_IGNORED); +// } +// if ($message = trim($e->getMessage())) { +// $this->saveMessage($message, MigrationInterface::MESSAGE_INFORMATIONAL); +// } + } + + try { + $source->next(); + } + catch (\Exception $e) { + $this->message->display( + $this->t('Migration failed with source plugin exception: @e', + ['@e' => $e->getMessage()]), 'error'); + return $rows; + } + } + + return $rows; + } + +}