| name | drupal-migrations |
| description | Guide for Drupal migrations including content imports, data transformations, and migration debugging. |
Drupal Migrations Skill
Comprehensive guide for building and debugging Drupal migrations.
Migration Basics
Migration Components
- Source Plugin - Where data comes from (CSV, JSON, database, API)
- Process Plugins - Transform data during migration
- Destination Plugin - Where data goes (entity types)
File Structure
modules/custom/mymodule_migrate/
├── mymodule_migrate.info.yml
├── mymodule_migrate.module
├── migrations/
│ ├── mymodule_users.yml
│ ├── mymodule_content.yml
│ └── mymodule_terms.yml
├── src/
│ └── Plugin/
│ └── migrate/
│ ├── source/
│ │ └── CustomSource.php
│ └── process/
│ └── CustomProcess.php
└── config/
└── install/
└── migrate_plus.migration.mymodule_users.yml
Migration YAML Templates
CSV Source
id: mymodule_articles_csv
label: 'Import articles from CSV'
migration_group: mymodule_content
source:
plugin: csv
path: data/imports/articles.csv
ids:
- id
header_row_count: 1
column_names:
0:
id: 'Unique ID'
1:
title: 'Article title'
2:
body: 'Article body'
3:
date: 'Publication date'
process:
type:
plugin: default_value
default_value: article
title: title
body/value: body
body/format:
plugin: default_value
default_value: full_html
created:
plugin: format_date
source: date
from_format: 'Y-m-d'
to_format: 'U'
uid:
plugin: default_value
default_value: 1
destination:
plugin: 'entity:node'
JSON Source
id: mymodule_events_json
label: 'Import events from JSON API'
migration_group: mymodule_content
source:
plugin: url
data_fetcher_plugin: http
data_parser_plugin: json
urls:
- 'https://api.example.com/events'
item_selector: data
ids:
id:
type: integer
fields:
- name: id
label: 'Event ID'
selector: id
- name: title
label: 'Event Title'
selector: attributes/title
- name: start_date
label: 'Start Date'
selector: attributes/start
process:
type:
plugin: default_value
default_value: event
title: title
field_event_date/value:
plugin: format_date
source: start_date
from_format: 'Y-m-d\TH:i:s'
to_format: 'Y-m-d\TH:i:s'
destination:
plugin: 'entity:node'
Database Source (SQL)
id: mymodule_legacy_users
label: 'Import users from legacy database'
migration_group: mymodule_users
source:
plugin: mymodule_legacy_users
process:
name: username
mail: email
status:
plugin: static_map
source: active
map:
1: 1
0: 0
default_value: 0
roles:
plugin: explode
source: role_names
delimiter: ','
destination:
plugin: 'entity:user'
Common Process Plugins
Value Transformation
field_type:
plugin: default_value
default_value: 'article'
field_status:
plugin: static_map
source: status_code
map:
A: 'active'
I: 'inactive'
P: 'pending'
default_value: 'unknown'
field_slug:
plugin: callback
callable: strtolower
source: title
field_clean_title:
- plugin: callback
callable: trim
- plugin: callback
callable: strip_tags
source: raw_title
field_full_name:
plugin: concat
source:
- first_name
- last_name
delimiter: ' '
Date Handling
field_date:
plugin: format_date
source: date_string
from_format: 'm/d/Y'
to_format: 'Y-m-d'
field_created:
plugin: format_date
source: timestamp
from_format: 'U'
to_format: 'Y-m-d\TH:i:s'
field_date:
plugin: format_date
source: date
from_format:
- 'Y-m-d'
- 'm/d/Y'
- 'd.m.Y'
to_format: 'Y-m-d'
Entity References
field_author:
plugin: migration_lookup
migration: mymodule_users
source: author_id
no_stub: true
field_category:
plugin: entity_lookup
entity_type: taxonomy_term
bundle_key: vid
bundle: categories
value_key: name
source: category_name
field_tags:
plugin: entity_generate
entity_type: taxonomy_term
bundle_key: vid
bundle: tags
value_key: name
source: tag_names
Array/Multiple Values
field_tags:
plugin: explode
source: tags_string
delimiter: ','
field_images:
plugin: sub_process
source: images
process:
target_id:
plugin: migration_lookup
migration: mymodule_files
source: file_id
field_optional:
plugin: skip_on_empty
source: optional_field
method: process
Conditional Logic
source_filter:
plugin: skip_on_value
source: status
value: 'deleted'
method: row
field_optional:
plugin: skip_on_empty
source: maybe_empty
method: process
field_count:
plugin: null_coalesce
source:
- count
- '@default_count'
default_value: 0
Custom Source Plugin
<?php
namespace Drupal\mymodule_migrate\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Row;
class LegacyUsers extends SqlBase {
public function query() {
return $this->select('legacy_users', 'u')
->fields('u', [
'id',
'email',
'first_name',
'last_name',
'status',
'created_at',
])
->condition('u.status', 'active');
}
public function fields() {
return [
'id' => $this->t('User ID'),
'email' => $this->t('Email address'),
'first_name' => $this->t('First name'),
'last_name' => $this->t('Last name'),
'status' => $this->t('Status'),
'created_at' => $this->t('Created date'),
];
}
public function getIds() {
return [
'id' => [
'type' => 'integer',
'alias' => 'u',
],
];
}
public function prepareRow(Row $row) {
$email = $row->getSourceProperty('email');
$username = explode('@', $email)[0];
$row->setSourceProperty('username', $username);
$fullName = trim(
$row->getSourceProperty('first_name') . ' ' .
$row->getSourceProperty('last_name')
);
$row->setSourceProperty('full_name', $fullName);
return parent::prepareRow($row);
}
}
Custom Process Plugin
<?php
namespace Drupal\mymodule_migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
class CodeTransform extends ProcessPluginBase {
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (empty($value)) {
return NULL;
}
$prefix = $this->configuration['prefix'] ?? 'NEW';
if (preg_match('/^[A-Z]+-(.+)$/', $value, $matches)) {
$value = $prefix . '-' . $matches[1];
}
return strtoupper($value);
}
}
Drush Commands
./vendor/bin/drush migrate:status
./vendor/bin/drush migrate:import mymodule_users
./vendor/bin/drush migrate:import mymodule_users --limit=100
./vendor/bin/drush migrate:import mymodule_users --feedback=50
./vendor/bin/drush migrate:import mymodule_users --update
./vendor/bin/drush migrate:rollback mymodule_users
./vendor/bin/drush migrate:reset-status mymodule_users
./vendor/bin/drush migrate:messages mymodule_users
./vendor/bin/drush migrate:stop mymodule_users
./vendor/bin/drush migrate:import --group=mymodule_content
./vendor/bin/drush migrate:rollback --group=mymodule_content
Debugging Migrations
Check Source Data
./vendor/bin/drush php:eval "
\$migration = \Drupal::service('plugin.manager.migration')
->createInstance('mymodule_users');
\$source = \$migration->getSourcePlugin();
\$count = 0;
foreach (\$source as \$row) {
print_r(\$row->getSource());
if (++\$count >= 3) break;
}
"
Check Processed Row
./vendor/bin/drush php:eval "
\$migration = \Drupal::service('plugin.manager.migration')
->createInstance('mymodule_users');
\$source = \$migration->getSourcePlugin();
\$executable = new \Drupal\migrate\MigrateExecutable(\$migration);
foreach (\$source as \$row) {
\$executable->processRow(\$row);
print_r(\$row->getDestination());
break;
}
"
View Migration Map
./vendor/bin/drush sql:query "SELECT * FROM migrate_map_mymodule_users LIMIT 10"
./vendor/bin/drush sql:query "SELECT * FROM migrate_map_mymodule_users LIMIT 10"
Common Error Solutions
| Error | Cause | Solution |
|---|
| Source not found | Wrong path/URL | Check path or urls in source |
| Destination entity not found | Missing bundle | Check destination plugin type |
| Process plugin not found | Typo in plugin name | Check plugin ID spelling |
| Required field missing | Empty source | Add skip_on_empty or default |
| Entity reference invalid | Target not migrated | Add migration_lookup with dependency |
Migration Dependencies
id: mymodule_articles
label: 'Articles'
migration_dependencies:
required:
- mymodule_users
- mymodule_categories
optional:
- mymodule_files
process:
uid:
plugin: migration_lookup
migration: mymodule_users
source: author_id