diff --git a/README.md b/README.md index 17998ae..c32b0fa 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Post content to multiple sites and sections. * Installation # INTRODUCTION -The Tide Site module provides the functionality to post to multiple sites and sections from - a single content server. +The Tide Site module provides the functionality to post to multiple sites and + sections from a single content server. # REQUIREMENTS * [Tide Core](https://github.com/dpc-sdp/tide_core) @@ -30,5 +30,6 @@ composer require dpc-sdp/tide_site # Caveats -Tide Site is on the alpha release, use with caution. APIs are likely to change before the stable version, that there will be breaking changes and that we're not supporting it for external production sites at the moment. - +Tide Site is on the alpha release, use with caution. APIs are likely to change + before the stable version, that there will be breaking changes and that we're + not supporting it for external production sites at the moment. diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 0000000..870975a --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,6 @@ +services: + tide_site.commands: + class: \Drupal\tide_site\Commands\TideSiteCommands + arguments: ['@tide_site.migrator'] + tags: + - { name: drush.command } diff --git a/scripts/composer/ScriptHandler.php b/scripts/composer/ScriptHandler.php index a75e4d3..190882e 100644 --- a/scripts/composer/ScriptHandler.php +++ b/scripts/composer/ScriptHandler.php @@ -1,12 +1,13 @@ migrator = $migrator; + } + + /** + * Move content from one Site to another Site. + * + * @param int $source_id + * Source Site ID. + * @param int $destination_id + * Destination Site ID. + * + * @command tide_site:migrate + * @aliases tide_site-migrate tsm + * @usage tide_site:migrate + * Move content from site to . + */ + public function migrate($source_id, $destination_id) { + try { + $batch = $this->migrator->getBatch($source_id, $destination_id); + if (!empty($batch)) { + batch_set($batch); + $batch =& batch_get(); + + // Because we are doing this on the back-end. + $batch['progressive'] = FALSE; + + // Start processing the batch operations. + drush_backend_batch_process(); + } + } + catch (\Exception $e) { + $this->output()->writeln($e->getMessage()); + } + } + +} diff --git a/src/Migrator.php b/src/Migrator.php new file mode 100644 index 0000000..a6b57ed --- /dev/null +++ b/src/Migrator.php @@ -0,0 +1,297 @@ +entityTypeManager = $entity_type_manager; + $this->helper = $helper; + $this->stringTranslation = $string_translation; + $this->time = $time; + } + + /** + * Generate batch to migrate content from Source Site to Destination Site. + * + * @param int $source_id + * Source Site ID. + * @param int $destination_id + * Destination Site ID. + * + * @return array + * Batch definitions. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getBatch($source_id, $destination_id) { + $source = $this->helper->getSiteById($source_id); + $destination = $this->helper->getSiteById($destination_id); + if (!$source || !$destination) { + return []; + } + + $batch = []; + $entity_ids = []; + + // Load all entities of the Source site. + foreach ($this->helper->getSupportedEntityTypes() as $entity_type) { + $field_site_field_name = TideSiteFields::normaliseFieldName(TideSiteFields::FIELD_SITE, $entity_type); + + $query = $this->entityTypeManager->getStorage($entity_type)->getQuery(); + $query->latestRevision()->condition($field_site_field_name, $source_id); + $ids = $query->execute(); + if (!empty($ids)) { + $entity_ids[$entity_type] = array_chunk($ids, self::BATCH_SIZE); + } + } + + // Prepare the batch. + if (!empty($entity_ids)) { + $batch = [ + 'title' => $this->t('Moving content from Site @source (@source_id) to @destination (@destination_id)...', [ + '@source' => $source->getName(), + '@source_id' => $source->id(), + '@destination' => $destination->getName(), + '@destination_id' => $destination->id(), + ]), + 'operations' => [], + 'finished' => 'Drupal\tide_site\Migrator::batchFinishedCallback', + 'progress_message' => $this->t('Processed @current out of @total.'), + ]; + + foreach ($entity_ids as $entity_type => $chunks) { + foreach ($chunks as $ids) { + $batch['operations'][] = [ + 'Drupal\tide_site\Migrator::batchProcessCallback', + [ + $entity_type, + $source_id, + $destination_id, + $ids, + ], + ]; + } + } + } + + return $batch; + } + + /** + * Migrate entities from one site to another site.. + * + * @param string $entity_type + * Entity type. + * @param int $source_id + * Source Site ID. + * @param int $destination_id + * Destination Site ID. + * @param array $ids + * List of entity ID to migrate. + * @param mixed $context + * Batch context. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function migrate($entity_type, $source_id, $destination_id, array $ids, &$context) { + $field_site_field_name = TideSiteFields::normaliseFieldName(TideSiteFields::FIELD_SITE, $entity_type); + $field_primary_site_field_name = TideSiteFields::normaliseFieldName(TideSiteFields::FIELD_PRIMARY_SITE, $entity_type); + + $source = $this->helper->getSiteById($source_id); + if (!$source) { + $context['errors'][] = $this->t('Invalid source Site ID @id.', ['@id' => $source_id]); + return; + } + $destination = $this->helper->getSiteById($destination_id); + if (!$destination) { + $context['errors'][] = $this->t('Invalid destination Site ID @id.', ['@id' => $destination_id]); + return; + } + + $entities = $this->entityTypeManager->getStorage($entity_type)->loadMultiple($ids); + /** @var \Drupal\Core\Entity\EditorialContentEntityBase $entity */ + foreach ($entities as $entity) { + $data_changed = FALSE; + + try { + // Migrate the Sites field. + if ($entity->hasField($field_site_field_name)) { + $field_site = $entity->get($field_site_field_name); + if (!$field_site->isEmpty()) { + $entity_sites = $this->helper->getEntitySites($entity, TRUE); + + $values = $field_site->getValue(); + foreach ($values as $delta => $value) { + // Remove the source site. + if ($value['target_id'] == $source_id) { + unset($values[$delta]); + } + // Remove the section of the source site. + if (!empty($entity_sites['sections'][$source_id]) && $value['target_id'] == $entity_sites['sections'][$source_id]) { + unset($values[$delta]); + } + // Remove the destination site, it will be re-add later. + if ($value['target_id'] == $destination_id) { + unset($values[$delta]); + } + } + $values = array_values($values); + $values[] = ['target_id' => $destination_id]; + $entity->set($field_site_field_name, $values); + $data_changed = TRUE; + } + } + + // Migrate the Primary Site field. + if ($entity->hasField($field_primary_site_field_name)) { + $primary_site = $this->helper->getEntityPrimarySite($entity); + if ($primary_site->id() == $source_id) { + $entity->set($field_primary_site_field_name, ['target_id' => $destination_id]); + $data_changed = TRUE; + } + } + + if ($data_changed) { + // Create a new revision. + $entity->setNewRevision(TRUE); + $entity->revision_log = $this->t('Moved from site @source (@source_id) to @destination (@destination_id)', [ + '@source' => $source->getName(), + '@source_id' => $source->id(), + '@destination' => $destination->getName(), + '@destination_id' => $destination->id(), + ]); + $entity->setRevisionCreationTime($this->time->getRequestTime()); + $entity->setRevisionUserId(1); + + // Maintain the moderation state. + if ($entity->hasField('moderation_state')) { + $current_state = $entity->get('moderation_state')->getValue(); + $entity->set('moderation_state', $current_state); + } + // Otherwise maintain the publishing status. + else { + $entity->setPublished($entity->isPublished()); + } + + $entity->save(); + + $context['results'][$entity_type][] = $entity->id(); + } + } + catch (\Exception $e) { + watchdog_exception('tide_site', $e); + $context['errors'][] = $e->getMessage(); + } + } + } + + /** + * Batch finished callback. + * + * @param bool $success + * Whether batch succeeds. + * @param array $results + * The results. + * @param array $operations + * The operations. + */ + public static function batchFinishedCallback($success, array $results, array $operations) { + if (!$success) { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $error = t('An error occurred while processing %error_operation with arguments: @arguments', [ + '%error_operation' => $error_operation[0], + '@arguments' => print_r($error_operation[1], TRUE), + ]); + \Drupal::messenger()->addError($error); + } + + foreach ($results as $entity_type => $ids) { + $message = \Drupal::translation()->formatPlural( + count($ids), + '@entity_type: 1 entity moved.', '@entity_type: @count entities moved.', + ['@entity_type' => $entity_type] + ); + \Drupal::messenger()->addMessage($message); + } + + if (!empty($results['errors'])) { + foreach ($results['errors'] as $error) { + \Drupal::messenger()->addError($error); + } + } + } + + /** + * Batch process callback. + * + * @param string $entity_type + * Entity type. + * @param int $source_id + * Source Site ID. + * @param int $destination_id + * Destination Site ID. + * @param array $ids + * List of ID to process. + * @param mixed $context + * Batch context. + */ + public static function batchProcessCallback($entity_type, $source_id, $destination_id, array $ids, &$context) { + $service = \Drupal::service('tide_site.migrator'); + $service->migrate($entity_type, $source_id, $destination_id, $ids, $context); + } + +} diff --git a/src/TideSiteServiceProvider.php b/src/TideSiteServiceProvider.php index 560983b..e927361 100644 --- a/src/TideSiteServiceProvider.php +++ b/src/TideSiteServiceProvider.php @@ -41,17 +41,16 @@ public function alter(ContainerBuilder $container) { * {@inheritdoc} */ public function register(ContainerBuilder $container) { - - // Dynamically define the service tide_site.get_route_subscriber + // Dynamically define the service tide_site.get_route_subscriber. $modules = $container->getParameter('container.modules'); - // Check for installed tide_api module.. - if (isset($modules['tide_api']) ) { - + // Check for installed tide_api module. + if (isset($modules['tide_api'])) { $container->register('tide_site.get_route_subscriber', 'Drupal\tide_site\EventSubscriber\TideSiteGetRouteSubscriber') ->addTag('event_subscriber') ->addMethodCall('setContainer', [new Reference('service_container')]) ->addMethodCall('setStringTranslation', [new Reference('string_translation')]); } } + } diff --git a/tide_site.drush.inc b/tide_site.drush.inc new file mode 100644 index 0000000..f9b588d --- /dev/null +++ b/tide_site.drush.inc @@ -0,0 +1,51 @@ + 'Move content from one Site to another Site.', + 'callback' => 'drush_tide_site_migrate', + 'drupal dependencies' => ['tide_site'], + 'arguments' => [ + 'source_id' => 'Source Site ID', + 'destination_id' => 'Destination Site ID', + ], + 'aliases' => ['tide_site:migrate', 'tsm'], + ]; + + return $items; +} + +/** + * Drush command callback. + * + * @param int $source_id + * Source Site ID. + * @param int $destination_id + * Destination Site ID. + */ +function drush_tide_site_migrate($source_id, $destination_id) { + try { + $batch = \Drupal::service('tide_site.migrator')->getBatch($source_id, $destination_id); + if (!empty($batch)) { + batch_set($batch); + $batch =& batch_get(); + + // Because we are doing this on the back-end. + $batch['progressive'] = FALSE; + + // Start processing the batch operations. + drush_backend_batch_process(); + } + } + catch (\Exception $e) { + drush_log($e->getMessage(), 'error'); + } +} diff --git a/tide_site.module b/tide_site.module index 6fa803e..9d0c3e2 100644 --- a/tide_site.module +++ b/tide_site.module @@ -9,6 +9,8 @@ use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Link; +use Drupal\node\Entity\Node; +use Drupal\search_api\IndexInterface; use Drupal\taxonomy\Entity\Term; use Drupal\tide_site\TideSiteFields; use Drupal\tide_site\TideSiteMenuAutocreate; @@ -615,16 +617,10 @@ function tide_site_linkit_substitution_alter(&$data) { } /** - * Implements hook_search_api_index_items_alter() - * - * @param \Drupal\search_api\IndexInterface $index - * The search index on which items will be indexed. - * @param \Drupal\search_api\Item\ItemInterface[] $items - * The items that will be indexed. + * Implements hook_search_api_index_items_alter(). */ -function tide_site_search_api_index_items_alter(\Drupal\search_api\IndexInterface $index, array &$items) { - foreach ($items as $item_id => $item) { - +function tide_site_search_api_index_items_alter(IndexInterface $index, array &$items) { + foreach ($items as $item) { // Add all urls to the url field. $url = $item->getField("url"); @@ -636,15 +632,16 @@ function tide_site_search_api_index_items_alter(\Drupal\search_api\IndexInterfac if (!is_null($item->getField("nid"))) { $nid = $item->getField("nid")->getValues()[0]; - $node = \Drupal\node\Entity\Node::load($nid); + $node = Node::load($nid); $path = $alias_storage->load(['source' => "/node/" . $nid]); if ($path) { - $aliases = $alias_helper->getAllSiteAliases($path, $node); - } - if ($aliases){ + $aliases = $alias_helper->getAllSiteAliases($path, $node); + } + if ($aliases) { $url->setValues($aliases); $item->setField("url", $url); } } } + } diff --git a/tide_site.services.yml b/tide_site.services.yml index ea9987b..6be19b5 100644 --- a/tide_site.services.yml +++ b/tide_site.services.yml @@ -27,3 +27,6 @@ services: - [setContainer, ['@service_container']] tags: - {name: event_subscriber} + tide_site.migrator: + class: Drupal\tide_site\Migrator + arguments: ['@entity_type.manager', '@tide_site.helper', '@string_translation', '@datetime.time']