BTMash

Blob of contradictions

Migrating Content part 3: Nodes with your own field handlers

Wed, 04/27/2011 - 14:45 -- btmash

(Updated June 4, 2011 - address new handler requirements with Migrate 2.1 by removing stdClass)

In this finale (?) on using the Migrate module, I will go through how to go about writing your very own field handler to pull in content into a field type that does not yet have a mapped path. In this example, we will be migrating an events (so the date field from the date module will require a migration path). At the time of writing, http://drupal.org/node/1021076 had not yet been resolved (or have the ability for date_to and date_from to be established) so we will write out a field handler that handles just this!

The first article discussed the merits of using the Migrate module. The second article was a walkthrough on how to import users. The third article was a walkthrough on how to import basic nodes.

Much like last time, we first define our migration class

  1. class MyEventMigration extends Migration {
  2. // ...
  3. }

And this class will consist of two functions - one is the class constructor while the other is for massaging/adding extra information on a per row basis.

  1. /**
  2.  * Class Constructor
  3.  */
  4. public function __construct() {
  5. // ...
  6. }
  7.  
  8. /**
  9.  * Add any additional data / clean up data for the current row / node that will be added/updated.
  10.  */
  11. public function prepareRow($current_row) {
  12. // ...
  13. }

In this full scenario, if you follow the steps that are described in the node migration walkthrough, you should be most of the way there. However, we need to define a way to map out our event information so we define the field handler that will handle our events. Much like how we defined certain pieces of data into classes such as the MigrateTextFieldHandler or the MigrateFileFieldHandler, let's call this one MigrateDateFieldHandler:

  1. // Define a way to migrate the date field.
  2. class MigrateDateFieldHandler extends MigrateFieldHandler {
  3. // Define the constructor of our Handler - it lets Migrate know what *types* of fields this handler is compatible against.
  4. public function __construct() {
  5. // ...
  6. }
  7.  
  8. // Define our arguments on where this data will be found during the preparation.
  9. static function arguments($start_date = NULL, $end_date = NULL, $rrule = NULL, $separator = NULL) {
  10. // ...
  11. }
  12.  
  13. // Based on the entity and return an array of field values to get processed.
  14. public function prepare($entity, array $field_info, array $instance, array $values) {
  15. // ...
  16. }
  17. }

DISCLAIMER: To note, the arguments() structure is based on how the handling was created for files (there are separators for each row of data so would have '2011-01-01 12:30|2011-01-02 1:30|## 2011-01-04 8:30|2011-01-10 10:00|')
First comes the implementation of the constructor:

  1. public function __construct() {
  2. // Handle the various date field types
  3. $this->registerTypes(array('date', 'datestamp', 'datetime'));
  4. }

Simple! Now migrate knows that this class will only handle these types of fields (so links, files, text will not get processed and instead throw an error).
Next comes the implementation of the arguments

  1. static function arguments($start_date = NULL, $end_date = NULL, $rrule = NULL, $separator = NULL) {
  2. // Allow the prepare to know whether these arguments are fixed or their position in an array. The separator will split the data.
  3. $arguments = array(
  4. 'start_date' => $start_date,
  5. 'end_date' => $end_date,
  6. 'rrule' => $rrule,
  7. 'separator' => $separator,
  8. 'timezone' => variable_get('date_default_timezone', @date_default_timezone_get()),
  9. );
  10. return $arguments;
  11. }

