Drupal 10: Migrating Flags With The Migrate Module

I've been doing a bit of Drupal migration recently, and one of the tasks I have undertaken is to migrate "likes" from a Drupal 7 site to a Drupal 10 site.

The likes in the old Drupal 7 site were built using a custom module, with the Flag module was selected to provide the new functionality on the new Drupal 10 site. This meant that I needed to migrate the old custom structure into the entity structure provided by the Flag module.

To add further complication, it was possible to add likes to nodes and comments, which meant that the flags on the new side needed to be applied to two different types of entity. This was simple enough to do using two different types of flag entity; one for nodes and one for comments.

An added complication came from migrating from a single resource into what is essentially two different bundle types; this needed a bit of logic to be written, but was certainly possible in a single migration step. In fact, the decision I took was to migrate the likes as a single process, which meant that I could run or re-run the migration with a single command.

In this article I will go through each step of the migration process from source to destination, including the custom processing plugin created to aid the migration. This won't be a detailed breakdown of each component so some prior knowledge of the migration system is expected.

Setting Up The Flag Module

The flag module is well written (and well tested!) and allows for different types of flags to be added to different types of content. The Drupal 10 configuration consisted of two different types of flag; one to flag content and one to flag comments. We must create "Personal" flags when configuring the module here in order to allow content to be flagged by more than one user.

By contrast, "Global" flags are used to allow users to add a single flag against an item of content. This might be used to create a bookmark system or to report an item of content to the site administrators for example.

This is what the flag configuration looks like after settings things up.

Drupal administration interface, showing the content and comments flag configuration.

With this in place we can now look at generating the entities that represent the flags users have attached to items of content.

Creating A Flagging Entity

Just a quick side note, the entity type used to link a user to a particular item of content through a flag is called a Flagging entity. This is the entity type that needs to be created to migrate the likes into the new site.

In our case, we are creating a non-session based flag that is linked to a content item and a user. The flagging entity has a few values that define what sort of flagging bundle it belongs to, and what sort of entity is being flagged.

Here is an example of how to create a flagging entity connected to an item of news content.

$flaggingStorage = \Drupal::service('entity_type.manager')->getStorage('flagging');
$flagging = $flaggingStorage->create([
  'flag_id' => 'like_content',
  'entity_type' => 'news',
  'entity_id' => 1,
  'uid' => 1,
  'created' => time(),
  'session_id' => NULL,
]);
$flagging->save();

The value of "like_content" for the "flag_id" field informs the flag modules of name of the flag that we setup in the flag module. In the above example we are creating a "like" on news page 1 for user 1. The session_id can remain null for the purposes of our setup here.

Creating The Migration Source

Due to the fact that the likes on the old site are custom, the source for the like migration also needs to be a custom migration source. In fact, the data was just stored in a table  on the Drupal 7 site and logic was created to like or unlike content.

To this end, we must create a migration source plugin and extend the \Drupal\migrate\Plugin\migrate\source\SqlBase class. The likes in the old site were stored in a table called "like" and because it was possible to like both news pages and comments we need to link that table with the node and comment tables to get all of the data. We use left joins here to allow null values to be brought through in the query results.

We don't want to migrate any likes for news pages, comments, or users that are not published or active, so the query we create also removes this data from the migration source.

Here is the migration source plugin class, with the query override. We are also informing the migration system about the fields we will be returning from the query results.

<?php

namespace Drupal\migration_like\Plugin\migrate\source;

use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Row;

/**
 * Drupal 7 node source from database.
 *
 * @MigrateSource(
 *     id = "like"
 * )
 */
class Like extends SqlBase {

  /**
   * {@inheritDoc}
   */
  public function query() {
    $query = $this->select('like', 'like');

    $query->leftJoin('node', 'n', "like.entity_id = n.nid AND like.entity_type = 'node' AND n.status = 1");
    $query->leftJoin('comment', 'c', "like.entity_id = c.cid AND like.entity_type = 'comment' AND c.status = 1");

    // Don't migrate any likes from inactive users.
    $query->join('users', 'u', 'like.uid = u.uid');
    $query->condition('u.status', '1');

    $query->fields('like', [
      'reaction_id',
      'entity_type',
      'entity_id',
      'uid',
      'reaction_date',
    ]);
    $query->orderBy('entity_id');
    return $query;
  }

  /**
   * {@inheritDoc}
   */
  public function fields() {
    $fields = [
      'reaction_id' => $this->t('The reaction ID.'),
      'entity_type' => $this->t('The entity type.'),
      'entity_id' => $this->t('The entity ID.'),
      'uid' => $this->t('The user ID.'),
      'reaction_date' => $this->t('Created timestamp.'),
    ];
    return $fields;
  }

  /**
   * {@inheritDoc}
   */
  public function getIds() {
    return [
      'reaction_id' => [
        'type' => 'integer',
      ],
    ];
  }

}

One extra thing we need in this class is to define a prepareRow() method, which we add to the above class. This method is called automatically during the migration for every row we retrieve from the database and allows us to map the type of entity being liked into the correct flagging type in the destination database.

Essentially, we find the entity_type in the row and use this to set the flag_id of the source. This can be "like_comment" for comments and "like_content" for nodes.

...
  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    // Use the entity_type to determine the flag_id.
    $entityType = $row->getSourceProperty('entity_type');
    switch ($entityType) {
      case 'comment':
        $row->setSourceProperty('flag_id', 'like_comment');
        break;

      case 'node':
        $row->setSourceProperty('flag_id', 'like_content');
        break;
    }

    return parent::prepareRow($row);
  }
