How to Migrate Paragraphs from Drupal 7 to Drupal 8
A few days ago, we encountered an exciting challenge: migrating paragraphs from a Drupal 7 platform to Drupal 8. While searching for the right solution, we scoured the web but found nothing that perfectly fit our needs. As a result, our backend developers created a custom solution. Special thanks to mtech-llc.com and Ada Hernández, whose work on field collection migration inspired this solution.
While Ada's original article describes migrating field collections, we adapted the approach for paragraphs migration. Her insights proved invaluable to our process.
Before proceeding, ensure you have:
1. A functional Drupal 8 installation
2. The following modules installed:
- migrate_drupal
- migrate_plus
- migrate_tools
- migrate_drupal_ui
Alternatively, you can add these as dependencies to your custom module.
After installing the required modules, create a paragraph with bundle "contact" containing these fields:
- Text plain (machine name: field_name)
- Email (machine name: field_email)
- Number (Integer) (machine name: field_phone)
Then create a content type called Organization (machine name: organization) with:
- Default body field
- Field paragraph type (machine name: field_contact)
This procedure is complex and requires careful attention to detail. If errors occur during any step, you may need to start over. Please read each step thoroughly before proceeding.
1. Specify Database Connection in settings.php
Add the second connection to Drupal 7 in your settings.php file. In this example, the database name is "drupal_7_56":
$databases['migrate']['default'] = array(
'database' => 'drupal_7_56',
'username' => 'root',
'password' => '',
'prefix' => '',
'host' => '127.0.0.1',
'port' => '33067',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
);
2. Create a Custom Module
Create a custom module with the machine name "custom".
3. Create YML Templates
Place the following YML templates in the config/install folder:
First, create the paragraph migration template (filename: migrate_plus.migration.d7_paragraph_contact.yml):
langcode: en
status: true
dependencies: {}
id: d7_paragraph_contact
class: null
field_plugin_method: null
cck_plugin_method: null
migration_tags:
- 'Drupal 7'
migration_group: migrate_drupal_7
label: Contacts
source:
plugin: d7_paragraph_item
key: migrate
field_name: field_contact
process:
field_name:
plugin: iterator
source: field_name
process:
value: value
revision_id: revision_id
field_email:
plugin: iterator
source: field_email
process:
value: email
revision_id: revision_id
field_phone:
plugin: iterator
source: field_phone
process:
value: value
revision_id: revision_id
destination:
plugin: 'entity_reference_revisions:paragraph'
default_bundle: contact
migration_dependencies:
required: {}
optional: {}
Then create the node migration template (filename: migrate_plus.migration.d7_node_organization.yml):
langcode: en
status: true
dependencies: {}
id: d7_node_organization
class: null
field_plugin_method: null
cck_plugin_method: null
migration_tags:
- 'Drupal 7'
- Content
migration_group: migrate_drupal_7
label: 'Nodes (Organization)'
source:
plugin: d7_node
node_type: organization
process:
nid: nid
vid: vid
langcode:
plugin: default_value
source: language
default_value: und
title: title
uid: node_uid
status: status
created: created
changed: changed
promote: promote
sticky: sticky
revision_uid: revision_uid
revision_log: log
revision_timestamp: timestamp
body:
plugin: iterator
source: body
process:
value: value
format:
- plugin: static_map
bypass: true
source: format
map:
- null
- plugin: skip_on_empty
method: process
- plugin: migration
migration:
- d6_filter_format
- d7_filter_format
source: format
field_contact:
- plugin: skip_on_empty
method: process
source: field_contact
- plugin: migration_lookup
migration: d7_paragraph_contact
no_stub: true
- plugin: iterator
process:
target_id: '0'
target_revision_id: '1'
destination:
plugin: 'entity:node'
default_bundle: organization
migration_dependencies:
required: {}
optional: {}
4. Create the Source Plugin Class
Create a file named ContactParagraph.php in the /src/Plugin/migrate/source directory:
<?php
namespace Drupal\mparagraf\Plugin\migrate\source;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* D7_paragraph_item source.
*
* @MigrateSource(
* id = "d7_paragraph_item"
* )
*/
class ContactParagraph extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
// Select node in its last revision.
$query = $this->select('paragraphs_item', 'fci')
->fields('fci', [
'item_id',
'field_name',
'revision_id',
]);
if (isset($this->configuration['field_name'])) {
$query->innerJoin('field_data_' . $this->configuration['field_name'], 'fd', 'fd.' . $this->configuration['field_name'] . '_value = fci.item_id');
$query->fields('fd', [
'entity_type',
'bundle',
'entity_id',
$this->configuration['field_name'] . '_revision_id',
]);
$query->condition('fci.field_name', $this->configuration['field_name']);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
// If field specified, get field revision ID so there aren't issues mapping.
if (isset($this->configuration['field_name'])) {
$row->setSourceProperty('revision_id', $row->getSourceProperty($this->configuration['field_name'] . '_revision_id'));
}
// Get field API field values.
foreach (array_keys($this->getFields('paragraphs_item', 'contact')) as $field) {
$item_id = $row->getSourceProperty('item_id');
$revision_id = $row->getSourceProperty('revision_id');
$row->setSourceProperty($field, $this->getFieldValues('paragraphs_item', $field, $item_id, $revision_id));
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'item_id' => $this->t('Item ID'),
'revision_id' => $this->t('Revision ID'),
'field_name' => $this->t('Name of field'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['item_id']['type'] = 'integer';
$ids['item_id']['alias'] = 'fci';
return $ids;
}
}
5. Implement the Migration Hook
Add the following code to your module's .module file:
<?php
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* Implements hook_migrate_MIGRATION_ID_prepare_row().
*/
function custom_migrate_d7_node_organization_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
$values = $row->getSourceProperty('field_contact');
$value_new = [];
if ($values) {
foreach ($values as $value) {
$value_new[] = ['item_id' => $value['value']];
}
$row->setSourceProperty('field_contact', $value_new);
}
}
6. Final Steps
Once you've completed all the above steps successfully, you can run your migration and watch as your paragraphs are transferred from Drupal 7 to Drupal 8. Take a moment to verify the results before proceeding with any additional paragraph-related tasks.
Remember to test thoroughly in a development environment before attempting this migration on a production site.