And finally, prepare() needs to be implemented:

  1. public function prepare($entity, array $field_info, array $instance, array $values) {
  2. // Get the list of values along with the arguments that were provided
  3. if (isset($values['arguments'])) {
  4. $arguments = $values['arguments'];
  5. unset($values['arguments']);
  6. }
  7. $date_type = $field_info['type'];
  8. $timezone = $field_info['settings']['tz_handling'];
  9. $timezone_db = $field_info['settings']['timezone_db'];
  10. $migration = Migration::currentMigration();
  11. $destination = $migration->getDestination();
  12. $language = isset($arguments['language']) ? $arguments['language'] : $destination->getLanguage();
  13. $separator = FALSE;
  14. if (!empty($arguments['separator'])) {
  15. $separator = TRUE;
  16. }
  17.  
  18. $return = array();
  19. foreach ($values as $value) {
  20. if ($separator) {
  21. $date_values = explode($arguments['separator'], $value);
  22. }
  23. else {
  24. //@TODO Figure out base behavior in this scenario. For documentation, assume it is *not* an issue.
  25. }
  26. // Utilize the separator, check if the value is set at the argument and set to that value.
  27. $return[$language][] = array(
  28. 'value' => (isset($arguments['start_date']) && isset($date_values[$arguments['start_date']])) ? $date_values[$arguments['start_date']] : '',
  29. 'value2' => (isset($arguments['end_date']) && isset($date_values[$arguments['end_date']])) ? $date_values[$arguments['end_date']] : '',
  30. 'rrule' => (isset($arguments['rrule']) && isset($date_values[$arguments['rrule']])) ? $date_values[$arguments['rrule']] : '',
  31. 'timezone' => $timezone,
  32. 'timezone_db' => $timezone_db,
  33. );
  34. }
  35. return $return;
  36. }

This will effectively map out the string above with the from/to dates in the correct values of the field.

Now we *return* back to the main Migration class that we have and add in our migration handler for the date(s)

  1. class MyEventMigration extends Migration {
  2. public function __construct() {
  3. // ...
  4. $source_fields = array(
  5. 'nid' => t('The node ID of the page'),
  6. 'front_page_image' => t('The front page image representing the gallery'),
  7. 'right_side_images' => t('The associated images that were on the right side'),
  8. 'linked_files' => t('The set of linked files'),
  9. 'event_date_range' => t('The date range'),
  10. 'event_dates' => t('The individual dates of the event'),
  11. 'terms' => t('The terms for the node'),
  12. );
  13. // ...
  14. // Where the data will be found in my field
  15. $generic_date_arguments = MigrateDateFieldHandler::arguments(0, 1, NULL, '|');
  16. $this->addFieldMapping('field_content_date_range', 'event_date_range')
  17. ->arguments($generic_date_arguments);
  18.  
  19. $this->addFieldMapping('field_content_date_time', 'event_dates')
  20. ->separator('##')
  21. ->arguments($generic_date_arguments);
  22. // ...
  23. }
  24.  
  25. public function prepareRow($current_row) {
  26. // ...
  27. // Handle the date range
  28. $event_date_range_query = db_select(MY_MIGRATION_DATABASE_NAME .'.content_field_event_date', 'cfed')
  29. ->fields('cfed')
  30. ->condition('cfed.vid', $nid, '=');
  31.  
  32. $results = $event_date_range_query->execute();
  33. foreach ($results as $row) {
  34. // This is to account for the lack of a timezone associated with the item. You will not require this
  35. $current_row->event_date_range = str_replace('T00', 'T09', $row->field_event_date_value .'|'. $row->field_event_date_value2 .'|');
  36. }
  37. // Handle the individual event dates.
  38. $event_date_query = db_select(REDCAT_MIGRATION_DATABASE_NAME .'.content_field_date_time', 'cfdt')
  39. ->fields('cfdt')
  40. ->condition('cfdt.vid', $nid, '=')
  41. ->orderBy('cfdt.delta', 'ASC');
  42.  
  43. $results = $event_date_query->execute();
  44. $dates = array();
  45. foreach ($results as $row) {
  46. $dates[] = $row->field_date_time_value .'|'. $row->field_date_time_value2 .'|';
  47. }
  48.  
  49. // Implode the date array with a double hash separator. This is used as the separator from the constructor.
  50. $current_row->event_dates = implode('##', $dates);
  51. // ...
  52. return TRUE;
  53. }
  54.  
  55. }

And with that, you now have your own field handler which can be used by the migrate module to bring in other types of fields/content to your new site (such as links, computed fields, etc).

Instead of posting out the entire module (over 150 lines of code) in here, I have posted the module in a sandbox called 'REDCAT migration' so you can go through and examine the code side by side with this documentation :)

I hope all of this has been helpful. If something doesn't make a lot of sense, leave a comment! I want to keep improving this documentation and hopefully, all of this will be even more useful to other folk looking to migrate content in the Drupal community.