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.
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
Thanks for this article, but wouldn't it be better to push things forward for a generic flag migration solution in this issue?
https://www.drupal.org/project/flag/issues/2409901
Why did you decide to do it custom?
Submitted by Julian on Thu, 02/09/2023 - 12:24
PermalinkHi 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.
Submitted by giHlZp8M8D on Thu, 02/09/2023 - 13:18
PermalinkJulian,
I've created a list of the core contexts available here https://www.hashbangcode.com/snippets/drupal-10-list-core-contexts
I hope that helps!
Phil
Submitted by giHlZp8M8D on Fri, 02/10/2023 - 22:37
PermalinkAdd new comment