This website has been migrated from Drupal 7 to WordPress using a custom process. I created a quick-and-dirty WordPress plugin to query specific Drupal 7 nodes and add them as WordPress posts.
In most cases, I would recommend building or using two plugins: one for Drupal 7 that queries and exports data using the Drupal’s API, and another that imports compatible data into WordPress. There are a number of plugins out there that can manage this. In this case, I was only migrating a specific subset of nodes. I excluded the file API to set the featured image but imported the title, summary, body, tags.
The Code
I decided to break the plugin out into two files: one to define the plugin menu item and settings screen, and another to perform the actual functionality. This plugin could have been written as a single file.
migrate.php
The first section of the code is a block of comments. This provides WordPress with the information to display the plugin information on the Plugins page. Even if you are just writing a plugin for yourself, it is still a good idea to include this information.
/* * @package MigratePal * @version 1.0 */ /* Plugin Name: MigratePal Plugin URI: http://noahjstewart/downloads/migratepal/2017/05/custom-drupal-7-to-wordpress-migration Description: Migration tool from Drupal 7 to WordPress Author: Noah Stewart Version: 1.0 Author URI: http://noahjstewart.com/ */ |
Next I include the external file and define the menu. The code in the external migration.inc file could be in the migration.php file instead.
include_once('migration.inc'); |
Below that I define a submenu for the built-in WordPress Tools menu that I call MigratePal.
add_action('admin_menu', 'migratepal_page_create'); function migratepal_page_create() { $page_title = 'Migrate From Drupal 7'; $menu_title = 'MigratePal'; $capability = 'edit_posts'; $menu_slug = 'migratepal_page'; $function = 'migratepal_page_display'; $icon_url = ''; $position = 24; add_submenu_page( 'tools.php', $page_title, $menu_title, $capability, $menu_slug, $function, $icon_url, $position ); } |
The function defined above is used to output the page content.
1 2 3 4 5 6 7 8 9 10 11 | //display for main page function migratepal_page_display() { echo '<div class="wrap">'; echo '<h1>Custom Migration from Drupal 7</h1>'; echo '<p>Custom import script for bringing select Drupal 7 content into WordPress</p>'; _migratepal_form_import(); echo '</div>'; } |
The form handler was defined in a separate function. Here I check for the menu slug for page and to see if the submit button has been pressed. If those two conditions are met, I run the migration function declared in the include file. The form is output last.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //function to create form function _migratepal_form_import() { //handle post data if(isset($_POST['submit']) && $_POST['page'] = 'migratepal_page') { //see include file migratepal_migration(); } //output form echo '<form method="POST" id="migratepal-form">'; echo '<p class="submit">'; echo '<input type="submit" name="submit" class="button button-primary" value="Import Nodes" />'; echo '</p>'; echo '</form>'; } |
migrate.inc
The include file has all of the actual migration functionality in it. In it, I read a second database that has the Drupal 7 tables in it. WordPress functions are used to insert data into the new website. The query selects the fields that are comparable to the WordPress post fields.
//define mysql connection define(DB_IMPORT_SERVER, 'localhost'); define(DB_IMPORT_NAME, 'drupaldb'); //drupal database define(DB_IMPORT_USER, 'drupaluser'); //drupal user define(DB_IMPORT_PASSWORD, 'drupalpass'); //migration script - customize this functionality function migratepal_migration() { echo 'migrating nodes...<br />'; $conn = new mysqli(DB_IMPORT_SERVER, DB_IMPORT_USER, DB_IMPORT_PASSWORD, DB_IMPORT_NAME); if (mysqli_connect_errno()) { echo "Failed to connect to MySQL: " . mysqli_connect_error(); } //if file connection is valid, loop through if($conn) { //set query and get result $sql = "SELECT n.nid, n.title, FROM_UNIXTIME(n.created) created, FROM_UNIXTIME(n.changed) changed, fdb.body_value body, fdfhs.field_header_summary_value summary, n.status published FROM node n INNER JOIN field_data_body fdb ON n.nid = fdb.entity_id INNER JOIN field_data_field_header_summary fdfhs ON n.nid = fdfhs.entity_id WHERE n.type = 'blog'"; $result = mysqli_query($conn, $sql); if(!$result) { echo 'No result from query: ' . $sql; } |
Next I set up a counter and determine the file paths. The file path is used in a string replace further on that updates images inserted into the HTML content.
//set counter and upload folder $counter = 0; $drupal_files = '/sites/default/files/'; $uploads_folder = wp_upload_dir(); $upload_base = esc_url($uploads_folder['baseurl']) . '/'; |
Next I loop through the results. If there isn’t already a post with the same title, I define the WordPress fields and insert the post. The unix timestamps are converted to php dates and the file paths are replaced in the body content.
//loop through results while($row = mysqli_fetch_array($result)) { //find title $post_id = post_exists($row['title']); if (!$post_id) { //convert dates $dateCreated = date($row['created']); $dateChanged = date($row['changed']); //update body $body = $row['body']; $body = str_replace($drupal_files, $upload_base, $body); //set post data $data = array ( 'post_type' => 'post', 'post_title' => $row['title'], 'post_name' => $row['title'], 'post_content' => $body, 'post_excerpt' => $row['summary'], 'post_date' => $dateCreated, 'post_date_gmt' => $dateCreated, 'post_modified' => $dateChanged, 'post_modified_gmt' => $dateChanged, 'post_status' => $row['published'] == 1 ? 'publish' : 'draft', 'comment_status' => 'closed', 'ping_status' => 'open', ); //get post id $post_id = wp_insert_post($data); |
Next I update the fields that I defined using the Advanced Custom Fields plugin. I created a separate title and description for social media. Since these two fields didn’t exist in the previous website, I set them to the same values as the title and summary.
Drupal stores tags in a several tables and since I’m not using the Drupal API, I need to perform another SQL query. I add the name of each tag to a tag array and set the post tags. Finally, I increment the counter and reset the postdata before looping again.
//post was created if($post_id) { //add custom field values add_post_meta($post_id, 'social_title', $row['title']); add_post_meta($post_id, 'social_description', $row['summary']); //get taxonomy entries $sql_tags = "SELECT ttd.name FROM taxonomy_term_data ttd INNER JOIN taxonomy_index ti ON ttd.tid = ti.tid WHERE ti.nid = " . $row['nid']; $result_tags = mysqli_query($conn, $sql_tags); //set tags array and add to post $tags = array(); if($result_tags) { while($row_tags = mysqli_fetch_array($result_tags)) { if(!in_array($row_tags['name'], $tags)) { array_push($tags, $row_tags['name']); } } } if(count($tags)) { wp_set_post_tags($post_id, $tags); } //increment $counter++; wp_reset_postdata(); } } |
When migrating a site, it is important to remember that there may be bookmarks and hyperlinks out there to the old url. Since I decided to change the permalink structure to include the year and month, it was necessary to create some redirects. Adding the entries directly to the site’s .htaccess file is the simplest and most straightforward way.
//generate redirect urls for the .htaccess file $sql_alias = "SELECT alias FROM url_alias ua WHERE ua.source = 'node/" . $row['nid'] . "'"; $result_alias = mysqli_query($conn, $sql_alias); while($row_alias = mysqli_fetch_array($result_alias)) { $link = str_replace(get_home_url(), '', get_permalink($post_id)); echo 'Redirect 301 /' . $row_alias['alias'] . ' ' . $link . '<br />'; } |
And finally, close the database and braces.
} //close database mysqli_close($conn); echo 'Added ' . $counter . ' posts<br />'; } else { echo 'Unable to establish database connection'; } } |