The migrate module supports the definition of different destination types - kinds of objects to be created in a migration. There is built-in support for nodes, users, comments, roles, and taxonomy terms - hooks can be defined for additional destination types.
Here are the steps we took to create the Role destination type. Your destination types will follow a similar pattern.
Implement hook_migrate_types(), returning an array mapped from the internal name of the type (used to build hook names, in particular) to the user-visible type name:
function user_migrate_types() { $types = array('user' => t('User'), 'role' => t('Role')); return $types; }
Implement hook_migrate_fields_<type>, returning an array mapped from the internal field name (within Drupal) to the user-visible name:
function user_migrate_fields_role($type) { $fields = array( 'name' => t('Role name'), ); return $fields; }
Implement hook_migrate_delete_<type>, which takes the unique identifier of the destination object on the Drupal side and deletes that object and everything that depends on that object:
function user_migrate_delete_role($rid) { db_query('DELETE FROM {users_roles} WHERE rid = %d', $rid); db_query('DELETE FROM {permission} WHERE rid = %d', $rid); db_query('DELETE FROM {role} WHERE rid = %d', $rid); }
Now, here’s the major work: implement hook_migrate_import_<type> :
function user_migrate_import_role($tblinfo, $row) { $errors = array(); $newrole = array(); // For each destination field, populate it with the source value if present, and if // if not use the specified default value foreach ($tblinfo->fields as $destfield => $values) { if ($values['srcfield'] && isset($row->$values['srcfield'])) { $newvalue = $row->$values['srcfield']; } else { $newvalue = $values['default_value']; } $newrole[$destfield] = $newvalue; } $role_name = $newrole['name']; if ($role_name) { db_query("INSERT INTO {role} (name) VALUES ('%s')", $role_name); $sql = "SELECT * FROM {role} WHERE name='%s'"; $role = db_fetch_object(db_query($sql, $role_name)); // Call completion hooks, for any additional role-related processing // (such as assigning permissions) timer_start('role completion hooks'); $errors = migrate_invoke_all('complete_role', $role, $tblinfo, $row); timer_stop('role completion hooks'); // Fill in the map table, so the migrate module knows this row is done $sourcekey = $tblinfo->sourcekey; migrate_add_mapping($tblinfo->mcsid, $row->$sourcekey, $role->rid); } return $errors; }
$tblinfo contains the meta-information on the content set, and $row is the source data for one object. An array of messages is returned - use migrate_message() to generate a message.
A content set defines the migration from a set of source data (implemented as a view) into a given destination type. Content sets can be defined interactively or programmatically. The typical process to create a content set programmatically would be as follows:
Add an update function to the .install file for a custom module (this document calls it ec_migrate.install). In the update function, first add any source tables to the Table Wizard. Pass TRUE to prevent immediate full analysis of the table (otherwise, for large tables, the update function risks timing out).
tw_add_tables('role_o', TRUE);
If these tables need to relate to other tables, add FK flags to any columns used in joins, and add the relationships:
function tw_add_fk($tablename, $colname) } /** * Add a relationship between two table columns, making it possible to join them in Views * * @param $leftcol * The left side of a potential join, expressed either as a column ID from * {tw_columns} or as a string in [connection.]table.column format. * @param $rightcol * The right side of a potential join, expressed either as a column ID from * {tw_columns} or as a string in [connection.]table.column format. * @param $automatic * Boolean indicating whether to create views joins (i.e., relate the table * automatically) or relationships. */ function tw_add_relationship($leftcol, $rightcol, $automatic = FALSE) {
If the content set is based only on a single source table with a single primary key, and requires no filtering, the default view instantiated by the Table Wizard for that table can be used in the content set. Otherwise, you need to define a view containing all the necessary data as a default view (create the view interactively, export, and paste the code into ec_migrate.views_default.inc). Naming convention is <destination>_content_set (e.g., role_content_set).
Create the content set in your update function, by building the object and saving it:
$content_set = new stdClass; $content_set->view_name = 'role_content_set'; $content_set->sourcekey = 'role_id'; $content_set->contenttype = 'role'; $content_set->description = 'Roles'; $content_set->weight = 9; migrate_save_content_set($content_set, array('base_table' => 'role_o')); $mcsid = $content_set->mcsid;
For each field to automatically be copied from a source field to a destination field, build a mapping object and save it:
$mapping = new stdClass; $mapping->mcsid = $mcsid; $mapping->srcfield = 'role_o_role_name'; $mapping->destfield = 'name'; migrate_save_content_mapping($mapping);
Usually, simply copying source fields to destination fields are not enough - you need to implement a hook to perform further manipulations. A destination type may support a prepare hook such as hook_migrate_prepare_OBJECT (called for each object after the automatic mappings are applied but before the destination is saved) and/or a complete hook (called after the destination is saved). The standard signature is:
function ec_migrate_migrate_prepare_user(&$account, $tblinfo, $row) // Role assignments timer_start('role assignments'); // Everyone's a contributor if (!isset($account['roles'])) { $account['roles'] = array(); } static $contributor_rid; if (!isset($contributor_rid)) { install_include(array('user')); $contributor_rid = install_get_rid('contributor'); } $account['roles'][$contributor_rid] = $contributor_rid; $sql = "SELECT map.destid AS rid FROM {customer_role} cr INNER JOIN {role_content_set_map} map ON cr.role_id=map.sourceid WHERE cr.customer_id=%d AND cr.active='Y' AND (cr.expires IS NULL OR cr.expires > NOW())"; $result = db_query($sql, $row->customer_id); while ($row = db_fetch_object($result)) { $account['roles'][$row->rid] = $row->rid; } timer_stop('role assignments'); return $errors; }