View source
<p>While simple migration projects may be accomplished purely through the administrative interference,
complex real-world cases will require some programming. The migrate module, at its heart, is simply
a framework for defining and performing migrations, which happens to come with default implementations
for the most important core objects. Developers can use the same API as these default implementations to
enhance migration to those objects, or to define other destinations.</p>
<!-- -->
<h3>Destinations</h3>
<p>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.</p>
<p>Here are the steps we took to create the Role destination type. Your destination types will
follow a similar pattern.</p>
<ul>
<li><p>Implement <strong>hook_migrate_types()</strong>, returning an array mapped
from the internal name of the type (used to build hook names, in particular) to the user-visible
type name:</p>
<p><pre>function user_migrate_types() {
$types = array('user' => t('User'), 'role' => t('Role'));
return $types;
}</pre></p></li>
<li><p>Implement <strong>hook_migrate_fields_<type></strong>, returning an
array mapped from the internal field name (within Drupal) to the user-visible name:</p>
<p><pre>function user_migrate_fields_role($type) {
$fields = array(
'name' => t('Role name'),
);
return $fields;
}</pre></p></li>
<li><p>Implement <strong>hook_migrate_delete_<type></strong>, which takes the
unique identifier of the destination object on the Drupal side and deletes that object and
everything that depends on that object:</p>
<p><pre>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);
}</pre></p></li>
<li><p>Now, here’s the major work: implement <strong>hook_migrate_import_<type></strong> :</p>
<p><pre>
function user_migrate_import_role($tblinfo, $row) {
$errors = array();
$newrole = array();
// Handle the update case
if ($row->destid) {
$new_role['rid'] = $row->destid;
}
// 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;
}</pre></p></li>
</ul>
<p>$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. </p>
<h2 id="contentsets">Content sets</h2>
<p>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 (NOTE:
this describes usage of the Table Wizard module for views-enabling your tables. Usage of
the Data module (now preferred) is TBD.):</p>
<ul>
<li><p>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).</p>
<p><pre>tw_add_tables('role_o', TRUE);</pre></p></li>
<li><p>If these tables need to relate to other tables, add FK flags to any columns used in joins,
and add the relationships:</p>
<p><pre>
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) {
</pre></p></li>
<li><p>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).</p></li>
<li><p>Create the content set in your update function, by building the object and saving it:</p>
<p><pre>$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;</pre></p>
<p>For each field to automatically be copied from a source field to a destination field, build
a mapping object and save it:</p>
<p><pre>$mapping = new stdClass;
$mapping->mcsid = $mcsid;
$mapping->srcfield = 'role_o_role_name';
$mapping->destfield = 'name';
migrate_save_content_mapping($mapping);</pre></p></li>
<li><p>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 <strong>hook_migrate_prepare_OBJECT</strong> (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:</p>
<p><pre>
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;
}
</pre></p>
<ul>
<li>$account - The destination object (in this case, a user account object)
<li>$tblinfo - Information on the content set itself
<li>$row - The source data
</ul>
</li>