Drupal Migrate API — Migrating Content from WordPress and Drupal
In this tutorial, you'll learn how Drupal's Migrate API works: migrating content from WordPress to Drupal, performing Drupal-to-Drupal upgrades, configuring source plugins for different data formats, using Process plugins for data transformation, and writing Migration YAML files.
What You'll Learn
- What the Migrate API is and how it powers Drupal migrations
- The core migrate modules: migrate, migrate_drupal, migrate_drupal_ui
- Migrating from WordPress using the WordPress Migrate module
- Migration structure: source, process, and destination
- Source plugins: EmbeddedSource, CSV, Database, File, URL, Xml
- Process plugins: get, concat, explode, extract, dedupe_entity, skip_on_empty, callback, default_value
- Destination plugins: entity:node, entity:user, entity:taxonomy_term, config:language
- Migration YAML structure: id, label, source, process, destination
- Running migrations with Drush: import, status, rollback
- Migration groups and dependencies
- Content transformation and field mapping
Why It Matters
Most Drupal projects involve migrating data from another platform. A client might have 10,000 blog posts in WordPress, 5,000 users in a custom CRM, or a legacy Drupal 7 site with years of content. Manually recreating this content is impractical. The Migrate API handles the transformation, deduplication, and validation automatically. It is the difference between a two-week migration project and a six-month manual copy-paste nightmare.
Real-World Use
A university with 15 years of content on WordPress decides to migrate to Drupal for better content modeling and security. They have 8,000 posts, 2,000 media files, 500 users, and 50 categories. Using the WordPress Migrate module, they write custom migrations that map WordPress posts to Drupal nodes, WordPress categories to Drupal taxonomy terms, and WordPress users to Drupal users. The migration runs in 30 minutes. Automated rollback lets them test repeatedly until the mapping is perfect.
Learning Path
flowchart LR A[Configuration Management] --> B[Migrate API] B --> C[Migrate Concepts] C --> D[Source Plugins] D --> E[Process Plugins] E --> F[Destination Plugins] F --> G[Migration YAML] G --> H[Running Migrations] H --> I[Performance and Go-Live]
What is the Migrate API?
The Migrate API is a framework for importing data into Drupal. It reads data from a source, transforms it through a series of process steps, and writes it to a destination.
Source (CSV, database, XML, JSON)
|
v
Process (transform, map, filter, deduplicate)
|
v
Destination (Drupal entities, configuration)
The API is in Drupal core, so no additional modules are required for basic migrations.
Migrate Modules
# Core migrate modules
drush pm:enable migrate # Core migration framework
drush pm:enable migrate_drupal # Drupal-to-Drupal migration
drush pm:enable migrate_drupal_ui # Web UI for Drupal migrations
# Contributed modules
composer require drupal/migrate_plus # Additional source/process plugins
composer require drupal/migrate_tools # Drush commands for migrations
composer require drupal/migrate_source_csv # CSV source plugin
composer require drupal/wordpress_migrate # WordPress import
Migration YAML Structure
Every migration is defined by a YAML file. Here is a typical structure:
# migrations/migrate_plus.migration.import_articles.yml
id: import_articles
label: 'Import articles from WordPress'
migration_group: wordpress
migration_tags:
- wordpress
- content
source:
plugin: wordpress_post
constants:
uid: 1
status: 1
process:
type:
plugin: default_value
default_value: article
title: title
body/value: body
body/format:
plugin: default_value
default_value: basic_html
field_tags:
plugin: migration_lookup
migration: import_tags
source: tags
uid: constants/uid
status: constants/status
created: post_date
changed: post_modified
destination:
plugin: entity:node
dependencies:
enforced:
module:
- migrate_plus
- wordpress_migrate
Migration Anatomy
- id: Unique machine name for the migration
- label: Human-readable description
- source: Defines where data comes from and how to read it
- process: Maps source fields to destination fields with optional transformations
- destination: Defines what to create (node, user, taxonomy term)
- dependencies: Other migrations that must run first
Source Plugins
Source plugins read data from various formats:
# CSV source
source:
plugin: csv
path: /var/www/migrations/users.csv
header_row_count: 1
keys:
- id
fields:
id: 'User ID'
name: 'Username'
email: 'Email address'
# Database source
source:
plugin: d7_node
node_type: article
# Embedded source (hardcoded data)
source:
plugin: embedded_data
data_rows:
-
id: 1
name: 'Static Page'
-
id: 2
name: 'About Us'
ids:
id:
type: integer
# URL/JSON source
source:
plugin: url
data_fetcher_plugin: http
data_parser_plugin: json
urls: 'https://api.example.com/posts'
item_selector: /posts
fields:
-
name: title
label: Title
selector: title
-
name: body
label: Body
selector: content
ids:
title:
type: string
Process Plugins
Process plugins transform source data before saving to the destination:
process:
# Direct mapping (source to destination as-is)
title: source_title
# Default value if source is empty
type:
plugin: default_value
default_value: article
# Concat multiple fields
field_full_name:
plugin: concat
source:
- first_name
- last_name
delimiter: ' '
# Skip empty values
field_summary:
plugin: skip_on_empty
method: process
source: excerpt
# Callback (PHP function)
created:
plugin: callback
callable: strtotime
source: post_date
# Explode a string into an array
field_tags:
plugin: explode
source: tags_string
delimiter: ','
# Extract a value from an array
field_coordinates:
plugin: extract
source: location
index:
- lat
- lng
# Reference another migration
field_author:
plugin: migration_lookup
migration: import_users
source: author_id
# Deduplicate entities
field_category:
plugin: dedupe_entity
entity_type: taxonomy_term
field: name
source: category_name
# Static map (replace values)
field_region:
plugin: static_map
source: region_code
map:
NE: 'Northeast'
SE: 'Southeast'
MW: 'Midwest'
W: 'West'
bypass: true
Destination Plugins
Destination plugins define where the processed data is saved:
# Create nodes
destination:
plugin: entity:node
# Create users
destination:
plugin: entity:user
# Create taxonomy terms
destination:
plugin: entity:taxonomy_term
# Create configuration
destination:
plugin: config:language
# Entity:node with bundle specified in process
process:
type:
plugin: default_value
default_value: article
Running Migrations with Drush
# List all migrations
drush migrate:status
# Show status of a specific migration
drush migrate:status import_articles
# Run a specific migration
drush migrate:import import_articles
# Run all migrations in a group
drush migrate:import wordpress
# Import with a limit (for testing)
drush migrate:import import_articles --limit=10
# Run migrations in parallel
drush migrate:import import_articles import_users --execute-dependencies
# Rollback a migration (removes imported data)
drush migrate:rollback import_articles
# Rollback all migrations in a group
drush migrate:rollback wordpress
# Show messages from a migration
drush migrate:messages import_articles
# Reset a stalled migration
drush migrate:reset import_articles
Migration Groups and Dependencies
Migrations can depend on each other:
# migrations/migrate_plus.migration.import_tags.yml
id: import_tags
label: 'Import WordPress categories as taxonomy terms'
migration_group: wordpress
source:
plugin: wordpress_category
destination:
plugin: entity:taxonomy_term
default_bundle: tags
# migrations/migrate_plus.migration.import_articles.yml
id: import_articles
label: 'Import WordPress posts as articles'
migration_group: wordpress
source:
plugin: wordpress_post
process:
field_tags:
plugin: migration_lookup
migration: import_tags # This migration must run first
source: categories
destination:
plugin: entity:node
default_bundle: article
# Dependencies ensure correct order
# 1. import_tags runs first (creates taxonomy terms)
# 2. import_articles runs second (references the terms)
Custom Process Plugin
Create custom process logic:
<?php
namespace Drupal\my_module\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipProcessException;
/**
* Generate a URL-safe slug from a string.
*
* @MigrateProcessPlugin(
* id = "my_slugify"
* )
*/
class Slugify extends ProcessPluginBase {
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (empty($value)) {
throw new MigrateSkipProcessException();
}
// Convert to lowercase, replace spaces with hyphens,
// remove special characters
$slug = mb_strtolower($value);
$slug = preg_replace('/[^a-z0-9-]+/', '-', $slug);
$slug = trim($slug, '-');
return $slug;
}
}
# Using the custom plugin
process:
field_slug:
plugin: my_slugify
source: title
Content Transformation Strategies
Mapping Field Values
# WordPress post type to Drupal content type
process:
type:
plugin: static_map
source: post_type
map:
post: article
page: page
news: news
bypass: true # Skip if no match
Combining Fields
# Combine first/last name into full name
process:
field_full_name:
plugin: concat
source:
- first_name
- last_name
delimiter: ' '
# Combine address fields
process:
field_address:
plugin: concat
source:
- street
- city
- zip
delimiter: ', '
Date Conversion
# WordPress stores dates differently from Drupal
process:
created:
plugin: callback
callable: strtotime
source: post_date_gmt
Common Mistakes
Running migrations without testing on a copy first: Always test migrations on a staging environment. Running directly on production can corrupt data or create duplicates.
Not setting migration dependencies: If import_articles depends on import_tags but you do not declare the dependency, the articles migration might fail trying to reference tags that do not exist yet.
Using wrong source field names: Migration YAML source fields must match the actual data columns. A typo like
titlleinstead oftitlesilently produces empty fields.Forgetting to map required fields: If a destination field is required (like
titleortype) and you do not map a source field to it, the migration fails.Not using skip_on_empty for optional fields: If a source field is empty for some rows, the migration might fail. Use
skip_on_emptyto skip the field when the source value is missing.
Practice Questions
- What is the purpose of
migration_lookupin a process plugin, and when would you use it? - A WordPress post has a comma-separated tags field like "news, updates, announcements". How do you map this to a Drupal entity reference field?
- What happens when you run
drush migrate:rollbackon a migration that other migrations depend on? - Challenge: Create a complete migration YAML file that reads from a CSV file containing: title, body, author_name, category, post_date, image_url. Map these to: a node of type "article", a user (create if not exists), a taxonomy term "category", and a media image (download from URL). Include at least three process plugins (migration_lookup, callback, skip_on_empty, default_value, concat).
FAQ
Mini Project
Goal: Migrate a WordPress site to Drupal using the Migrate API.
- Export your WordPress content as XML (Tools > Export)
- Set up a fresh Drupal installation
- Install the WordPress Migrate module and its dependencies
- Create a custom migration module that:
- Migrates WordPress posts to Drupal articles
- Maps WordPress categories to Drupal taxonomy terms
- Maps WordPress tags to Drupal tags
- Maps WordPress authors to Drupal users (create new if missing)
- Downloads featured images and attaches them to articles
- Run the migration and verify: article count, taxonomy terms, user accounts, media files
- Write a YAML migration that imports additional data from a CSV file containing user ratings for each post
What's Next
Now that you understand migrations, proceed to performance optimization to learn Caching strategies, CDN integration, and server tuning. Then explore backups and maintenance to keep your Drupal site healthy.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro