Drupal Migrate Module Configuration for Content Migration
Drupal Migrate is a built-in ETL system (Extract, Transform, Load) for importing data from any sources: old Drupal versions, WordPress, CSV, XML, JSON, third-party SQL databases. Configured via YAML, extensible with plugins.
Migrate Architecture
Source — where data is read from (CSV, SQL, JSON, Drupal 7 DB) Process — transformation: field mapping, conversion, enrichment Destination — where data is written (Node, Term, User, File, Config)
Each migration is a separate YAML file in config/install/migrate_plus.migration.*.yml.
Installation
composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv
drush en migrate migrate_plus migrate_tools -y
CSV Migration
File config/install/migrate_plus.migration.articles_from_csv.yml:
id: articles_from_csv
label: 'Articles from CSV'
migration_group: content_import
source:
plugin: csv
path: 'public://import/articles.csv'
ids:
- external_id
header_row_count: 1
column_names:
- external_id
- title
- body
- category
- publish_date
- image_url
process:
title: title
'body/value': body
'body/format':
plugin: default_value
default_value: full_html
created:
plugin: format_date
source: publish_date
from_format: 'd.m.Y'
to_format: 'U'
status:
plugin: default_value
default_value: 1
field_category:
plugin: migration_lookup
migration: categories_from_csv
source: category
field_image:
plugin: download
source:
- image_url
- '@filename'
destination:
plugin: 'public://images'
rename: true
destination:
plugin: 'entity:node'
default_bundle: article
migration_dependencies:
required:
- categories_from_csv
Custom Source Plugin
For non-standard sources — custom plugin:
// src/Plugin/migrate/source/ExternalApiSource.php
namespace Drupal\mymodule\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* @MigrateSource(
* id = "external_api",
* source_module = "mymodule"
* )
*/
class ExternalApiSource extends SourcePluginBase {
public function getIds(): array {
return ['id' => ['type' => 'integer']];
}
public function fields(): array {
return [
'id' => 'Record ID',
'title' => 'Title',
'content' => 'Content',
'tags' => 'Tags (comma-separated)',
];
}
protected function initializeIterator(): \Iterator {
$page = 0;
do {
$response = \Drupal::httpClient()->get(
'https://api.external.com/posts?page=' . $page,
['headers' => ['Authorization' => 'Bearer ' . $this->configuration['api_key']]]
);
$data = json_decode($response->getBody(), true);
$items = $data['items'];
foreach ($items as $item) {
yield $item;
}
$page++;
} while (!empty($items) && $page < $data['total_pages']);
}
}
Custom Process Plugin
// src/Plugin/migrate/process/ExtractFirstImage.php
namespace Drupal\mymodule\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(id = "extract_first_image")
*/
class ExtractFirstImage extends ProcessPluginBase {
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): ?string {
if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/', $value, $matches)) {
return $matches[1];
}
return NULL;
}
}
Running and Monitoring
# List available migrations
drush migrate:status
# Run specific migration
drush migrate:import articles_from_csv
# Run all from group
drush migrate:import --group=content_import
# With update of already migrated records
drush migrate:import articles_from_csv --update
# Rollback migration
drush migrate:rollback articles_from_csv
# Status with progress
drush migrate:status --format=table
Error Handling
# View migration errors
drush migrate:messages articles_from_csv
# Skip records with errors and continue
drush migrate:import articles_from_csv --continue-on-failure
Media File Migration
# Step 1: file migration
id: files_migration
source:
plugin: csv
path: 'public://import/files.csv'
process:
filename:
plugin: callback
callable: basename
source: file_url
uri:
plugin: download
source:
- file_url
- '@filename'
destination:
plugin: 'public://migrated'
destination:
plugin: 'entity:file'
# Step 2: in node migration
field_image:
plugin: migration_lookup
migration: files_migration
source: file_id
Incremental Migration
To migrate only new records on re-run, source should track highwater mark:
source:
plugin: csv
path: 'public://import/articles.csv'
ids:
- external_id
track_changes: true # re-migrate on row change
# Or via highwater mark (for dates)
highwaterProperty:
name: updated_at
alias: u
Timeline
Simple CSV migration (500–5000 records) — 2–3 days. Complex migration from multiple sources with custom plugins and transformations — 1–2 weeks.







