This article is one of Metal Toad's Top 20 Drupal Tips. Enjoy!
Note: Now updated for Drupal 8.2!
Drupal 8 provides a flexible, plugin-based architecture for migrating data into a site. In Part 3 of this series, we explored how to migrate taxonomies from a Drupal 7 site. We will now expand on this by migrating basic nodes from a Drupal 7 site into Drupal 8.
The code examples in this post build on the migration module begun in Part 2 of this series. If you are trying this code out yourself, it is recommended to start building your custom migration module according to the examples in that post.
Because Drupal nodes can be of many types and have many different user-defined fields, it is complicated to write a single migration script that can handle all fields for all node types. To keep things simple, we will only migrate the built-in "Article" content type, which has the same default fields in Drupal 7 and Drupal 8.
Starting with the "Migrate Custom" module we created in Part 2, we now add the following configuration file.
modules/migrate_custom/config/install/migrate_plus.migration.custom_article.yml
id: custom_article label: Custom article node migration from Drupal 7 migration_group: custom dependencies: enforced: module: - migrate_custom source: plugin: custom_article destination: plugin: entity:node bundle: article process: nid: nid vid: vid type: type langcode: plugin: static_map bypass: true source: language map: und: en title: title uid: uid status: status created: created changed: changed promote: promote sticky: sticky 'body/format': plugin: static_map bypass: true source: body_format map: 1: plain_text 2: restricted_html 3: full_html 4: full_html 'body/value': body_value 'body/summary': body_summary field_tags: tags field_image: images
Pay attention to the last two fields in the definition, "field_tags" and "field_image." These fields can be configured to accept multiple values. (In the case of "field_image" the out-of-the-box configuration allows only one value, but this is easy to change using the Admin UI.) We account for these in the migration by providing only a single property name here. In our source plugin below, we will set these properties to be arrays, thus allowing as many values as exist in our source data.
What's more, "field_image", like the body, is a compound field, in this case consisting of a file ID, ALT text, width, and height. We could specify those values in the definition, but that would limit us to importing only one image. Instead, we will use an associative array in our source plugin to populate all the components of the compound field.
Similar to our Users source plugin in Part 2 of this series, our Blog source definition needs to implement both the query()
and processRow()
methods. We will do this in the following file:
modules/migrate_custom/src/Plugin/migrate/source/Article.php
<?php /** * @file * Contains \Drupal\migrate_custom\Plugin\migrate\source\Article. */ namespace Drupal\migrate_custom\Plugin\migrate\source; use Drupal\migrate\Row; use Drupal\migrate\Plugin\migrate\source\SqlBase; /** * Drupal 7 Blog node source plugin * * @MigrateSource( * id = "custom_article" * ) */ class Article extends SqlBase { /** * {@inheritdoc} */ public function query() { // this queries the built-in metadata, but not the body, tags, or images. $query = $this->select('node', 'n') ->condition('n.type', 'article') ->fields('n', array( 'nid', 'vid', 'type', 'language', 'title', 'uid', 'status', 'created', 'changed', 'promote', 'sticky', )); $query->orderBy('nid'); return $query; } /** * {@inheritdoc} */ public function fields() { $fields = $this->baseFields(); $fields['body/format'] = $this->t('Format of body'); $fields['body/value'] = $this->t('Full text of body'); $fields['body/summary'] = $this->t('Summary of body'); return $fields; } /** * {@inheritdoc} */ public function prepareRow(Row $row) { $nid = $row->getSourceProperty('nid'); // body (compound field with value, summary, and format) $result = $this->getDatabase()->query(' SELECT fld.body_value, fld.body_summary, fld.body_format FROM {field_data_body} fld WHERE fld.entity_id = :nid ', array(':nid' => $nid)); foreach ($result as $record) { $row->setSourceProperty('body_value', $record->body_value ); $row->setSourceProperty('body_summary', $record->body_summary ); $row->setSourceProperty('body_format', $record->body_format ); } // taxonomy term IDs // (here we use MySQL's GROUP_CONCAT() function to merge all values into one row.) $result = $this->getDatabase()->query(' SELECT GROUP_CONCAT(fld.field_tags_tid) as tids FROM {field_data_field_tags} fld WHERE fld.entity_id = :nid ', array(':nid' => $nid)); foreach ($result as $record) { if (!is_null($record->tids)) { $row->setSourceProperty('tags', explode(',', $record->tids) ); } } // images $result = $this->getDatabase()->query(' SELECT fld.field_image_fid, fld.field_image_alt, fld.field_image_title, fld.field_image_width, fld.field_image_height FROM {field_data_field_image} fld WHERE fld.entity_id = :nid ', array(':nid' => $nid)); // Create an associative array for each row in the result. The keys // here match the last part of the column name in the field table. $images = []; foreach ($result as $record) { $images[] = [ 'target_id' => $record->field_files_fid, 'alt' => $record->field_image_alt, 'title' => $record->field_image_title, 'width' => $record->field_image_width, 'height' => $record->field_image_height, ]; } $row->setSourceProperty('images', $images); return parent::prepareRow($row); } /** * {@inheritdoc} */ public function getIds() { $ids['nid']['type'] = 'integer'; $ids['nid']['alias'] = 'n'; return $ids; } /** * {@inheritdoc} */ public function bundleMigrationRequired() { return FALSE; } /** * {@inheritdoc} */ public function entityTypeId() { return 'node'; } /** * Returns the user base fields to be migrated. * * @return array * Associative array having field name as key and description as value. */ protected function baseFields() { $fields = array( 'nid' => $this->t('Node ID'), 'vid' => $this->t('Version ID'), 'type' => $this->t('Type'), 'title' => $this->t('Title'), 'format' => $this->t('Format'), 'teaser' => $this->t('Teaser'), 'uid' => $this->t('Authored by (uid)'), 'created' => $this->t('Created timestamp'), 'changed' => $this->t('Modified timestamp'), 'status' => $this->t('Published'), 'promote' => $this->t('Promoted to front page'), 'sticky' => $this->t('Sticky at top of lists'), 'language' => $this->t('Language (fr, en, ...)'), ); return $fields; } }
Remember to reinstall the module to load the new migration configuration. See Part 3 of this series for more information.
As we did with our user migration, we now run the migration using Drush.
drush migrate-import custom_article
This migration only handles a single content type, and only the fields that are configured in an out-of-the-box Drupal site. To practice what you have learned here, try adding some custom fields to the content types, then add them to the migration definition and source plugin. For more practice, try writing a custom migration for a different content type, like the Basic Page, or a custom content type from your site.