Quantcast
Channel: drupal.org - Site administrators
Viewing all articles
Browse latest Browse all 426

Simple D6 -> D7 profile migration

$
0
0

Several months ago, as I started my migration odyssey, I found a number of helpful tutorials here and elsewhere on the web. BTMash's explanations were incredibly helpful, as were the tutorials in this cookbook on user migration and migrating users/profiles from CSV files. But I could find nothing on migrating D6 profile fields to D7 Profile2 directly.

In my first attempt I used content_profile to migrate a site's profiles into nodes, then the process became just like any node migration. The problem is, content_profile seems to work well on a VPS or dedicated server, but not at all in a shared hosting environment. Invariably, it creates the content type and adds the needed fields, but it times out before all the data is migrated to those fields. As the project page warns clearly, you cannot restart the process to retrieve the data. All that does is replicate the fields on the content type. So, you restore your backup and try again, and again, and again.

But, I reasoned, what makes D6 core profile fields so different from any others? Why is a direct migration so difficult, er, ... impossible? The answer to the second question is that the migration isn't difficult at all. The answer to the first is that when you migrate data from CCK fields you only have to deal with one table, either the D6 content_field_yourfield table, or the content_type_yourtype table. With core profile fields you need to look in two tables, and with at least one profile_field type, date fields, the values are stored as serialized data, not as unix timestamps or other forms understood by the date api.

In fact, all of the heavy lifting happens in the prepareRow() function of the migration. If you have only a few profile fields you probably could add processing of those fields to you user migration, for example, the simple D6 -> D7 user migration here. If you have a bunch of profile fields it might be more manageable to create a separate class to migrate profiles. For clarity, that's what I'll do here.

As I said, the migration is very standard and relatively simple.

/**
* Profile2 migration class D6 -> D7 users.
*/

class RSB_Profile2Migration extends RSB_BasicMigration {
  public function __construct() {
    parent::__construct(MigrateGroup::getInstance('RSBMigrate'));
    $this->dependencies = array('RSB_User');

    //Add a field to your data row for each profile field you need to migrate
    $source_fields = array(
      'uid' => t('User ID'),
      'full_name' => t('The full name of the user'),
      'membership_type' => t('Individual or organization member'),
      'phone_cell' => t('Mobile phone number'),
      'phone_home' => t('Home phone number'),
      'phone_fax' => t('Fax number'),
      'phone_work' => t('Work phone number'),
      'lname' => t('Last name'),
      'fname' => t('First name'),
      'memberdate' => t('The date the member joined'),
      'website' => t('The web site of the user'),
      'sendinfo' => t('Does the user want more information about the organization'),
      'nonmember' => t('The user is not a member'),
      'memberindiv' => t('The user is an individual member'),
      'memberorg' => t('The user is an organization member'),
      'title' => t('Job title'),
      'zip' => t('Postal code'),
      'state' => t('State'),
      'city' => t('City'),
      'street2' => t('Street address 2'),
      'street1' => t('Street address 1'),
      'org' => t('Company or organization'),
    );

    // Select fields from the Drupal 6 user table.
    $connection = rsb_migration2_get_source_connection();
    $query = $connection
      ->select('users', 'u')
      ->fields('u', array('uid'))
      ->condition('u.uid', 1, '>'); // You do not want to mess with User-1.

    // Set source and destination.
    $this->source = new MigrateSourceSQL($query, $source_fields, NULL, array('map_joinable' => FALSE));
    $this->destination = new MigrateDestinationProfile2('main'); // The machine name of your profile.

    // Set up database maping.
    $this->map = new MigrateSQLMap($this->machineName,
      array(
        'uid' => array(
          'type' => 'int',
          'unsigned' => TRUE,
          'not null' => TRUE,
          'description' => 'D6 Unique User ID',
          'alias' => 'u',
        )
      ),
      MigrateDestinationProfile2::getKeySchema()
    );

    // Connecting the profile2 to the user:

    $this->addFieldMapping('uid', 'uid')
         ->sourceMigration('RSB_User')
         ->description(t('The assignment of profile2-items to the respective user profile'));
    $this->addFieldMapping('language')->defaultValue('en');
    $this->addFieldMapping('field_address')->defaultValue('US'); // Using addressfield to hold all this stuff.
    $this->addFieldMapping('field_address:name_line', 'full_name');
    $this->addFieldMapping('field_address:first_name', 'fname');
    $this->addFieldMapping('field_address:last_name', 'lname');
    $this->addFieldMapping('field_address:thoroughfare', 'street1');
    $this->addFieldMapping('field_address:premise', 'street2');
    $this->addFieldMapping('field_address:administrative_area', 'state');
    $this->addFieldMapping('field_address:locality', 'city');
    $this->addFieldMapping('field_address:postal_code', 'zip');
    $this->addFieldMapping('field_address:organisation_name', 'org');
    $this->addFieldMapping('field_sendinfo', 'sendinfo');
    $this->addFieldMapping('field_membership', 'membership_type');
    $this->addFieldMapping('field_title', 'title');
    $this->addFieldMapping('field_title:language')->defaultValue('en');
    $this->addFieldMapping('field_website', 'website');
    $this->addFieldMapping('field_website:title', 'website'); // If your link field had a title subfield adjust accordingly.
    $this->addFieldMapping('field_website:attributes')->defaultValue('');
    $this->addFieldMapping('field_website:language')->defaultValue('en');
    $this->addFieldMapping('field_join_date', 'memberdate')
      ->defaultValue(''); // empty = unknown
    $this->addFieldMapping('field_join_date:timezone')->defaultValue('America/New_York');
    $this->addFieldMapping('field_phone_work')->defaultValue('us'); // Using cck_phone for this.
    $this->addFieldMapping('field_phone_work:number', 'phone_work');
    $this->addFieldMapping('field_phone_work:extension')->defaultValue('');
    $this->addFieldMapping('field_phone_home')->defaultValue('us');
    $this->addFieldMapping('field_phone_home:number', 'phone_home');
    $this->addFieldMapping('field_phone_mobile')->defaultValue('us');
    $this->addFieldMapping('field_phone_mobile:number', 'phone_cell');
    $this->addFieldMapping('field_fax')->defaultValue('us');
    $this->addFieldMapping('field_fax:number', 'phone_fax');
    $this->addUnmigratedDestinations(array('revision_uid', 'field_join_date:rrule', 'field_join_date:to',
      'field_phone_home:extension', 'field_fax:extension', 'field_phone_mobile:extension',
      'field_address:sub_premise', 'field_address:sub_administrative_area', field_address:dependent_locality',
      'field_address:data'));
   $this->addUnmigratedSources(array('nonmember', 'memberorg', 'memberindiv'));
  }

So far, so good. No surprises here and now you see why this could be part of your standard user migration.

Here's where the work begins.

