Migrate nodes from Drupal 7 to custom entity in Drupal 8

Development |

In this section we will show you how to migrate nodes from drupal 7 into a custom content entity in drupal 8. If you didn’t run through previous lessons I would suggest you to do it first.

For this lesson you can use the same docker-compose.yml file as previous.

Let’s start

    Build up vanilla drupal 7 site. Create book content type with fields listed below:

  • Title (default)

  • Body (default)

  • field_image

  • field_price (integer)

  • field_isbn (integer)

Create few nodes of book content type so you have enough stuff for migration. Export database.sql file.

Now you have to install vanilla drupal 8 site. Make sure your drupal console works correctly. We will use drupal console to generate module and custom content entity.

Jump into php container and type ‘drupal’ if you don’t have drupal console installed you will see message like this one:

Zoom with default options

Just type ‘composer require drupal/console:~1.0 --prefer-dist --optimize-autoloader’ and installation will start.

Now when you type ‘drupal’ the commands for auto generate should be listed.

Zoom with default options

Ok, we are ready to continue. Create new module storage_module. You can do it easy by typing drupal generate:module or just drupal gm (alias).

This module we will use for our custom content entity. When module is created, we are ready to create custom content entity.

Also we will use drupal console for that job. Just type drupal generate:entity:content or drupal geco and generate book custom entity.

When entity is created we need to modify entity fields. Make the same fields as for book content type in drupal 7.

Zoom with default options

Book.php is the main file for our custom content entity, all you need is to modify fields in this file.

Here is how it should looks like:

class Book extends RevisionableContentEntityBase implements BookInterface {

 use EntityChangedTrait;

 /**
  * {@inheritdoc}
  */
 public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
   parent::preCreate($storage_controller, $values);
   $values += [
     'user_id' => \Drupal::currentUser()->id(),
   ];
 }

 /**
  * {@inheritdoc}
  */
 protected function urlRouteParameters($rel) {
   $uri_route_parameters = parent::urlRouteParameters($rel);

   if ($rel === 'revision_revert' && $this instanceof RevisionableInterface) {
     $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
   }
   elseif ($rel === 'revision_delete' && $this instanceof RevisionableInterface) {
     $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId();
   }

   return $uri_route_parameters;
 }

 /**
  * {@inheritdoc}
  */
 public function preSave(EntityStorageInterface $storage) {
   parent::preSave($storage);

   foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
     $translation = $this->getTranslation($langcode);

     // If no owner has been set explicitly, make the anonymous user the owner.
     if (!$translation->getOwner()) {
       $translation->setOwnerId(0);
     }
   }

   // If no revision author has been set explicitly, make the book owner the
   // revision author.
   if (!$this->getRevisionUser()) {
     $this->setRevisionUserId($this->getOwnerId());
   }
 }

 /**
  * {@inheritdoc}
  */
 public function getName() {
   return $this->get('name')->value;
 }

 /**
  * {@inheritdoc}
  */
 public function setName($name) {
   $this->set('name', $name);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function getImage() {
   return $this->get('image')->value;
 }

 /**
  * {@inheritdoc}
  */
 public function setImage($name) {
   $this->set('image', $name);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function getNotes() {
   return $this->get('notes')->value;
 }

 /**
  * {@inheritdoc}
  */
 public function setNotes($name) {
   $this->set('notes', $name);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function getIsbn() {
   return $this->get('isbn')->value;
 }

 /**
  * {@inheritdoc}
  */
 public function setIsbn($name) {
   $this->set('isbn', $name);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function getPrice() {
   return $this->get('price')->value;
 }

 /**
  * {@inheritdoc}
  */
 public function setPrice($name) {
   $this->set('price', $name);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function getCreatedTime() {
   return $this->get('created')->value;
 }

 /**
  * {@inheritdoc}
  */
 public function setCreatedTime($timestamp) {
   $this->set('created', $timestamp);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function getOwner() {
   return $this->get('user_id')->entity;
 }

 /**
  * {@inheritdoc}
  */
 public function getOwnerId() {
   return $this->get('user_id')->target_id;
 }

 /**
  * {@inheritdoc}
  */
 public function setOwnerId($uid) {
   $this->set('user_id', $uid);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function setOwner(UserInterface $account) {
   $this->set('user_id', $account->id());
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public function isPublished() {
   return (bool) $this->getEntityKey('status');
 }

 /**
  * {@inheritdoc}
  */
 public function setPublished($published) {
   $this->set('status', $published ? TRUE : FALSE);
   return $this;
 }

 /**
  * {@inheritdoc}
  */
 public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
   $fields = parent::baseFieldDefinitions($entity_type);

   $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
     ->setLabel(t('Authored by'))
     ->setDescription(t('The user ID of author of the Book entity.'))
     ->setRevisionable(TRUE)
     ->setSetting('target_type', 'user')
     ->setSetting('handler', 'default')
     ->setTranslatable(TRUE)
     ->setDisplayOptions('view', [
       'label' => 'hidden',
       'type' => 'author',
       'weight' => 0,
     ])
     ->setDisplayOptions('form', [
       'type' => 'entity_reference_autocomplete',
       'weight' => 5,
       'settings' => [
         'match_operator' => 'CONTAINS',
         'size' => '60',
         'autocomplete_type' => 'tags',
         'placeholder' => '',
       ],
     ])
     ->setDisplayConfigurable('form', TRUE)
     ->setDisplayConfigurable('view', TRUE);

   $fields['image'] = BaseFieldDefinition::create('image')
     ->setLabel(t('Image'))
     ->setRequired(TRUE)
     ->setDisplayOptions('view', [
       'label'   => 'above',
       'type'    => 'image',
       'weight'  => 0,
     ])
     ->setDisplayOptions('form', [
       'type'    => 'image_image',
       'weight'  => 0,
     ])
     ->setRequired(FALSE);

   $fields['name'] = BaseFieldDefinition::create('string')
     ->setLabel(t('Name'))
     ->setDescription(t('The name of the Book entity.'))
     ->setRevisionable(TRUE)
     ->setSettings([
       'max_length' => 50,
       'text_processing' => 0,
     ])
     ->setDefaultValue('')
     ->setDisplayOptions('view', [
       'label' => 'above',
       'type' => 'string',
       'weight' => -4,
     ])
     ->setDisplayOptions('form', [
       'type' => 'string_textfield',
       'weight' => -4,
     ])
     ->setDisplayConfigurable('form', TRUE)
     ->setDisplayConfigurable('view', TRUE)
     ->setRequired(TRUE);

   $fields['isbn'] = BaseFieldDefinition::create('integer')
     ->setLabel(t('ISBN'))
     ->setDescription(t('ISBN of the Book'))
     ->setRevisionable(TRUE)
     ->setTranslatable(TRUE)
     ->setDisplayOptions('form', array(
       'type' => 'string_textfield',
       'settings' => array(
         'display_label' => TRUE,
       ),
     ))
     ->setDisplayOptions('view', array(
       'label' => 'hidden',
       'type' => 'string',
     ))
     ->setDisplayConfigurable('form', TRUE)
     ->setRequired(FALSE);

   $fields['price'] = BaseFieldDefinition::create('float')
     ->setLabel(t('Price'))
     ->setDescription(t('Price of the Book'))
     ->setRevisionable(TRUE)
     ->setTranslatable(TRUE)
     ->setDisplayOptions('form', array(
       'type' => 'string_textfield',
       'settings' => array(
         'display_label' => TRUE,
       ),
     ))
     ->setDisplayOptions('view', array(
       'label' => 'hidden',
       'type' => 'string',
     ))
     ->setDisplayConfigurable('form', TRUE)
     ->setRequired(FALSE);

   $fields['notes'] = BaseFieldDefinition::create('string_long')
     ->setLabel(t('Notes'))
     ->setDescription(t('Example of string_long field.'))
     ->setDefaultValue('')
     ->setRequired(FALSE)
     ->setDisplayOptions('view', [
       'label' => 'visible',
       'type' => 'basic_string',
       'weight' => 5,
     ])
     ->setDisplayOptions('form', [
       'type' => 'string_textarea',
       'weight' => 5,
       'settings' => ['rows' => 4],
     ])
     ->setDisplayConfigurable('view', TRUE)
     ->setDisplayConfigurable('form', TRUE);

   $fields['status'] = BaseFieldDefinition::create('boolean')
     ->setLabel(t('Publishing status'))
     ->setDescription(t('A boolean indicating whether the Book is published.'))
     ->setRevisionable(TRUE)
     ->setDefaultValue(TRUE)
     ->setDisplayOptions('form', [
       'type' => 'boolean_checkbox',
       'weight' => -3,
     ]);

   $fields['created'] = BaseFieldDefinition::create('created')
     ->setLabel(t('Created'))
     ->setDescription(t('The time that the entity was created.'));

   $fields['changed'] = BaseFieldDefinition::create('changed')
     ->setLabel(t('Changed'))
     ->setDescription(t('The time that the entity was last edited.'));

   return $fields;
 }

}
          

At the end of the day you can use my code from bitbucket.

To be sure that everything is ok go to /admin/structure/book and add some books.

Now we are ready for migration.

Create new module entity_migrate

Zoom with default options

As before, we need migration group migrate_plus.migration_group.yml

File migration migrate_plus.migration.book_file.yml

And node migration migrate_plus.migration.custom_book

migrate_plus.migration_group.d7.yml

id: d7
label: D7 imports
description: Migrations importing from the legacy D7 ya_example site
source_type: Drupal 7
shared_configuration:
 source:
   key: old_drupal
            

migrate_plus.migration.book_file.yml

# Every migration that references a file by Drupal 7 fid should specify this
# migration as an optional dependency.
id: book_file
label: d7 blog files
audit: true
migration_group: d7
migration_tags:
 - Drupal 7
 - Content
source:
 plugin: d7_file
 scheme: public
 constants:
   # The tool configuring this migration must set source_base_path. It
   # represents the fully qualified path relative to which URIs in the files
   # table are specified, and must end with a /. See source_full_path
   # configuration in this migration's process pipeline as an example.
   source_base_path: 'sites/default/files/migrate_files'
process:
 # If you are using this file to build a custom migration consider removing
 # the fid field to allow incremental migrations.
 fid: fid
 filename: filename
 source_full_path:
   -
     plugin: concat
     delimiter: /
     source:
       - constants/source_base_path
       - filepath
   -
     plugin: urlencode
 uri:
   plugin: file_copy
   source:
     - '@source_full_path'
     - uri
 filemime: filemime
 # No need to migrate filesize, it is computed when file entities are saved.
 # filesize: filesize
 status: status
 # Drupal 7 didn't keep track of the file's creation or update time -- all it
 # had was the vague "timestamp" column. So we'll use it for both.
 created: timestamp
 changed: timestamp
 uid: uid
destination:
 plugin: entity:file

As before, pay attention on

            source_base_path: 'sites/default/files/migrate_files
          

migrate_files is folder in drupal 8 where we have to copy whole site directory from drupal 7.

Folder structure:

Zoom with default options

migrate_plus.migration.custom_book.yml

id: custom_book
label: Custom book node migration from Drupal 7
migration_group: d7
dependencies:
 enforced:
   module:
     - entity_migrate
source:
 plugin: d7_node
 node_type: book
destination:
 plugin: entity:book
process:
 id: nid
 vid: vid
 type: type
 langcode:
   plugin: static_map
   bypass: true
   source: language
   map:
     und: en
 name: title
 status: status
 created: created
 changed: changed
 promote: promote
 sticky: sticky
 notes: body
 price: field_price
 image:
   plugin: iterator
   source: field_image
   process:
     target_id:
       plugin: migration_lookup
       migration: book_file
       source: fid
     alt: title
     title: title
     height: height
     width: width
 isbn: field_isbn
          

The field mapping is really simple 1:1

Custom entity fields:

  • name (in drupal 7 it is title)

  • notes (in drupal 7 it is body field)

  • image (in drupal 7 it is field_image)

  • price (in drupal 7 it is field_price)

  • isbn (in drupal 7 it is field_isbn)

Now we are ready for migration

Type drush migrate-status

Zoom with default options

First thing we are gonna migrate is book_file.

Run migration by typing drush migrate:import book_file.

After that finish migration with drush migrate:import custom_book.

That’s all, have a fun with migration.

You can find source code here:

Drupal 8 custom content entity migration

RELATED ARTICLES