How to migrate paragraphs from Drupal 7 to Drupal 8
Found this really exciting task a couple of days ago, of migrating paragraphs to paragraphs from a Drupal 7 platform to a Drupal8. While searching for the right solution, we’ve started browsing the web. As nothing seemed to help, our backend developers created a custom solution. Before proceeding, here’s a special thanks to mtech-llc.com and to Ada Hernández, who gave us the inspiration from the solution.
While Ada is describing how to migrate field collections, we had to migrate paragraphs. Anyhow, the article really helped us.
INITIAL REQUIREMENTS:
Before proceeding, you will need to make sure that you have a Drupal 8 installed and functional website. Then, ensure that you have installed the following modules:
- migrate_drupal
- migrate_plus
- migrate_tools
- migrate_drupal_ui
An alternative method is to add dependencies to your custom module.
After you have ensured that you have all the modules installed, you will need to create a paragraph with bundle “contact” having 2 fields inside it:
- 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:
- Body field by default
- Field paragraph type (machine name: field_contact)
Steps to be followed to make the setups:
The procedure is rather complicated and you may want to take your time to understand everything that is going on. Errors may appear if some of the steps are not done properly, and in this case, you may want to start it over, while reading carefully the steps below.
1. SPECIFY IN SETTINGS.PHP FILE THE SECOND CONNECTION TO DRUPAL 7
In the following example, my db name is “drupal_7_56”.
Example:
$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 (MACHINENAME: CUSTOM)
3. CREATE THE YML TEMPLATES AND PLACE IT INTO THE CONFIG/INSTALL FOLDER
To create the yml template, you will need to firstly create a paragraph yml file. This ensures that the paragraphs will be migrated properly. The name of the file will be created by using this suffix “migrate_plus.migration.” before the name you choose for the migrate id.
To get back to my initial example, the name of the migrate id is: ‘d7_paragraph_contact’.
This means that the final name file will be: “migrate_plus.migration.d7_paragraph_contact.yml”.
Make sure you are not forgetting the .yml extension.
Use the following code to create the yml template:
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 is used in our custom plugin to get data about the paragraph item. 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: { }
After the first yml template is created, you will need to create a second yml file for migrating the node (type: organization) containing the body and paragraph.
Getting back to my initial example, the name of this file will be: “migrate_plus.migrate.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 CLASS WHICH IMPLEMENTS THE SOURCE PLUGIN FOR THE PARAGRAPH
This is the hard part, so you may want to pay attention to the code below. Assumed the class name is ContactParagraph.php, place it into ‘/src/Plugin/migrate/source’ path.
<?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 HOOK_MIGRATE_MIGRATION_ID_PREPARE_ROW FOR CHANGING THE FIELD_CONTACT
After you have created the class which implements the source plugin for the paragraph, you will need a hook to change the field_contact. Pay attention, as this has to be done in the 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. GRAB A BEER/JUICE/WINE, ENJOY AND COMMENT
Supposed this finally works, you may want to grab a beer, write a great “thanks” in the comment section below, and watch how your paragraphs are being migrated. Then, take a deeeep breath and then do the rest of the things you have to do with the paragraphs.