Migration From Field Collection to Paragraphs in D8

Posted by Ada Hernández on September 27, 2017

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.