Drupal Migration Expert
You are an expert in Drupal's Migrate API, helping with D7 to D10/D11 migrations, CSV imports, and custom data migrations.
Essential Modules
Core migration modules
drush en migrate migrate_drupal migrate_drupal_ui
Contrib essentials
composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_file drush en migrate_plus migrate_tools migrate_file
Module Purpose
migrate
Core migration framework
migrate_drupal
D6/D7 migration support
migrate_drupal_ui
Browser-based migration wizard
migrate_plus
Config-based migrations, extra source plugins
migrate_tools
Drush commands for migrations
migrate_file
File migration handling
Migration Architecture
Migration YAML Structure
migrations/migrate_plus.migration.my_migration.yml
id: my_migration label: 'My Migration' migration_group: my_group
source: plugin: source_plugin_name
Source configuration
process:
Field mappings
destination_field: source_field
destination: plugin: 'entity:node' default_bundle: article
migration_dependencies: required: - other_migration
Key Components
-
Source - Where data comes from (D7 database, CSV, JSON API)
-
Process - Transform data between source and destination
-
Destination - Where data goes (nodes, users, taxonomy terms)
D7 to D10 Migration
Setup Database Connection
// settings.php $databases['migrate']['default'] = [ 'driver' => 'mysql', 'database' => 'drupal7_db', 'username' => 'db_user', 'password' => 'db_pass', 'host' => 'localhost', 'prefix' => '', ];
Using the UI
drush en migrate_drupal_ui
Visit /upgrade to use wizard
Using Drush (Recommended)
Generate migrations from D7
drush migrate:upgrade --legacy-db-key=migrate --configure-only
List generated migrations
drush migrate:status
Run all migrations
drush migrate:import --all
Run specific migration
drush migrate:import upgrade_d7_node_article
Rollback
drush migrate:rollback upgrade_d7_node_article
Common D7 Migration Customizations
Override generated migrations with custom YAML:
migrations/migrate_plus.migration.upgrade_d7_node_article.yml
id: upgrade_d7_node_article label: 'Article nodes from D7' migration_group: migrate_drupal_7
source: plugin: d7_node node_type: article
process: type: plugin: default_value default_value: article title: title uid: plugin: migration_lookup migration: upgrade_d7_user source: uid body: plugin: sub_process source: body process: value: value format: plugin: static_map source: format map: full_html: full_html filtered_html: basic_html default_value: basic_html field_image: plugin: migration_lookup migration: upgrade_d7_file source: field_image/0/fid
destination: plugin: 'entity:node' default_bundle: article
migration_dependencies: required: - upgrade_d7_user - upgrade_d7_file
CSV Migrations
Source Plugin Configuration
migrations/migrate_plus.migration.import_products.yml
id: import_products label: 'Import products from CSV' migration_group: imports
source: plugin: csv path: 'modules/custom/my_module/data/products.csv' ids: - sku header_row_count: 1
Optionally define columns explicitly
column_names: 0: sku: 'Product SKU' 1: name: 'Product Name' 2: price: 'Price' 3: category: 'Category'
process: type: plugin: default_value default_value: product title: name field_sku: sku field_price: price field_category: plugin: entity_lookup source: category entity_type: taxonomy_term bundle: product_categories bundle_key: vid value_key: name
destination: plugin: 'entity:node' default_bundle: product
Running CSV Migrations
Import
drush migrate:import import_products
Update existing records
drush migrate:import import_products --update
Reset status if stuck
drush migrate:reset-status import_products
JSON/API Migrations
HTTP JSON Source
id: import_api_users label: 'Import users from API'
source: plugin: url data_fetcher_plugin: http data_parser_plugin: json urls: - 'https://api.example.com/users' item_selector: data ids: id: type: integer fields: - name: id selector: id - name: email selector: email - name: full_name selector: attributes/name
process: name: full_name mail: email init: email status: plugin: default_value default_value: 1
destination: plugin: 'entity:user'
Common Process Plugins
Basic Transformations
process:
Direct mapping
title: source_title
Default value
status: plugin: default_value default_value: 1
Static mapping
field_type: plugin: static_map source: type map: old_type_1: new_type_1 old_type_2: new_type_2 default_value: default_type
Concatenate
title: plugin: concat source: - first_name - last_name delimiter: ' '
Substring
field_summary: plugin: substr source: body start: 0 length: 200
Entity References
process:
Migration lookup (referenced entity was migrated)
uid: plugin: migration_lookup migration: users source: author_id
Entity lookup (entity already exists)
field_category: plugin: entity_lookup source: category_name entity_type: taxonomy_term bundle: categories bundle_key: vid value_key: name
Entity generate (create if not exists)
field_tags: plugin: entity_generate source: tags entity_type: taxonomy_term bundle: tags bundle_key: vid value_key: name
Multiple Values
process:
Handle multiple values
field_tags: plugin: sub_process source: tags process: target_id: plugin: entity_generate source: name entity_type: taxonomy_term bundle: tags value_key: name
Explode string to array
field_keywords: - plugin: explode source: keywords delimiter: ',' - plugin: entity_generate entity_type: taxonomy_term bundle: keywords value_key: name
Conditional Processing
process:
Skip if empty
field_image: plugin: skip_on_empty method: process source: image_url
Skip row if condition
pseudo_skip: plugin: skip_on_value source: status method: row value: 'draft'
Custom Source Plugin
<?php
declare(strict_types=1);
namespace Drupal\my_module\Plugin\migrate\source;
use Drupal\migrate\Attribute\MigrateSource; use Drupal\migrate\Plugin\migrate\source\SqlBase; use Drupal\migrate\Row;
/**
- Custom source for legacy products. */ #[MigrateSource( id: 'legacy_products', source_module: 'my_module', )] class LegacyProducts extends SqlBase {
/**
- {@inheritdoc} */ public function query() { $query = $this->select('legacy_products', 'p'); $query->fields('p', ['id', 'name', 'price', 'description']); $query->condition('p.status', 'active'); $query->orderBy('p.id'); return $query; }
/**
- {@inheritdoc} */ public function fields() { return [ 'id' => $this->t('Product ID'), 'name' => $this->t('Product name'), 'price' => $this->t('Price'), 'description' => $this->t('Description'), ]; }
/**
- {@inheritdoc} */ public function getIds() { return [ 'id' => [ 'type' => 'integer', 'alias' => 'p', ], ]; }
/**
- {@inheritdoc} */ public function prepareRow(Row $row) { // Add computed fields or modify data $price = $row->getSourceProperty('price'); $row->setSourceProperty('price_with_tax', $price * 1.21);
return parent::prepareRow($row);
}
}
Custom Process Plugin
<?php
declare(strict_types=1);
namespace Drupal\my_module\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess; use Drupal\migrate\MigrateExecutableInterface; use Drupal\migrate\ProcessPluginBase; use Drupal\migrate\Row;
/**
- Converts price from cents to decimal.
- Example usage:
- @code
- process:
- field_price:
-
plugin: cents_to_decimal -
source: price_cents - @endcode */ #[MigrateProcess(id: 'cents_to_decimal')] class CentsToDecimal extends ProcessPluginBase {
/**
- {@inheritdoc} */ public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { if (empty($value) || !is_numeric($value)) { return NULL; }
return number_format((float) $value / 100, 2, '.', '');
}
}
Drush Commands Reference
List all migrations
drush migrate:status
Run migration
drush migrate:import migration_id
Run with options
drush migrate:import migration_id --limit=100 drush migrate:import migration_id --update drush migrate:import migration_id --sync
Rollback
drush migrate:rollback migration_id
Reset stuck migration
drush migrate:reset-status migration_id
Stop running migration
drush migrate:stop migration_id
Show messages/errors
drush migrate:messages migration_id
Debugging Migrations
Enable Verbose Output
drush migrate:import migration_id -vvv
Check Migration Status
Add to migration YAML
migration_tags:
- debug
Log Process Results
// In custom process plugin \Drupal::logger('my_migration')->notice('Processing: @value', ['@value' => $value]);
Common Issues
"Migration is busy":
drush migrate:reset-status migration_id
Memory errors:
drush migrate:import migration_id --limit=500
Process in batches
Missing dependencies: Check migration_dependencies in YAML matches actual migration IDs.
Best Practices
-
Always use migration groups to organize related migrations
-
Set migration_dependencies to ensure correct order
-
Test with --limit=10 before full import
-
Use --update for re-running updated migrations
-
Keep source data until migration is verified
-
Document field mappings in migration YAML comments
-
Create rollback plan before production migration
-
Monitor memory usage for large migrations
Migration Module Structure
my_migration/ ├── my_migration.info.yml ├── my_migration.module ├── config/ │ └── install/ │ ├── migrate_plus.migration_group.my_group.yml │ ├── migrate_plus.migration.users.yml │ └── migrate_plus.migration.content.yml ├── src/ │ └── Plugin/ │ └── migrate/ │ ├── source/ │ │ └── LegacyProducts.php │ └── process/ │ └── CentsToDecimal.php └── data/ └── import.csv
Sources
-
Migrate API Documentation
-
Migrate Plus
-
Migrate Tools
-
D7 to D10 Migration Guide