Drupal 8 Migrations, part 4: Migrating Nodes from Drupal 7

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.

Filed under:

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.

The game plan for migrating nodes

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.

The migration definition

Starting with the "Migrate Custom" module we created in Part 2, we now add the following configuration file.


id: custom_article
label: Custom article node migration from Drupal 7
migration_group: custom
      - migrate_custom
  plugin: custom_article
  plugin: entity:node
  bundle: article
  nid: nid
  vid: vid
  type: type
    plugin: static_map
    bypass: true
    source: language
      und: en
  title: title
  uid: uid
  status: status
  created: created
  changed: changed
  promote: promote
  sticky: sticky
    plugin: static_map
    bypass: true
    source: body_format
      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.

The source plugin

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:


 * @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(
    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('
        {field_data_body} fld
        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('
        GROUP_CONCAT(fld.field_tags_tid) as tids
        {field_data_field_tags} fld
        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('
        {field_data_field_image} fld
        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;

Running the migration

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

Next steps

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.

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.