migmag-1.0.x-dev/migmag_menu_link_migrate/src/MigMagMenuLinkMigrate.php
migmag_menu_link_migrate/src/MigMagMenuLinkMigrate.php
<?php declare(strict_types=1); namespace Drupal\migmag_menu_link_migrate; use Drupal\Component\Utility\Unicode; use Drupal\migmag\Utility\MigMagArrayUtility; use Drupal\migmag\Utility\MigMagMigrationUtility; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\migrate\Row; use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; // cspell:ignore plid /** * Alters and prepares menu link migrations to migrate as much data as possible. */ class MigMagMenuLinkMigrate { /** * The plugin ID of the menu link trap migration. * * @const string */ const TRAP_MIGRATION_ID = 'migmag_unmigratable_menu_link_trap'; /** * The path set for unmigratable (or stub) menu links. * * @const string */ const NON_MIGRATABLE_LINKS_PATH = 'route:<current>'; /** * Adds fixes to menu link migrations. * * @param array $migrations * An array of migration configurations keyed by migration ID. See * hook_migration_plugins_alter(). * @param bool $migrate_as_much_as_possible * Whether link/uri validation exceptions also should be suppressed. */ public static function applyMenuLinkMigrationConfigurationFixes(array &$migrations, bool $migrate_as_much_as_possible = FALSE): void { $menu_link_migrations = array_filter( $migrations, function (array $definition) { return in_array( $definition['id'], [ 'd7_menu_links', 'node_translation_menu_links', ] ); } ); foreach ($menu_link_migrations as $plugin_id => $plugin_definition) { if ($migrate_as_much_as_possible) { // If we want to migrate as much data as possible, we will wrap the // whole 'link/uri' process pipeline into migmag_try. $plugin_definition['process']['link/uri'] = [ 'plugin' => 'migmag_try', 'process' => $plugin_definition['process']['link/uri'], 'catch' => ['Drupal\migrate\MigrateException' => self::NON_MIGRATABLE_LINKS_PATH], ]; // Ensure that 'enabled' and 'title' follow 'link/uri'. MigMagArrayUtility::moveAfterKey( $plugin_definition['process'], 'link/uri', 'enabled' ); MigMagArrayUtility::moveAfterKey( $plugin_definition['process'], 'link/uri', 'title' ); // ...so we can use the processed value of 'path_is_available' in the // process pipeline of 'title' and 'enabled'! 'path_is_available' // determines whether the migrated link path exists on the destination. MigMagArrayUtility::insertAfterKey( $plugin_definition['process'], 'link/uri', 'path_is_available', [ 'plugin' => 'static_map', 'source' => '@link/uri', 'map' => [self::NON_MIGRATABLE_LINKS_PATH => 0], 'default_value' => 1, ] ); // We add the original 'enabled' process as 'original_enabled' in front // of 'enabled', and add call 'intval' on its result: we want to use // this later in a static map. MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'enabled', 'original_enabled', array_merge( MigMagMigrationUtility::getAssociativeMigrationProcess($plugin_definition['process']['enabled']), [['plugin' => 'callback', 'callable' => 'intval']] ) ); // ...then we change the process pipeline of 'enabled': If we are about // to migrate a menu link whose destination path is missing then the // menu link should be disabled. $plugin_definition['process']['enabled'] = [ 'plugin' => 'static_map', 'source' => [ '@path_is_available', '@original_enabled', ], 'map' => [ 1 => [ 0 => 0, 1 => 1, ], ], 'default_value' => 0, ]; // To help our users, we also add a suffix to the menu link's title // which will contain the original menu link path. // First add the constants we will use. $plugin_definition['source']['constants']['missing_path_prefix'] = " (unavailable: '"; $plugin_definition['source']['constants']['missing_path_suffix'] = "')"; // Add the pipeline of 'title_suffix' which will contain the path in the // source if the link does not exist on the destination. It will be NULL // otherwise. MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'title', 'title_suffix', [ [ 'plugin' => 'static_map', 'source' => ['@path_is_available', '@stub'], 'map' => [ 0 => ['do not skip'], ], 'default_value' => NULL, ], [ 'plugin' => 'skip_on_empty', 'method' => 'process', ], [ 'plugin' => 'concat', 'source' => [ 'constants/missing_path_prefix', 'link_path', 'constants/missing_path_suffix', ], ], ] ); MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'title_suffix', 'stub', [ 'plugin' => 'default_value', 'source' => 'stub', 'default_value' => 0, ] ); // Re-add the original process pipeline of 'title' as 'original_title', // in front of 'title'. MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'title', 'original_title', $plugin_definition['process']['title'] ); // Finally, concatenate the processed value of 'original_title' and // 'title_suffix'! $plugin_definition['process']['title'] = [ 'plugin' => 'concat', 'source' => [ '@original_title', '@title_suffix', ], ]; } // Add the required constant for 'lookup_stub_parent'. $plugin_definition['source']['constants']['plugin_prefix'] = 'menu_link_content'; MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'parent', 'lookup_stub_parent', [ [ 'plugin' => 'skip_on_empty', 'source' => '@parent_uuid', 'method' => 'process', ], [ 'plugin' => 'concat', 'source' => [ 'constants/plugin_prefix', '@parent_uuid', ], 'delimiter' => ':', ], ] ); // Add the 'parent_uuid' process right before the new 'parent' process // pipeline. MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'lookup_stub_parent', 'parent_uuid', [ [ 'plugin' => 'callback', 'callable' => 'is_string', 'source' => '@original_parent', ], [ 'plugin' => 'callback', 'callable' => 'intval', ], [ 'plugin' => 'static_map', 'map' => [1, 0], ], [ 'plugin' => 'skip_on_empty', 'method' => 'process', ], [ 'plugin' => 'skip_on_empty', 'source' => 'plid', 'method' => 'process', ], [ 'plugin' => 'migmag_lookup', 'stub_default_values' => [ 'menu_name' => '@menu_name', ], 'migration' => [ 'd7_menu_links', 'd7_menu_links_localized', 'd7_menu_links_translation', 'node_translation_menu_links', ], 'fallback_stub_id' => static::TRAP_MIGRATION_ID, ], [ 'plugin' => 'skip_on_empty', 'method' => 'process', ], [ 'plugin' => 'migmag_get_entity_property', 'entity_type_id' => 'menu_link_content', 'property' => 'uuid', ], ] ); // We need the original parent process pipeline because it also searches // for parents based on the parent menu link's path. But we will use // migmag_try to catch its exceptions. MigMagArrayUtility::insertInFrontOfKey( $plugin_definition['process'], 'parent_uuid', 'original_parent', [ [ 'plugin' => 'migmag_try', 'process' => $plugin_definition['process']['parent'], 'catch' => ['Drupal\migrate\MigrateSkipRowException' => NULL], 'saveMessage' => FALSE, ], [ 'plugin' => 'default_value', 'default_value' => NULL, ], ] ); // Change the parent process to use null coalesce. $plugin_definition['process']['parent'] = [ 'plugin' => 'null_coalesce', 'source' => [ '@original_parent', '@lookup_stub_parent', ], ]; $migrations[$plugin_id] = $plugin_definition; } } /** * Prepares the migration of menu link stubs. * * @param \Drupal\migrate\Row $row * The row being imported. * @param \Drupal\migrate\Plugin\MigrationInterface $migration * The current migration. */ public static function prepareMenuLinkStubMigration(Row $row, MigrationInterface $migration): void { if ( !$migration->getDestinationPlugin()->getPluginId() === 'entity:menu_link_content' || !$row->isStub() ) { return; } // Do not process plid, link_path for menu link content stubs. $source_path = $row->getSourceProperty('link_path'); $title_suffix = $source_path ? " (stub: '{$source_path}')" : ''; $row->setSourceProperty( 'link_title', $row->getSourceProperty('link_title') . $title_suffix ); $row->setSourceProperty('stub', 1); $row->setSourceProperty('plid', 0); $row->setSourceProperty('link_path', 'route:<current>'); // Stubs must be disabled. $row->setSourceProperty('enabled', FALSE); } /** * Returns the link path of the specified menu link. * * @param string|int $mlid * The menu link identifier in the source Drupal 7 instance. * * @return array|null * The crucial data of the specified menu link, or NULL if the menu link * cannot be found in the source. */ public static function getSourceMenuLinkData($mlid): ?array { // We need a Drupal Sql based source plugin. // @see MigrationDeriverTrait::getSourcePlugin() $manager = \Drupal::service('plugin.manager.migration'); assert($manager instanceof MigrationPluginManagerInterface); $variable = $manager->createStubMigration([ 'source' => ['ignore_map' => TRUE, 'plugin' => 'variable'], 'destination' => ['plugin' => 'null'], 'idMap' => ['plugin' => 'null'], ])->getSourcePlugin(); assert($variable instanceof DrupalSqlBase); $menu_link_path = $variable->getDatabase() ->select('menu_links', 'ml') ->fields('ml', [ 'link_title', 'link_path', 'options', 'weight', 'hidden', 'expanded', ]) ->condition('ml.mlid', $mlid) ->execute() ->fetch(\PDO::FETCH_ASSOC); if (!$menu_link_path) { return NULL; } // See \Drupal\menu_link_content\Plugin\migrate\source::prepareRow() $menu_link_path['enabled'] = (int) !$menu_link_path['hidden']; $menu_link_path['options'] = unserialize($menu_link_path['options']); $menu_link_path['description'] = Unicode::truncate($menu_link_path['options']['attributes']['title'] ?? '', 255); return $menu_link_path; } }