From 26cd9a366b4b0cf986b99056a455b208d13e93c5 Mon Sep 17 00:00:00 2001 From: Anthony Michaels <a.michaels@webpackager.com> Date: Tue, 11 Mar 2025 07:45:39 +0000 Subject: [PATCH] Create Drush migration script for D7 Link blocks to text paragraphs; disable the original link block migration script; --- ...ation.upgrade_d7_paragraphs_link_block.yml | 2 +- cambridge_migrations.services.yml | 10 +- drush.services.yml | 8 +- .../Commands/MigrateLinkBlocksCommand.php | 267 ++++++++++++++++++ 4 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 src/Drush/Commands/MigrateLinkBlocksCommand.php diff --git a/cambridge_migration_config/config/install/migrate_plus.migration.upgrade_d7_paragraphs_link_block.yml b/cambridge_migration_config/config/install/migrate_plus.migration.upgrade_d7_paragraphs_link_block.yml index 6dbaab3..016817e 100644 --- a/cambridge_migration_config/config/install/migrate_plus.migration.upgrade_d7_paragraphs_link_block.yml +++ b/cambridge_migration_config/config/install/migrate_plus.migration.upgrade_d7_paragraphs_link_block.yml @@ -1,5 +1,5 @@ langcode: en -status: true +status: false dependencies: { } id: upgrade_d7_paragraphs_link_block class: Drupal\migrate\Plugin\Migration diff --git a/cambridge_migrations.services.yml b/cambridge_migrations.services.yml index 9919969..cc08c86 100644 --- a/cambridge_migrations.services.yml +++ b/cambridge_migrations.services.yml @@ -1,6 +1,12 @@ services: - cambridge_migrations.commands: - class: \Drupal\cambridge_migrations\Drush\Commands\MigrateTextBlocksCommand + cambridge_migrations.text_blocks_command: + class: Drupal\cambridge_migrations\Drush\Commands\MigrateTextBlocksCommand + arguments: ['@entity_type.manager', '@file_system', '@database'] + tags: + - { name: drush.command } + + cambridge_migrations.link_blocks_command: + class: Drupal\cambridge_migrations\Drush\Commands\MigrateLinkBlocksCommand arguments: ['@entity_type.manager', '@file_system', '@database'] tags: - { name: drush.command } diff --git a/drush.services.yml b/drush.services.yml index a810dea..cc08c86 100644 --- a/drush.services.yml +++ b/drush.services.yml @@ -1,6 +1,12 @@ services: - cambridge_migrations.commands: + cambridge_migrations.text_blocks_command: class: Drupal\cambridge_migrations\Drush\Commands\MigrateTextBlocksCommand arguments: ['@entity_type.manager', '@file_system', '@database'] tags: - { name: drush.command } + + cambridge_migrations.link_blocks_command: + class: Drupal\cambridge_migrations\Drush\Commands\MigrateLinkBlocksCommand + arguments: ['@entity_type.manager', '@file_system', '@database'] + tags: + - { name: drush.command } diff --git a/src/Drush/Commands/MigrateLinkBlocksCommand.php b/src/Drush/Commands/MigrateLinkBlocksCommand.php new file mode 100644 index 0000000..4327896 --- /dev/null +++ b/src/Drush/Commands/MigrateLinkBlocksCommand.php @@ -0,0 +1,267 @@ +<?php + +namespace Drupal\cambridge_migrations\Drush\Commands; + +use Drush\Commands\DrushCommands; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Database\Connection; +use Drupal\paragraphs\Entity\Paragraph; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Drush command for migrating D7 Link block paragraphs to D10 Text paragraphs. + * + * Each D7 Link block paragraph has: + * - field_paragraph_heading (plain text, single entry) + * - field_paragraph_links (link field, unlimited entries) + * + * This command converts each D7 Link block into a D10 Text paragraph. + * The resulting content is structured as: + * - If present, the heading is output as an <h2> element. + * - Below the heading, the links are rendered as an unordered list. + * + * To run: + * ddev drush cambridge:migrate-link-blocks + */ +class MigrateLinkBlocksCommand extends DrushCommands { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The file system service. + * + * @var \Drupal\Core\File\FileSystemInterface + */ + protected $fileSystem; + + /** + * Old (source) DB connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $sourceDb; + + /** + * New (target) DB connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $targetDb; + + /** + * Constructs a new MigrateLinkBlocksCommand object. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + FileSystemInterface $file_system, + Connection $database + ) { + parent::__construct(); + $this->entityTypeManager = $entity_type_manager; + $this->fileSystem = $file_system; + // Assumes your D7 database connection is configured under the key 'migrate'. + $this->sourceDb = \Drupal\Core\Database\Database::getConnection('default', 'migrate'); + $this->targetDb = \Drupal\Core\Database\Database::getConnection('default', 'default'); + } + + /** + * Clean up HTML content. + * + * @param string $content + * The raw content. + * + * @return string + * The cleaned content. + */ + protected function cleanContent($content) { + if (empty($content)) { + return $content; + } + // Remove empty <p> tags. + $content = preg_replace('/<p>\s*<\/p>/', '', $content); + // Collapse multiple blank lines. + $content = preg_replace('/(\r?\n){2,}/', "\n\n", $content); + return trim($content); + } + + /** + * Remove content teaser paragraphs from a node. + * + * This is used to clear out any existing content teasers from a node + * before adding new paragraph items. + * + * @param \Drupal\node\NodeInterface $node + * The node from which to remove content teaser paragraphs. + */ + protected function removeContentTeasers($node) { + if (!$node->hasField('field_paragraph')) { + return; + } + $paragraphs = $node->get('field_paragraph')->referencedEntities(); + $updated_paragraphs = []; + $removed_count = 0; + foreach ($paragraphs as $paragraph) { + if ($paragraph->getType() !== 'content_teaser') { + $updated_paragraphs[] = [ + 'target_id' => $paragraph->id(), + 'target_revision_id' => $paragraph->getRevisionId(), + ]; + } + else { + $paragraph->delete(); + $removed_count++; + } + } + if ($removed_count > 0) { + $node->set('field_paragraph', $updated_paragraphs); + $node->save(); + $this->logger()->notice(dt('Removed @count content teaser paragraph(s) from node @nid', [ + '@count' => $removed_count, + '@nid' => $node->id(), + ])); + } + } + + /** + * Migrate D7 Link block paragraphs to D10 Text paragraphs. + * + * @command cambridge:migrate-link-blocks + * @aliases cm-link-blocks + */ + public function migrateLinkBlocks() { + try { + // Query the D7 database for Link block paragraphs. + $query = $this->sourceDb->select('paragraphs_item', 'p') + ->fields('p', ['item_id', 'bundle', 'field_name']) + ->condition('p.bundle', 'link_block'); + + // Join with the heading field. + $query->leftJoin('field_data_field_paragraph_heading', 'h', + 'p.item_id = h.entity_id AND h.entity_type = :entity_type', + [':entity_type' => 'paragraphs_item'] + ); + $query->fields('h', ['field_paragraph_heading_value']); + + // Join with the node reference field to get the parent node ID. + $query->leftJoin('field_data_field_content_items', 'n', + 'p.item_id = n.field_content_items_value' + ); + $query->fields('n', ['entity_id']); + + $link_blocks = $query->execute()->fetchAll(); + if (empty($link_blocks)) { + $this->logger()->warning(dt('No link blocks found to migrate.')); + return; + } + + $success_count = 0; + $error_count = 0; + + foreach ($link_blocks as $block) { + try { + // Skip if no node reference. + if (empty($block->entity_id)) { + $this->logger()->warning(dt('Skipping link block @id - no node reference found', [ + '@id' => $block->item_id, + ])); + continue; + } + + // Start building the content. + $content = ''; + if (!empty($block->field_paragraph_heading_value)) { + $content .= '<h2>' . $block->field_paragraph_heading_value . '</h2>'; + } + + // Query for all links associated with this D7 Link block. + $links_query = $this->sourceDb->select('field_data_field_paragraph_links', 'l') + ->fields('l', ['field_paragraph_links_url', 'field_paragraph_links_title']) + ->condition('l.entity_id', $block->item_id) + ->orderBy('l.delta', 'ASC'); + $links = $links_query->execute()->fetchAll(); + + if (!empty($links)) { + // Render each link as an unordered list. + $links_output = '<ul>'; + foreach ($links as $link) { + $url = $link->field_paragraph_links_url; + $title = !empty($link->field_paragraph_links_title) ? $link->field_paragraph_links_title : $url; + $links_output .= '<li><a href="' . $url . '">' . $title . '</a></li>'; + } + $links_output .= '</ul>'; + $content .= $links_output; + } + + // Clean up the final content. + $content = $this->cleanContent($content); + + // Load the corresponding D10 node. + $node = $this->entityTypeManager->getStorage('node')->load($block->entity_id); + if (!$node) { + throw new \Exception("Node {$block->entity_id} not found in D10."); + } + + // Remove content teasers before adding the new paragraph. + $this->removeContentTeasers($node); + + // Create a new paragraph of type 'text' with the generated content. + $paragraph = Paragraph::create([ + 'type' => 'text', + 'field_text' => [ + 'value' => $content, + 'format' => 'filtered_html' + ], + ]); + $paragraph->save(); + + // Attach the new paragraph to the node’s field_paragraph. + $node->field_paragraph[] = [ + 'target_id' => $paragraph->id(), + 'target_revision_id' => $paragraph->getRevisionId(), + ]; + $node->save(); + + $success_count++; + $this->logger()->notice(dt('Successfully migrated link block @id to node @nid', [ + '@id' => $block->item_id, + '@nid' => $block->entity_id, + ])); + } + catch (\Exception $e) { + $error_count++; + $this->logger()->error(dt('Error migrating link block @id: @message', [ + '@id' => $block->item_id, + '@message' => $e->getMessage(), + ])); + } + } + + $this->logger()->notice(dt('Migration complete. Successes: @success, Errors: @errors', [ + '@success' => $success_count, + '@errors' => $error_count, + ])); + } + catch (\Exception $e) { + $this->logger()->error(dt('Migration failed: @message', [ + '@message' => $e->getMessage(), + ])); + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type_manager'), + $container->get('file_system'), + $container->get('database') + ); + } +} -- GitLab