Custom Drupal 7 to WordPress Migration

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';
  }
 
}