Drupal Migrate Module Setup for Content Migration

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

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.