...

This could also have been done in the database query, but having the source entity_type is useful for later processing, which we will come onto later.

With this class in hand we just need to add the source for our migration to the migration yml file.

source:
  plugin: like

We can now start working on the process section to turn this data into the structure we need for the destination flagging entity.

Creating The Migration Process

The migration process itself has a number of different components.

We start off the definitions of where some of the fields should be mapped to, including a migration lookup on the user. As we sorted out the flag_id field in the prepareRow() method in our source plugin we can just map this to the flag_id field of the destination. The "global" field is set to always be 0 using the default_value migration plugin.

process:
  flag_id: flag_id
  entity_type: entity_type
  created: reaction_date
  global:
    plugin: default_value
    default_value: 0
  uid:
    - plugin: migration_lookup
      migration: user
      source: uid
      no_stub: true
    - plugin: skip_on_empty
      method: row
      message: "uid is missing"

Next, we need to load into memory the destination entity ID information. Due to the fact that this process will be adding likes to comments and news pages we need to use the migration_lookup plugin to find the ID information from one of those migration steps. As there is no guarantee that the destination exists we set the "no_stub" option to "true", which prevents stub entities from being created when they would never be filled in.

process:
...
  cid:
    - plugin: migration_lookup
      migration: comment
      source: entity_id
      no_stub: true
  nid:
    - plugin: migration_lookup
      migration: news
      source: entity_id
      no_stub: true

This leaves us with the information we need to import the like, but we still need to process this into the destination entity_id field. Here, we create a custom process plugin that will process each row in the migration and assign the correct destination entity_id. The plugin is called "like_process" and is added to the yml file like this.

process:
...
  entity_id:
    - plugin: like_process

The process plugin class extends the Drupal\migrate\ProcessPluginBase and adds the MigrateProcessPlugin annotation to the class docblock. By default, the transform() method is called for each row in the migration process, and we can use this row to process the information we need for the destination flagging entity.

Here is the source code of the process plugin.

<?php

namespace Drupal\migration_like\Plugin\migrate\process;

use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;

/**
 * Drupal 7 like destination.
 *
 * @MigrateProcessPlugin(
 *     id = "like_process"
 * )
 */
class Like extends ProcessPluginBase {

  /**
   * {@inheritDoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    // Use the entity type to get the correct entity id from the source.
    $entityType = $row->getSourceProperty('entity_type');
    switch ($entityType) {
      case 'comment':
        $entityId = $row->getDestinationProperty('cid');
        break;

      case 'node':
        $entityId = $row->getDestinationProperty('nid');
        break;
    }

    if (!isset($entityId) || $entityId === NULL) {
      // If no entity ID was set then skip this item.
      throw new MigrateSkipRowException('Mapped entity id not found for flag.');
    }

    $row->setDestinationProperty('entity_id', $entityId);

    return $entityId;
  }

}

What we are doing here is looking for the entity_type and setting the entity_id property based on the information found in the migration data. We have to do this in a process plugin as we need to look at the information found after the migration_lookup steps for comments and nodes. We find the destination property based on the type of entity we are currently looking at, this gives us the entity_id of the destination.

As a precaution before setting the entity_id we perform a check to make sure that the entity_id has a value. If it doesn't have a value then we throw a MigrateSkipRowException exception, which has the effect of ignoring this row in the migration process.

Creating The Destination

There are a couple of options open to us in terms of the migration destination of the flagging entities, but the simplest thing we can do is to use the entity migration destination plugin and inform it that we want to create a flagging entity.

destination:
  plugin: entity:flagging

The destination plugin will figure out what data goes into what fields and create our flagging entities automatically.

Running the Migration

Before we run the migration we must first add a list of dependencies to the like migration. This informs the migration system that the user, news and comment migrations need to happen before this migration can be run.

migration_dependencies:
  required:
    - user
    - news
    - comment

For completeness, here is the full migration yml file.

id: like
label: Like Migration

source:
  plugin: like

process:
  flag_id: flag_id
  entity_type: entity_type
  created: reaction_date
  global:
    plugin: default_value
    default_value: 0
  uid:
    - plugin: migration_lookup
      migration: user
      source: uid
      no_stub: true
    - plugin: skip_on_empty
      method: row
      message: "uid is missing"
  cid:
    - plugin: migration_lookup
      migration: comment
      source: entity_id
      no_stub: true
  nid:
    - plugin: migration_lookup
      migration: news
      source: entity_id
      no_stub: true
  entity_id:
    - plugin: like_process

destination:
  plugin: entity:flagging

migration_dependencies:
  required:
    - user
    - news
    - comment

With all this in place we can now run the migration using the migrate:import Drush command, which is part of the Migrate Tools module. This command would be run like this.

drush migrate:import likes --execute-dependencies

Adding the --execute-dependencies will mean that the migration dependencies will run before running this migration.

The fact that we can create any known entity through the migration system means that we can easily migrate any data from source to destination. All we need to do is ensure that all of the fields are intact and the migration system will figure out the rest.

Comments

Hi Julian,

It wasn't that I decided to do it custom, it was that the original module was a custom created module. In other words, I wasn't migrating flags to flags, I was migrating random data in database tables to flags. The final destination was perfect fine though, Drupal's ability to migrate data into entities using just a plugin made things easy once I had the data.

Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
2 + 17 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.