5 * Command-line tools to aid performing and developing migrations.
8 use Drupal\Component\Utility\Unicode;
9 use Drupal\migrate\Exception\RequirementsException;
10 use Drupal\migrate\Plugin\MigrationInterface;
11 use Drupal\migrate\Plugin\RequirementsInterface;
12 use Drupal\migrate_plus\Entity\MigrationGroup;
13 use Drupal\migrate_tools\DrushLogMigrateMessage;
14 use Drupal\migrate_tools\MigrateExecutable;
17 * Implements hook_drush_command().
19 function migrate_tools_drush_command() {
20 $items['migrate-status'] = [
21 'description' => 'List all migrations with current status.',
23 'group' => 'A comma-separated list of migration groups to list',
24 'tag' => 'Name of the migration tag to list',
25 'names-only' => 'Only return names, not all the details (faster)',
28 'migration' => 'Restrict to a comma-separated list of migrations. Optional',
31 'migrate-status' => 'Retrieve status for all migrations',
32 'migrate-status --group=beer' => 'Retrieve status for all migrations in a given group',
33 'migrate-status --tag=user' => 'Retrieve status for all migrations with a given tag',
34 'migrate-status --group=beer --tag=user' => 'Retrieve status for all migrations in the beer group and with the user tag',
35 'migrate-status beer_term,beer_node' => 'Retrieve status for specific migrations',
37 'drupal dependencies' => ['migrate_tools'],
41 $items['migrate-import'] = [
42 'description' => 'Perform one or more migration processes.',
44 'all' => 'Process all migrations.',
45 'group' => 'A comma-separated list of migration groups to import',
46 'tag' => 'Name of the migration tag to import',
47 'limit' => 'Limit on the number of items to process in each migration',
48 'feedback' => 'Frequency of progress messages, in items processed',
49 'idlist' => 'Comma-separated list of IDs to import',
50 'update' => ' In addition to processing unprocessed items from the source, update previously-imported items with the current data',
51 'force' => 'Force an operation to run, even if all dependencies are not satisfied',
52 'execute-dependencies' => 'Execute all dependent migrations first.',
55 'migration' => 'ID of migration(s) to import. Delimit multiple using commas.',
58 'migrate-import --all' => 'Perform all migrations',
59 'migrate-import --group=beer' => 'Import all migrations in the beer group',
60 'migrate-import --tag=user' => 'Import all migrations with the user tag',
61 'migrate-import --group=beer --tag=user' => 'Import all migrations in the beer group and with the user tag',
62 'migrate-import beer_term,beer_node' => 'Import new terms and nodes',
63 'migrate-import beer_user --limit=2' => 'Import no more than 2 users',
64 'migrate-import beer_user --idlist=5' => 'Import the user record with source ID 5',
66 'drupal dependencies' => ['migrate_tools'],
67 'aliases' => ['mi', 'mim'],
70 $items['migrate-rollback'] = [
71 'description' => 'Rollback one or more migrations.',
73 'all' => 'Process all migrations.',
74 'group' => 'A comma-separated list of migration groups to rollback',
75 'tag' => 'ID of the migration tag to rollback',
76 'feedback' => 'Frequency of progress messages, in items processed',
79 'migration' => 'Name of migration(s) to rollback. Delimit multiple using commas.',
82 'migrate-rollback --all' => 'Perform all migrations',
83 'migrate-rollback --group=beer' => 'Rollback all migrations in the beer group',
84 'migrate-rollback --tag=user' => 'Rollback all migrations with the user tag',
85 'migrate-rollback --group=beer --tag=user' => 'Rollback all migrations in the beer group and with the user tag',
86 'migrate-rollback beer_term,beer_node' => 'Rollback imported terms and nodes',
88 'drupal dependencies' => ['migrate_tools'],
92 $items['migrate-stop'] = [
93 'description' => 'Stop an active migration operation.',
95 'migration' => 'ID of migration to stop',
97 'drupal dependencies' => ['migrate_tools'],
101 $items['migrate-reset-status'] = [
102 'description' => 'Reset a active migration\'s status to idle.',
104 'migration' => 'ID of migration to reset',
106 'drupal dependencies' => ['migrate_tools'],
107 'aliases' => ['mrs'],
110 $items['migrate-messages'] = [
111 'description' => 'View any messages associated with a migration.',
113 'migration' => 'ID of the migration',
116 'csv' => 'Export messages as a CSV',
119 'migrate-messages MyNode' => 'Show all messages for the MyNode migration',
121 'drupal dependencies' => ['migrate_tools'],
122 'aliases' => ['mmsg'],
125 $items['migrate-fields-source'] = [
126 'description' => 'List the fields available for mapping in a source.',
128 'migration' => 'ID of the migration',
131 'migrate-fields-source my_node' => 'List fields for the source in the my_node migration',
133 'drupal dependencies' => ['migrate_tools'],
134 'aliases' => ['mfs'],
141 * Display migration status.
143 * @param string $migration_names
144 * The migration names.
146 function drush_migrate_tools_migrate_status($migration_names = '') {
147 $names_only = drush_get_option('names-only');
149 $migrations = drush_migrate_tools_migration_list($migration_names);
152 // Take it one group at a time, listing the migrations within each group.
153 foreach ($migrations as $group_id => $migration_list) {
154 $group = MigrationGroup::load($group_id);
155 $group_name = !empty($group) ? "{$group->label()} ({$group->id()})" : $group_id;
158 dt('Group: @name', ['@name' => $group_name]),
163 dt('Group: @name', ['@name' => $group_name]),
171 foreach ($migration_list as $migration_id => $migration) {
173 $map = $migration->getIdMap();
174 $imported = $map->importedCount();
175 $source_plugin = $migration->getSourcePlugin();
177 catch (Exception $e) {
178 drush_log(dt('Failure retrieving information on @migration: @message',
179 ['@migration' => $migration_id, '@message' => $e->getMessage()]));
183 $table[] = [$migration_id];
187 $source_rows = $source_plugin->count();
188 // -1 indicates uncountable sources.
189 if ($source_rows == -1) {
190 $source_rows = dt('N/A');
191 $unprocessed = dt('N/A');
194 $unprocessed = $source_rows - $map->processedCount();
197 catch (Exception $e) {
198 drush_print($e->getMessage());
199 drush_log(dt('Could not retrieve source count from @migration: @message',
200 ['@migration' => $migration_id, '@message' => $e->getMessage()]));
201 $source_rows = dt('N/A');
202 $unprocessed = dt('N/A');
205 $status = $migration->getStatusLabel();
206 $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported');
207 $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE);
208 if ($last_imported) {
209 /** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */
210 $date_formatter = \Drupal::service('date.formatter');
211 $last_imported = $date_formatter->format($last_imported / 1000,
212 'custom', 'Y-m-d H:i:s');
228 drush_print_table($table);
232 * Import a migration.
234 * @param string $migration_names
235 * The migration names.
237 function drush_migrate_tools_migrate_import($migration_names = '') {
238 $group_names = drush_get_option('group');
239 $tag_names = drush_get_option('tag');
240 $all = drush_get_option('all');
242 // Display a depreciation message if "mi" alias is used.
243 $args = drush_get_arguments();
244 if ($args[0] === 'mi') {
245 drush_log('The \'mi\' alias is deprecated and will no longer work with Drush 9. Consider the use of \'mim\' alias instead.', 'warning');
249 if (!$all && !$group_names && !$migration_names && !$tag_names) {
250 drush_set_error('MIGRATE_ERROR', dt('You must specify --all, --group, --tag or one or more migration names separated by commas'));
254 foreach (['limit', 'feedback', 'idlist', 'update', 'force'] as $option) {
255 if (drush_get_option($option)) {
256 $options[$option] = drush_get_option($option);
260 $migrations = drush_migrate_tools_migration_list($migration_names);
261 if (empty($migrations)) {
262 drush_log(dt('No migrations found.'), 'error');
265 // Take it one group at a time, importing the migrations within each group.
266 foreach ($migrations as $group_id => $migration_list) {
267 array_walk($migration_list, '_drush_migrate_tools_execute_migration', $options);
272 * Executes a single migration.
274 * If the --execute-dependencies option was given, the migration's dependencies
275 * will also be executed first.
277 * @param \Drupal\migrate\Plugin\MigrationInterface $migration
278 * The migration to execute.
279 * @param string $migration_id
280 * The migration ID (not used, just an artifact of array_walk()).
281 * @param array $options
282 * Additional options for the migration.
284 function _drush_migrate_tools_execute_migration(MigrationInterface $migration, $migration_id, array $options = []) {
285 $log = new DrushLogMigrateMessage();
287 if (drush_get_option('execute-dependencies')) {
288 if ($required_IDS = $migration->get('requirements')) {
289 $manager = \Drupal::service('plugin.manager.migration');
290 $required_migrations = $manager->createInstances($required_IDS);
291 $dependency_options = array_merge($options, ['is_dependency' => TRUE]);
292 array_walk($required_migrations, __FUNCTION__, $dependency_options);
295 if (!empty($options['force'])) {
296 $migration->set('requirements', []);
298 if (!empty($options['update'])) {
299 $migration->getIdMap()->prepareUpdate();
301 $executable = new MigrateExecutable($migration, $log, $options);
302 // Function drush_op() provides --simulate support.
303 drush_op([$executable, 'import']);
304 if ($count = $executable->getFailedCount()) {
305 // Nudge Drush to use a non-zero exit code.
306 drush_set_error('MIGRATE_ERROR', dt('!name Migration - !count failed.', [
307 '!name' => $migration_id,
314 * Rollback migrations.
316 * @param string $migration_names
317 * The migration names.
319 function drush_migrate_tools_migrate_rollback($migration_names = '') {
320 $group_names = drush_get_option('group');
321 $tag_names = drush_get_option('tag');
322 $all = drush_get_option('all');
324 if (!$all && !$group_names && !$migration_names && !$tag_names) {
325 drush_set_error('MIGRATE_ERROR', dt('You must specify --all, --group, --tag, or one or more migration names separated by commas'));
329 if (drush_get_option('feedback')) {
330 $options['feedback'] = drush_get_option('feedback');
333 $log = new DrushLogMigrateMessage();
335 $migrations = drush_migrate_tools_migration_list($migration_names);
336 if (empty($migrations)) {
337 drush_log(dt('No migrations found.'), 'error');
340 // Take it one group at a time, rolling back the migrations within each group.
341 foreach ($migrations as $group_id => $migration_list) {
342 // Roll back in reverse order.
343 $migration_list = array_reverse($migration_list);
344 foreach ($migration_list as $migration_id => $migration) {
345 $executable = new MigrateExecutable($migration, $log, $options);
346 // drush_op() provides --simulate support.
347 drush_op([$executable, 'rollback']);
355 * @param string $migration_id
358 function drush_migrate_tools_migrate_stop($migration_id = '') {
359 /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
360 $migration = \Drupal::service('plugin.manager.migration')
361 ->createInstance($migration_id);
363 $status = $migration->getStatus();
365 case MigrationInterface::STATUS_IDLE:
366 drush_log(dt('Migration @id is idle', ['@id' => $migration_id]), 'warning');
368 case MigrationInterface::STATUS_DISABLED:
369 drush_log(dt('Migration @id is disabled', ['@id' => $migration_id]), 'warning');
371 case MigrationInterface::STATUS_STOPPING:
372 drush_log(dt('Migration @id is already stopping', ['@id' => $migration_id]), 'warning');
375 $migration->interruptMigration(MigrationInterface::RESULT_STOPPED);
376 drush_log(dt('Migration @id requested to stop', ['@id' => $migration_id]), 'success');
381 drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error');
388 * @param string $migration_id
391 function drush_migrate_tools_migrate_reset_status($migration_id = '') {
392 /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
393 $migration = \Drupal::service('plugin.manager.migration')
394 ->createInstance($migration_id);
396 $status = $migration->getStatus();
397 if ($status == MigrationInterface::STATUS_IDLE) {
398 drush_log(dt('Migration @id is already Idle', ['@id' => $migration_id]), 'warning');
401 $migration->setStatus(MigrationInterface::STATUS_IDLE);
402 drush_log(dt('Migration @id reset to Idle', ['@id' => $migration_id]), 'status');
406 drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error');
413 * @param string $migration_id
416 function drush_migrate_tools_migrate_messages($migration_id) {
417 /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
418 $migration = \Drupal::service('plugin.manager.migration')
419 ->createInstance($migration_id);
421 $map = $migration->getIdMap();
424 foreach ($map->getMessageIterator() as $row) {
427 // @todo: Ideally, replace sourceid* with source key names. Or, should
428 // getMessageIterator() do that?
429 foreach ($row as $column => $value) {
430 $table[0][] = $column;
434 $table[] = (array) $row;
437 drush_log(dt('No messages for this migration'), 'status');
440 if (drush_get_option('csv')) {
441 foreach ($table as $row) {
442 fputcsv(STDOUT, $row);
447 foreach ($table[0] as $header) {
448 $widths[] = strlen($header) + 1;
450 drush_print_table($table, TRUE, $widths);
455 drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error');
460 * Print source fields.
462 * @param string $migration_id
465 function drush_migrate_tools_migrate_fields_source($migration_id) {
466 /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
467 $migration = \Drupal::service('plugin.manager.migration')
468 ->createInstance($migration_id);
470 $source = $migration->getSourcePlugin();
472 foreach ($source->fields() as $machine_name => $description) {
473 $table[] = [strip_tags($description), $machine_name];
475 drush_print_table($table);
478 drush_log(dt('Migration @id does not exist', ['@id' => $migration_id]), 'error');
483 * Retrieve a list of active migrations.
485 * @param string $migration_ids
486 * Comma-separated list of migrations - if present, return only these
489 * @return \Drupal\migrate\Plugin\MigrationInterface[][]
490 * An array keyed by migration group, each value containing an array of
491 * migrations or an empty array if no migrations match the input criteria.
493 function drush_migrate_tools_migration_list($migration_ids = '') {
494 // Filter keys must match the migration configuration property name.
495 $filter['migration_group'] = drush_get_option('group') ? explode(',', drush_get_option('group')) : [];
496 $filter['migration_tags'] = drush_get_option('tag') ? explode(',', drush_get_option('tag')) : [];
498 $manager = \Drupal::service('plugin.manager.migration');
499 $plugins = $manager->createInstances([]);
500 $matched_migrations = [];
502 // Get the set of migrations that may be filtered.
503 if (empty($migration_ids)) {
504 $matched_migrations = $plugins;
507 // Get the requested migrations.
508 $migration_ids = explode(',', Unicode::strtolower($migration_ids));
509 foreach ($plugins as $id => $migration) {
510 if (in_array(Unicode::strtolower($id), $migration_ids)) {
511 $matched_migrations[$id] = $migration;
516 // Do not return any migrations which fail to meet requirements.
517 /** @var \Drupal\migrate\Plugin\Migration $migration */
518 foreach ($matched_migrations as $id => $migration) {
519 if ($migration->getSourcePlugin() instanceof RequirementsInterface) {
521 $migration->getSourcePlugin()->checkRequirements();
523 catch (RequirementsException $e) {
524 unset($matched_migrations[$id]);
529 // Filters the matched migrations if a group or a tag has been input.
530 if (!empty($filter['migration_group']) || !empty($filter['migration_tags'])) {
531 // Get migrations in any of the specified groups and with any of the
533 foreach ($filter as $property => $values) {
534 if (!empty($values)) {
535 $filtered_migrations = [];
536 foreach ($values as $search_value) {
537 foreach ($matched_migrations as $id => $migration) {
538 // Cast to array because migration_tags can be an array.
539 $configured_values = (array) $migration->get($property);
540 $configured_id = (in_array($search_value, $configured_values)) ? $search_value : 'default';
541 if (empty($search_value) || $search_value == $configured_id) {
542 if (empty($migration_ids) || in_array(Unicode::strtolower($id), $migration_ids)) {
543 $filtered_migrations[$id] = $migration;
548 $matched_migrations = $filtered_migrations;
553 // Sort the matched migrations by group.
554 if (!empty($matched_migrations)) {
555 foreach ($matched_migrations as $id => $migration) {
556 $configured_group_id = empty($migration->get('migration_group')) ? 'default' : $migration->get('migration_group');
557 $migrations[$configured_group_id][$id] = $migration;
560 return isset($migrations) ? $migrations : [];