  public function prepareRow($current_row) {
    $source_id = $current_row->uid;

    $connection = rsb_migration2_get_source_connection();
    $query = $connection->select('profile_values', 'pv') // The data in each profile field is stored here.
      ->fields('pv', array('value'))
      ->condition('pv.uid', $source_id, '=');
    $query->join('profile_fields', 'pf', 'pv.fid = pf.fid'); // The field names are stored here.
    $query->addField('pf', 'name');
    $query->orderBy('pf.fid', 'ASC');
    $result = $query->execute();

    // The next few lines are what make the whole thing work.
    foreach ($result as $row) {
      $field_name = str_replace('profile_', '', $row->name); // Simplify the field names.
      $current_row->$field_name = $row->value;
    }

    if ($current_row->memberorg == 1) { // Here we convert data from 3 check boxes to one set of radio buttons
      $current_row->membership_type = 1;
    }
    elseif ($current_row->memberindiv == 1) {
      $current_row->membership_type = 0;
    }
    else $current_row->membership_type = 3;

    if ($current_row->sendinfo == '') { // Get rid of NULL values from D6 table
      $current_row->sendinfo = 0;
    }

    // You need to pull date parts from serialized data in profile date fields. Convert it to a format the date api understands, then convert the string to a date.
    if (isset($current_row->memberdate)) {
      $dateparts = unserialize($current_row->memberdate);
      $current_row->memberdate = $dateparts['month'] . '/' . $dateparts['day'] . '/' . $dateparts['year'];
      $current_row->memberdate = date('Y-m-d', strtotime($current_row->memberdate));
    }
    $current_row->full_name = $current_row->fname . '' . $current_row->lname;

    // This sanitizes phone numbers because cck_phone stores them as strings of numbers. This may not be absolutely necessary because cck_phone has a function to clean up numbers.
    $symbols = array('(', ')', '', '-', '.');
    $current_row->phone_work = str_replace($symbols, '', $current_row->phone_work);
    $current_row->phone_home = str_replace($symbols, '', $current_row->phone_home);
    $current_row->phone_mobile = str_replace($symbols, '', $current_row->phone_cell);
    $current_row->phone_fax = str_replace($symbols, '', $current_row->phone_fax);

  }
  return TRUE;

}

That's really all there is to it.

Thanks to Mike Ryan, BTMash and others from whom I learned.


Viewing all articles
Browse latest Browse all 426

Trending Articles