Migrating Wordpress into Drupal 8
Quite a bit has changed for the Migrate module in Drupal 8: the primary module is part of core and some of the tools have been split into their own modules. Recently, we migrated a Wordpress site into Drupal 8 and this article will help guide you in that process. If you’re looking for information about Wordpress to Drupal 7 migrations, check out Joel Steidl’s article on that here.
At the time of writing this post, the migration modules are considered "experimental" so be aware of that as well. The module's location in core also means that all Drupal core modules also have migration-related code to help out with your Drupal upgrades. We used the WP Migrate module (Migrate Wordpress) as a starting point in bringing this content to Drupal.
This module will give you a good basis for migration, but it is missing a few things that you might want to consider:
- It will create all vocabularies and taxonomies based on what is in Wordpress but you will need to add some code to connect the taxonomies with posts.
- Also, it will not bring in featured images.
- WP content might be using the "line break to paragraphs" functionality, which you need to account for either in your text format for posts or in the migration.
And if you are looking for information about Wordpress to Drupal 7 migrations, check out Joel Steidl's article on that here.
Taxonomy
There's code existing to pull in Wordpress's terms and vocabularies, but you will need to do some work to put them into the right fields with your posts. For this, I ended up taking a more efficient route by querying the source database in prepareRow():
<?php
// place in Posts.php prepareRow()
// get terms for this blog post
$tags = $this->select('wp_term_relationships', 'r')
->join('wp_term_taxonomy', 't', 't.term_taxonomy_id=r.term_taxonomy_id')
->fields('r')
->condition('t.taxonomy', 'tags')
->condition('object_id', $row->getSourceProperty('id'))->execute();
$tags = $tags->fetchAll();
$tags = array_map(function($tag) {
return intval($tag['term_taxonomy_id']);
}, $tags);
$row->setSourceProperty('tags', $tags);
// get categories for this blog post
$category = $this->select('wp_term_relationships', 'r')
->join('wp_term_taxonomy', 't', 't.term_taxonomy_id=r.term_taxonomy_id')
->fields('r')
->condition('t.taxonomy', 'category')
->condition('object_id', $row->getSourceProperty('id'))->execute();
$category = $category->fetchAll();
$category = array_map(function($tag) {
return intval($tag['term_taxonomy_id']);
}, $category);
$row->setSourceProperty('categories', $category);
And then I updated the migration template with those new values:
# add to the process section
field_tags: tags
field_category: tags
Featured Images
Wordpress stores featured images as attachment posts and stores the relationship in the postmeta table. To bring these in as image fields, we need to make file entities in Drupal which means configuring a new migration.
First, create a migration template called wp_feature_images.yml. Note that I stole some of this from Drupal's core file module:
id: wp_feature_images
label: Wordpress Feature Images
migration_tags:
- Wordpress
migration_group: wordpress
source:
plugin: feature_images
destination:
plugin: entity:file
process:
filename: filename
uri: uri
status:
plugin: default_value
default_value: 1
# migration_dependencies:
# required:
# - wp_users
And then create a source plugin:
<?php
/**
* @file
* Contains \Drupal\migrate_wordpress\Plugin\migrate\source\FeatureImages.
*/
namespace Drupal\migrate_wordpress\Plugin\migrate\source;
use Drupal\migrate\Row;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\Core\File\FileSystemInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Core\State\StateInterface;
/**
* Extract feature images from Wordpress database.
*
* @MigrateSource(
* id = "feature_images"
* )
*/
class FeatureImages extends SqlBase {
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, FileSystemInterface $file_system) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state);
$this->fileSystem = $file_system;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('state'),
$container->get('file_system')
);
}
/**
* {@inheritdoc}
*/
public function query() {
$query = $this
->select('wp_postmeta', 'm')
->fields('p', ['ID', 'guid']);
$query->join('wp_posts', 'p', 'p.ID=m.meta_value');
$query
->condition('m.meta_key', '_thumbnail_id', '=')
->condition('p.post_type', 'attachment', '=')
->condition('p.guid', '', '<>')
// this prevents some duplicates to get the count closer to even
->groupBy('ID, guid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = array(
'ID' => $this->t('The file ID.'),
'guid' => $this->t('The file path'),
);
return $fields;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$url = $row->getSourceProperty('guid');
$parsed_url = parse_url($url);
$filename = basename($parsed_url['path']);
$row->setSourceProperty('filename', $filename);
$public_path = 'public://' . $parsed_url['path'];
$row->setSourceProperty('uri', $public_path);
// download the file if it does not exist
if (!file_exists($public_path)) {
$public_dirname = dirname($public_path);
// create directories if necessary
if (!file_exists($public_dirname)) {
$this->fileSystem->mkdir($public_dirname, 0775, TRUE);
}
// try to download it
$copied = @copy($url, $public_path);
if (!$copied) {
return FALSE;
}
}
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function bundleMigrationRequired() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return array(
'ID' => array(
'type' => 'integer',
'alias' => 'p',
),
);
}
}
In Migrate, the template defines what source, processing, and fields are created. The source plugin is used by that migration to allow you to specify what is created. The source plugin above will get the feature images for posts, but also try and download the image into Drupal's files directory.
You can add this as a dependency for the wp_posts migration. A word of warning though: if one migration (Migration A) depends on a different migration (Migration B), all of the content from A must be migrated before B can be run. If there are images that cannot be resolved for some reason (maybe leftover DB references after an image or post is deleted), this might stop the migration because the dependency cannot be resolved.
And finally, you will also need to add "wp_feature_images" to your manifest_wordpress.yml before running the migration.
Converting content
So far we have updated migration source plugins, but there are also process plugins, which can be used to change row values. As mentioned, the WP content often uses the autop filter to create paragraph/line breaks automatically so we need to change those to HTML for Drupal. (You can also just use this functionality in your text format and skip this step if having this on will not cause issues with other content)
First, create a "src/Plugin/migrate/process" directory if one does not exist in the module and add this processor:
<?php
namespace Drupal\migrate_wordpress\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Apply the automatic paragraph filter to content
*
* @MigrateProcessPlugin(
* id = "wp_content"
* )
*/
class WpContent extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Split the 'administer nodes' permission from 'access content overview'.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
return _filter_autop($value);
}
}
Then, update the "process" section of "wp_posts.yml" to include this processor:
'body/value':
plugin: wp_content
source: post_content
All of this should put you on the road to getting Wordpress content migrated into a Drupal 8 site, although you’ll probably have to adjust code to your specific circumstances along the way.