If you need to migrate field collections from Drupal 7 into Drupal 8, here's a walk through of how to do it. But before you start reading, I'd like you to stop and see how you can make this process better. In this Meta issue, we have a plan to automate all parts of this process. As the heir aparent to Field collections, we are working with the larger Drupal community to include for support migrating directly into Drupal 8 from field collections.
But in the meantime, until that issue is resolved, we'll continue with our demonstration. Since we don't have an existing migration template yet, we'll start by building one manually.
My example consists of a paragraph/field collection called Contact with three fields that are connected to a node type called Organization.
Field Collection/Paragraph: Contact with fields: Email (Email), Phone (Telephone number), Website (Link)
Node type: Organization with field: Contact (Entity reference revisions).
Since we don't have any automation (yet), we have to manually create these fields on the destination side. Soon, we won't have to first create the fields. Their configuration will get migrated for you. But for now, go ahead an create the fields.
Now, in a custom module (in our case called custom_migrate), in src/Plugin/migrate/source, create a class called FieldCollection with the follow details.
<?php
namespace Drupal\custom_migrate\Plugin\migrate\source;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
/**
* d7_field_collection_item source.
*
* @MigrateSource(
* id = "d7_field_collection_item"
* )
*/
class FieldCollection extends FieldableEntity {
/**
* {@inheritdoc}
*/
public function query() {
// Select node in its last revision.
$query = $this->select('field_collection_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('field_collection_item', $row->getSourceProperty('field_name'))) as $field) {
$item_id = $row->getSourceProperty('item_id');
$revision_id = $row->getSourceProperty('revision_id');
$row->setSourceProperty($field, $this->getFieldValues('field_collection_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;
}
}
After this, create a yml template and place this in a the same custom module's config/sync folder.
migrate_plus.migration.d7_field_collection_contacts.yml with the following data:
langcode: en
status: true
dependencies: { }
id: d7_field_collection_contacts
class: null
field_plugin_method: null
cck_plugin_method: null
migration_tags:
- 'Drupal 7'
migration_group: migrate_drupal_7
label: Contacts
source:
plugin: d7_field_collection_item
key: migrate
# field_name is used in our custom plugin to get data about this field_collection_item.
field_name: field_contact
process:
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
field_website:
plugin: iterator
source: field_website
process:
uri: value
revision_id: revision_id
destination:
plugin: 'entity_reference_revisions:paragraph'
default_bundle: contact
migration_dependencies:
required: { }
optional: { }
No we can enable our custom module, flush cache (drush cr), export our configuration (drush cex), or use the generated migration right away (drush mi d7_field_collection_contacts) and test things out. For example, in Drupal 7, the link field 'field_website_uri' needed to get re-mapped to the Drupal 8 'field_website_value'. Again, all this will eventually get done for you the field plugin system. But for now we have to do it manually.
Now that we've migrated the paragraph data, we can proceed with our migration to link the paragraphs to our Organization node type. If we generated all the rest of the migrations for our Drupal 7 to Drupal 8 migration, we'll just be modifying the d7_node_organization migration. If you didn't generate it, then create one now manually. And make the following changes:
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: tnid
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_contacts:
-
plugin: skip_on_empty
method: process
source: field_contact_new
-
plugin: migration_lookup
migration: d7_field_collection_contacts
no_stub: true
-
plugin: iterator
process:
target_id: '0'
target_revision_id: '1'
destination:
plugin: 'entity:node'
default_bundle: organization
migration_dependencies:
required:
- d7_user
- d7_node_type
optional:
- d7_field_instance
- d6_filter_format
- d7_filter_format
We need to change the value of source field_contact in a hook_migrate_MIGRATION_ID_prepare_row in our custom module's .module file; this is done because we need to pass the values to the migration_lookup plugin keyed with item_id.
/**
* Implements hook_migrate_MIGRATION_ID_prepare_row().
*/
function custom_migrate_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_new', $value_new);
}
Here is the existing manual process to migrate field_collection_items to a Paragraph and after that relate it to a node. We invite you to come and help us out in the issue queue to make this process more seamless in your next migration to Paragraphs.
Are you looking for help with a Drupal migration or upgrade? Regardless of the site or data complexity, MTech can help you move from a proprietary CMS or upgrade to the latest version–Drupal 8.
Write us about your project, and we’ll get back to you within 48 hours.