Hello, I'm having trouble to migrate a hierarchical taxonomy structure using parent_name. I hope you can help me to figure out how this works. I tried to suck up all documentation I could find.

I'm trying to create a hierarchical taxonomy from a perpared migration database table which looks like that:

ID | NAME | PARENT
------------------
 0 | FOO  |       
 1 | BAR  |       
 2 | LORE | FOO
 3 | IPSE | FOO
 4 | DOLO | BAR

It's a pretty messy data structure, but fact is there are parent and child terms in one table.

What I did so far is migrating the parent terms first. Now my custom taxonomy looks like this:

_
|_ FOO
|_ BAR

The migration code snipped I used to achieve this works like that:

$this->addFieldMapping('name', 'PARENT');

Until now everything works as expected!

Now, in a second migration step I want to add the child terms like that to my taxonomy:

_
|_ FOO
  |_ LORE
  |_ IPSE
|_ BAR
  |_ DOLO

What I tried is to add a hierarchical mapping to my migration:

$this->addFieldMapping('name', 'NAME');
$this->addFieldMapping('parent_name', 'PARENT')
    ->arguments(array('source_type' => 'name'));

But this does not work, the result is an unhierarchical list like that:

_
|_ FOO
|_ LORE
|_ IPSE
|_ BAR
|_ DOLO

Could you help me? Thanks!

Posted this question here first, but didn't recieve any attention.

Comments

Anonymous’s picture

You're on the right track.

What destination handler are you using? Are you sure it accepts the parent field? You can tell if it is listed in the UI by going ot the migration and reviewing the destination fields.

You can use a variable name in the addFieldMapping parameters and define the things later on if you need to do some manipulation.

You probably have a "public function prepareRow($row)" where you can dpm($row) to see if the data is getting through. If you have a "public function prepare($entity, $row)" which runs after that previous function you can then dpm($entity) and see what your destination handler is expecting.

You might want to consider upgrading to migrate-2.6 and migrate_d2d-2.1 so you can map fields in the UI. If you've been custom coding your code should still be vaild as long as you are not using "automatic registration" (which isn't in the docs anymore).

donSchoe’s picture

Issue summary: View changes
Status: Active » Closed (works as designed)

Hi Ryan, thanks for your help.

After some intense debugging I noticed my script is working as desired but my database was not having parent tags. *dang*

This works just fine:

$this->addFieldMapping('name', 'NAME');
$this->addFieldMapping('parent_name', 'PARENT')
    ->arguments(array('source_type' => 'name'));

Your hint with DPM($ROW) finally pointed me to the issue. Thanks again.
donSchoe

guypaddock’s picture

Status: Closed (works as designed) » Active

I am guessing that the issue is actually the order in which the data appears -- parents need to come before their children?

Any suggestions on how to accomplish this without having to sort the data topologically?

guypaddock’s picture

Status: Active » Closed (works as designed)

Posting this here in case anyone else needs a pointer.

Since I'm using UUID with a taxonomy hierarchy, I ended up implementing a custom destination handler (part of this should probably be contributed to #1870886: Add support for UUID module for 7.x), as follows:

/**
 * Special migration destination handler that imports taxonomy terms by UUID,
 * preserving hierarchy.
 * 
 * If the terms in the source data are not guaranteed to be ordered such that
 * parent terms always precede their children, then this handler requires
 * special multi-pass handling in the migration to ensure that all terms
 * are imported.
 * 
 * @see    #hasDeferredImports()
 * @see    #runDeferredImports()
 * 
 * @author Guy Paddock (guy.paddock@redbottledesign.com)
 */
class MigrateDestinationUuidTerm extends MigrateDestinationTerm {
  protected $deferredImports;
  
  /**
   * Constructor for MigrateDestinationUuidTerm.
   * 
   * @see           MigrateDestinationTerm::__construct()
   * 
   * @param string  $bundle
   *                The machine name of the taxonomy vocabulary.
   *                
   * @param array   $options
   *                An optional array of options to control the import process.
   */
  public function __construct($bundle, array $options = array()) {
    parent::__construct($bundle, $options);
    
    $this->deferredImports = array();
  }
  
  /**
   * Returns the fields that are exposed by this migration destination.
   * 
   * The only additional field is "Parent (by UUID)".
   */
  public function fields($migration = NULL) {
    $fields = parent::fields();
    
    $fields['parent_uuid'] = t('Parent (by UUID).');

    return $fields;
  }
  
  /**
   * Import the provided term, using information from the provided row.
   * 
   * @see MigrateDestinationTerm::import()
   */
  public function import(stdClass $term, stdClass $row) {
    // Look up parent name if provided
    if (isset($term->parent_uuid)) {
      $parent_uuid = trim($term->parent_uuid);
      
      if (!empty($parent_uuid)) {
        $parent_tid = entity_get_id_by_uuid('taxonomy_term', array($parent_uuid));
        
        if (!empty($parent_tid)) {
          $term->parent = $parent_tid[$parent_uuid];
        }
        
        else {
          $deferredImport = new stdClass();

          $deferredImport->term = $term;
          $deferredImport->row  = $row;

          $this->deferredImports[] = $deferredImport;
        }
      }
    }
    
    $tid = parent::import($term, $row);
    
    return $tid;
  }
  
  /**
   * Check whether or not this destination has terms for which the import was
   * "deferred" because their parent term could not be found.
   * 
   * @return  boolean TRUE if there are terms for which import was deferred;
   *                  FALSE, otherwise.
   */
  public function hasDeferredImports() {
    return (count($this->deferredImports) > 0);
  }
  
  /**
   * Return the UUIDs of any terms for which import was deferred.
   * 
   * @return array  An array of UUID keys for all deferred term imports.
   */
  public function getDeferredImportKeys() {
    $keys = array();
    
    foreach ($this->deferredImports as $import) {
      $keys[] = $import->term->uuid;
    }
    
    return $keys;
  }
  
  /**
   * Attempt to import any terms for which import was previously deferred.
   * 
   * This will run import() again for each term and row, which will again
   * attempt to match up each term with its parent term.
   * 
   * Any term for which a parent term is still not found will be returned to
   * the set of deferred imports. If the import is a multi-level hierarchy,
   * this method can be invoked multiple times until every level of the
   * hierarchy has been successfully imported.
   */
  public function runDeferredImports() {
    $currentDeferredImports = $this->deferredImports;
    
    // Clear the list of deferred imports, to be rebuilt during the import.
    $this->deferredImports  = array();
    
    foreach ($currentDeferredImports as $import) {
      // This will automatically re-queue any imports that still need to be deferred.
      $this->import($import->term, $import->row);
    }
  }
}

Then, in my migration, I override postImport():

class MyMigration extends Migration {
  // ... off-topic code omitted ... //

  function postImport() {
    /* @var $destination MigrateDestinationUuidTerm */
    $destination = $this->destination;
    
    $remainingPassLimit = 100;
    
    /* Keep going until we have no more orphan terms, or we're stuck in a
     * seemingly infinite loop (only happens with bad data).
     */
    while ($destination->hasDeferredImports() && ($remainingPassLimit > 0)) {
      $destination->runDeferredImports();
      
      --$remainingPassLimit;
    }
    
    // Display an error if we still have orphans left over.
    if ($destination->hasDeferredImports()) {
      drupal_set_message(
        t("One or more terms could not be imported because their parent terms could not be found: (%terms)",
          array('%terms' => implode(", ", $destination->getDeferredImportKeys()))),
        'warning');
    }
  }
}
guypaddock’s picture

Anyone using my code snippet above -- it doesn't appear to work as well as it should. I am working on a new way to do these migrations.