Introduction
The World Economic Forum (the Forum) runs WELCOM, their internal collaboration platform on Drupal. The Forum is an independent international organization committed to improving the state of the world by engaging business, political, academic and other leaders. It is best known for its annual conference in Davos, Switzerland. These world leaders use WELCOM during and after the conference to work on major issues such as climate change, world health, poverty, etc. It is vitally important for our planet that WELCOM serves its users well.
Before Drupal
WELCOM has been around for quite a while. When the Acquia Migration team arrived, WELCOM was an impressive mashup of Flash, JBoss, LDAP, custom web services in C#, Sharepoint Web Services, MS Exchange, and MS SQL Server. Acquia decided early on to focus on the MS SQL Server layer and migrate data directly from there. Acquia employed one of its favorite open source weapons, the migrate module. Having already developed a source plugin for MS SQL Server on the Examiner.com project, Acquia was off and running.
Drupal Commons
WELCOM is a full featured social networking application. Users have profile fields with granular privacy control. Users may join groups and post public or private content into those groups. Groups have wikis, documents, discussions, events, etc. Users have mutually confirmed friend relationships. Users have activity streams (aka news feed). To fulfill all these needs in Drupal, the Forum turned to Drupal Commons.
Building the Migrations
An early development task is to break up the project into several distinct migrations
A migration represents a flow from one set of source data (typically the output of a database query) to a Drupal content type. Destinations can include nodes, taxonomy terms, users, profiles, comments, or private messages. Here are some migrations at the Forum:
- Users
- Discussions
- Documents
- Profiles
- User relationships (friends)
- Friend requests, with audit trail
- Communities and memberships (organic groups)
Write Migration code
The heart of a migration class is to specify a SQL query or other method of fetching the source data and also define mappings between source columns and Drupal properties such as $node->status, $user->mail, $comment->subject, etc. Here is the user migration:
class UserWelcomMigration extends WelcomMigration {
public function __construct() {
parent::__construct();
// Source system has username is unique id.
$this->map = new MigrateSQLMap($this->machineName,
array('Username' => array(
'type' => 'varchar',
'length' => 50,
'not null' => TRUE,
)
),
MigrateDestinationUser::getKeySchema()
);
$query = 'SELECT p.Username, p.EmailAddress, p.PersonEmailAddress1, p.PersonEmailAddress2, ups.* FROM PROFILE p LEFT JOIN UserPrivacySettings ups ON p.Username = ups.UsernameFK WHERE :criteria ORDER BY p.Username ASC';
$count_query = 'SELECT COUNT(*) FROM PROFILE';
$this->source = new MigrateSourceMSSQL($this->db, $query, $count_query);
$this->destination = new MigrateDestinationUser();
// Mapped fields
$this->addFieldMapping('name', 'Username');
$this->addFieldMapping('mail', 'EmailAddress');
$this->addFieldMapping('status')
->defaultValue(1);
}
function prepare(stdClass $account, stdClass $row) {
// Give these users an email address so the account is valid.
if (empty($account->mail)) {
$account->mail = $row->Username . '@example.com';
}
$map = array(
'field_hometown' => 'HomeTown',
'field_birthday' => 'Birthday',
'field_spouse' => 'Spouse',
'field_little_known_facts' => 'LittleKnownFacts'
'my_contacts' => 'MyContacts',
'field_primary_email' => 'PrimaryEmail',
'field_work_phone' => 'WorkPhone',
'field_mobile_phone' => 'MobilePhone',
);
foreach ($map as $drupal => $mssql) {
if (isset($row->$mssql)) {
$account->cck_profile_privacy[$drupal] = $row->$mssql;
}
}
}
}
The highlights here are:
- Specify a mapping between the source id (username) and the destination ID (user ID).
- Provide a fetch query which pulls columns from the source.
- Provide mappings
between source columns and properties in the Drupal $user object. - Use the 'prepare' hook to massage data that can't be directly mapped. Here we provide an email address where none
exists in the source. Drupal is a bit unhappy when it doesn't have an email for a user. We also build an array of profile privacy values in the user object.
Write Destination Plugins
Migrate and Migrate Extras modules provide lots of plugins which know how to insert and report on data managed by CCK, Location, Flag, etc. Here is an example of such a plugin written for the Forum. This plugin is for migration of Organic Group (OG) Memberships:
class MigrateDestinationOGMembership extends MigrateDestination {
function getKeySchema() {
return array(
'gid' => array(
'type' => 'int',
'not null' => TRUE,
'description' => 'NID of group',
),
'uid' => array(
'type' => 'int',
'not null' => TRUE,
'description' => 'UID of member',
),
);
}
/**
* Delete a membership.
* @param $id
* ID to be deleted.
*/
function rollback(array $id) {
og_delete_subscription($id['gid'], $id['uid']);
}
/**
* Import a single membership.
*
* @param $entity
* Object object to build. Prefilled with any fields mapped in the Migration.
* @param $row
* Raw source data object - passed through to prepare/complete handlers.
* @return array
* Array of key fields of the object that was saved if
* successful. FALSE on failure.
*/
function import(stdClass $entity, stdClass $row) {
if (empty($entity->gid)) throw new MigrateException('Missing $entity->gid');
if (empty($entity->uid)) throw new MigrateException('Missing $entity->uid');
$args = array(
'is_active' => $entity->is_active,
'is_admin' => $entity->is_admin,
'created' => $entity->created,
);
og_save_subscription($entity->gid, $entity->uid, $args);
// No failure handling in OG
return array($entity->gid, $entity->uid);
}
function fields() {
return array(
'gid' => 'Group node id',
'uid' => 'User ID',
'is_active' => 'User\'s group membership is active (1) or pending (0)',
'is_admin' => 'Is member an administrator in this group',
'created' => 'Create date for this membership. Defaults to time()',
);
}
}
As you can see, a destination plugin is a pretty simple class. This one implements 4 brief methods. The most important one is the import() method, which handles the saving into Drupal for each record in the source data. We use the OG API functions to do so, so that all hooks are properly called and contrib/custom modules can react as needed. This plugin is now part of the Migrate Extras module.
Summary
Thanks to this project and others, Migrate module now knows how to import into all the various Drupal features that make up a social networking site. You can import friends, points, groups, flags, etc. Go forth and confidently re-launch you social networking features with Drupal.