3 namespace Drupal\migrate_upgrade;
5 use Drupal\Core\Config\Entity\ConfigEntityInterface;
6 use Drupal\migrate\Plugin\MigrationInterface;
7 use Drupal\migrate\Event\MigrateEvents;
8 use Drupal\migrate\Event\MigrateIdMapMessageEvent;
9 use Drupal\migrate\MigrateExecutable;
10 use Drupal\Core\StringTranslation\StringTranslationTrait;
11 use Drupal\migrate_drupal\MigrationConfigurationTrait;
12 use Drupal\migrate_plus\Entity\Migration;
13 use Drupal\migrate_plus\Entity\MigrationGroup;
14 use Drupal\Core\Database\Database;
15 use Drush\Sql\SqlBase;
17 class MigrateUpgradeDrushRunner {
19 use MigrationConfigurationTrait;
20 use StringTranslationTrait;
23 * The list of migrations to run and their configuration.
25 * @var \Drupal\migrate\Plugin\Migration[]
27 protected $migrationList;
30 * MigrateMessage instance to display messages during the migration process.
32 * @var \Drupal\migrate_upgrade\DrushLogMigrateMessage
34 protected static $messages;
37 * The Drupal version being imported.
44 * The state key used to store database configuration.
48 protected $databaseStateKey;
51 * List of D6 node migration IDs we've seen.
55 protected $d6NodeMigrations = [];
58 * List of D6 node revision migration IDs we've seen.
62 protected $d6RevisionMigrations = [];
65 * Drush options parameters.
69 protected $options = [];
72 * List of process plugin IDs used to lookup migrations.
76 protected $migrationLookupPluginIds = [
82 * MigrateUpgradeDrushRunner constructor.
84 * @param array $options
85 * Drush options parameters.
87 public function __construct(array $options = []) {
88 $this->setOptions($options);
92 * Set options parameters according to Drush version.
94 * @param array $options
95 * Drush options parameters.
97 protected function setOptions(array $options = []) {
98 $this->options = $options;
100 if (empty($this->options)) {
102 'legacy_db_key' => drush_get_option('legacy-db-key'),
103 'legacy-db-url' => drush_get_option('legacy-db-url'),
104 'legacy-db-prefix' => drush_get_option('legacy-db-prefix'),
105 'legacy-root' => drush_get_option('legacy-root'),
106 'debug' => drush_get_option('debug'),
107 'migration-prefix' => drush_get_option('migration-prefix', 'upgrade_'),
113 * From the provided source information, instantiate the appropriate migrations
114 * in the active configuration.
118 public function configure() {
119 $legacy_db_key = $this->options['legacy-db-key'];
120 $db_url = $this->options['legacy-db-url'];
121 $db_prefix = $this->options['legacy-db-prefix'];
122 if (!empty($legacy_db_key)) {
123 $connection = Database::getConnection('default', $legacy_db_key);
124 $this->version = $this->getLegacyDrupalVersion($connection);
125 $database_state['key'] = $legacy_db_key;
126 $database_state_key = 'migrate_drupal_' . $this->version;
127 \Drupal::state()->set($database_state_key, $database_state);
128 \Drupal::state()->set('migrate.fallback_state_key', $database_state_key);
131 $db_spec = SqlBase::dbSpecFromDbUrl($db_url);
132 $db_spec['prefix'] = $db_prefix;
133 $connection = $this->getConnection($db_spec);
134 $this->version = $this->getLegacyDrupalVersion($connection);
135 $this->createDatabaseStateSettings($db_spec, $this->version);
138 $this->databaseStateKey = 'migrate_drupal_' . $this->version;
139 $migrations = $this->getMigrations($this->databaseStateKey, $this->version);
140 $this->migrationList = [];
141 foreach ($migrations as $migration) {
142 $this->applyFilePath($migration);
143 $this->expandNodeMigrations($migration);
144 $this->prefixFileMigration($migration);
145 $this->migrationList[$migration->id()] = $migration;
150 * Adds the source base path configuration to relevant migrations.
152 * @param \Drupal\migrate\Plugin\MigrationInterface $migration
153 * Migration to alter with the provided path.
155 protected function applyFilePath(MigrationInterface $migration) {
156 $destination = $migration->getDestinationConfiguration();
157 if ($destination['plugin'] === 'entity:file') {
158 // Make sure we have a single trailing slash.
159 $source_base_path = rtrim($this->options['legacy-root'], '/') . '/';
160 $source = $migration->getSourceConfiguration();
161 $source['constants']['source_base_path'] = $source_base_path;
162 $migration->set('source', $source);
167 * For D6 term_node migrations, make sure the nid reference is expanded.
169 * @param \Drupal\migrate\Plugin\MigrationInterface $migration
170 * Migration to alter with the list of node migrations.
172 protected function expandNodeMigrations(MigrationInterface $migration) {
173 $source = $migration->getSourceConfiguration();
174 // Track the node and node_revision migrations as we see them.
175 if ($source['plugin'] == 'd6_node') {
176 $this->d6NodeMigrations[] = $migration->id();
178 elseif ($source['plugin'] == 'd6_node_revision') {
179 $this->d6RevisionMigrations[] = $migration->id();
181 elseif ($source['plugin'] == 'd6_term_node') {
182 // If the ID mapping is to the underived d6_node migration, replace
183 // it with an expanded list of node migrations.
184 $process = $migration->getProcess();
185 $new_nid_process = [];
186 foreach ($process['nid'] as $delta => $plugin_configuration) {
187 if (in_array($plugin_configuration['plugin'], $this->migrationLookupPluginIds) &&
188 is_string($plugin_configuration['migration']) &&
189 substr($plugin_configuration['migration'], -7) == 'd6_node') {
190 $plugin_configuration['migration'] = $this->d6NodeMigrations;
192 $new_nid_process[$delta] = $plugin_configuration;
194 $migration->setProcessOfProperty('nid', $new_nid_process);
196 elseif ($source['plugin'] == 'd6_term_node_revision') {
197 // If the ID mapping is to the underived d6_node_revision migration, replace
198 // it with an expanded list of node migrations.
199 $process = $migration->getProcess();
200 $new_vid_process = [];
201 foreach ($process['vid'] as $delta => $plugin_configuration) {
202 if (in_array($plugin_configuration['plugin'], $this->migrationLookupPluginIds) &&
203 is_string($plugin_configuration['migration']) &&
204 substr($plugin_configuration['migration'], -16) == 'd6_node_revision') {
205 $plugin_configuration['migration'] = $this->d6RevisionMigrations;
207 $new_vid_process[$delta] = $plugin_configuration;
209 $migration->setProcessOfProperty('vid', $new_vid_process);
214 * For D6 file fields, make sure the d6_file migration is prefixed.
216 * @param \Drupal\migrate\Plugin\MigrationInterface $migration
217 * Migration to alter.
219 protected function prefixFileMigration(MigrationInterface $migration) {
220 $process = $migration->getProcess();
221 foreach ($process as $destination => &$plugins) {
222 foreach ($plugins as &$plugin) {
223 if ($plugin['plugin'] == 'd6_field_file') {
224 $plugin['migration'] = $this->modifyId($plugin['migration']);
231 * Run the configured migrations.
233 public function import() {
234 static::$messages = new DrushLogMigrateMessage();
235 if ($this->options['debug']) {
236 \Drupal::service('event_dispatcher')->addListener(MigrateEvents::IDMAP_MESSAGE,
237 [get_class(), 'onIdMapMessage']);
239 foreach ($this->migrationList as $migration_id => $migration) {
240 drush_print(dt('Upgrading @migration', ['@migration' => $migration_id]));
241 $executable = new MigrateExecutable($migration, static::$messages);
242 // drush_op() provides --simulate support.
243 drush_op([$executable, 'import']);
248 * Export the configured migration plugins as configuration entities.
250 public function export() {
251 $db_info = \Drupal::state()->get($this->databaseStateKey);
253 // Create a group to hold the database configuration.
255 'id' => $this->databaseStateKey,
256 'label' => 'Import from Drupal ' . $this->version,
257 'description' => 'Migrations originally generated from drush migrate-upgrade --configure-only',
258 'source_type' => 'Drupal ' . $this->version,
259 'shared_configuration' => [
261 'key' => 'drupal_' . $this->version,
266 // Only add the database connection info to the configuration entity
267 // if it was passed in as a parameter.
268 if (!empty($this->options['legacy-db-url'])) {
269 $group_details['shared_configuration']['source']['database'] = $db_info['database'];
272 // Ditto for the key.
273 if (!empty($this->options['legacy-db-key'])) {
274 $group_details['shared_configuration']['source']['key'] = $this->options['legacy-db-key'];
277 // Load existing migration group and update it with changed settings,
278 // or create a new one if none exists.
279 $group = MigrationGroup::load($group_details['id']);
281 $group = MigrationGroup::create($group_details);
284 $this->setEntityProperties($group, $group_details);
287 foreach ($this->migrationList as $migration_id => $migration) {
288 drush_print(dt('Exporting @migration as @new_migration',
289 ['@migration' => $migration_id, '@new_migration' => $this->modifyId($migration_id)]));
290 $migration_details['id'] = $migration_id;
291 $migration_details['class'] = $migration->get('class');
292 $migration_details['cck_plugin_method'] = $migration->get('cck_plugin_method');
293 $migration_details['field_plugin_method'] = $migration->get('field_plugin_method');
294 $migration_details['migration_group'] = $this->databaseStateKey;
295 $migration_details['migration_tags'] = $migration->get('migration_tags');
296 $migration_details['label'] = $migration->get('label');
297 $migration_details['source'] = $migration->getSourceConfiguration();
298 $migration_details['destination'] = $migration->getDestinationConfiguration();
299 $migration_details['process'] = $migration->get('process');
300 $migration_details['migration_dependencies'] = $migration->getMigrationDependencies();
301 $migration_details = $this->substituteIds($migration_details);
302 $migration_entity = Migration::load($migration_details['id']);
303 if (empty($migration_entity)) {
304 $migration_entity = Migration::create($migration_details);
307 $this->setEntityProperties($migration_entity, $migration_details);
309 $migration_entity->save();
314 * Set entity properties.
316 * @param ConfigEntityInterface $entity
317 * The entity to update.
318 * @param array $properties
319 * The properties to update.
321 protected function setEntityProperties(ConfigEntityInterface $entity, array $properties) {
322 foreach ($properties as $key => $value) {
323 $entity->set($key, $value);
325 foreach ($entity as $property => $value) {
326 // Filter out values not in updated properties.
327 if (!isset($properties[$property])) {
328 $entity->set($property, NULL);
334 * Rewrite any migration plugin IDs so they won't conflict with the core
337 * @param array $entity_array
338 * A configuration array for a migration.
341 * The migration configuration array modified with new IDs.
343 protected function substituteIds(array $entity_array) {
344 $entity_array['id'] = $this->modifyId($entity_array['id']);
345 foreach ($entity_array['migration_dependencies'] as $type => $dependencies) {
346 foreach ($dependencies as $key => $dependency) {
347 $entity_array['migration_dependencies'][$type][$key] = $this->modifyId($dependency);
350 $this->substituteMigrationIds($entity_array['process']);
351 return $entity_array;
355 * Recursively substitute IDs for migration plugins.
357 * @param mixed $process
359 protected function substituteMigrationIds(&$process) {
360 if (is_array($process)) {
361 // We found a migration plugin, change the ID.
362 if (isset($process['plugin']) && in_array($process['plugin'], $this->migrationLookupPluginIds)) {
363 if (is_array($process['migration'])) {
365 foreach ($process['migration'] as $migration) {
366 $new_migration[] = $this->modifyId($migration);
368 $process['migration'] = $new_migration;
371 $process['migration'] = $this->modifyId($process['migration']);
373 // The source_ids configuration for migrate_lookup is keyed by
374 // migration id. If it is there, we need to rekey to the new ids.
375 if (isset($process['source_ids']) && is_array($process['source_ids'])) {
376 $new_source_ids = [];
377 foreach ($process['source_ids'] as $migration_id => $source_ids) {
378 $new_migration_id = $this->modifyId($migration_id);
379 $new_source_ids[$new_migration_id] = $source_ids;
381 $process['source_ids'] = $new_source_ids;
385 // Recurse on each array member.
386 foreach ($process as &$subprocess) {
387 $this->substituteMigrationIds($subprocess);
395 * The original core plugin ID.
398 * The ID modified to serve as a configuration entity ID.
400 protected function modifyId($id) {
401 return $this->options['migration-prefix'] . str_replace(':', '_', $id);
405 * Rolls back the configured migrations.
407 public function rollback() {
408 static::$messages = new DrushLogMigrateMessage();
409 $database_state_key = \Drupal::state()->get('migrate.fallback_state_key');
410 $database_state = \Drupal::state()->get($database_state_key);
411 $db_spec = $database_state['database'];
412 $connection = $this->getConnection($db_spec);
413 $version = $this->getLegacyDrupalVersion($connection);
414 $migrations = $this->getMigrations('migrate_drupal_' . $version, $version);
416 // Roll back in reverse order.
417 $this->migrationList = array_reverse($migrations);
419 foreach ($migrations as $migration) {
420 drush_print(dt('Rolling back @migration', ['@migration' => $migration->id()]));
421 $executable = new MigrateExecutable($migration, static::$messages);
422 // drush_op() provides --simulate support.
423 drush_op([$executable, 'rollback']);
428 * Display any messages being logged to the ID map.
430 * @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
433 public static function onIdMapMessage(MigrateIdMapMessageEvent $event) {
434 if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE ||
435 $event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) {
441 $source_id_string = implode(',', $event->getSourceIdValues());
442 $message = t('Source ID @source_id: @message',
443 ['@source_id' => $source_id_string, '@message' => $event->getMessage()]);
444 static::$messages->display($message, $type);