You are here

function term_merge_action in Term Merge 7

Action function. Perform action "Term Merge".

7 string references to 'term_merge_action'
EntityReferenceTermMergeWebTestCase::testEntityReferenceField in ./term_merge.test
Verify that entity reference field values get update upon term merging.
RedirectTermMergeWebTestCase::testTermMergeAction in ./term_merge.test
Test the action 'term_merge_action' in terms of integration with Redirect.
SynonymsTermMergeWebTestCase::testTermMergeAction in ./term_merge.test
Test the action 'term_merge_action' in terms of integration with Synonyms.
TermMergeTermMergeWebTestCase::testTermMerge in ./term_merge.test
Test merging two terms.
TermMergeTermMergeWebTestCase::testTermMergeResistance in ./term_merge.test
Test all cases for potentially "buggy" input.

... See full list

File

./term_merge.module, line 304
Provide functionality for merging taxonomy terms one into another.

Code

function term_merge_action($object, $context) {
  $term_branch = $object;
  $term_trunk = taxonomy_term_load($context['term_trunk']);
  $vocabulary = taxonomy_vocabulary_load($term_branch->vid);
  $term_branch_children = array();
  foreach (taxonomy_get_tree($term_branch->vid, $term_branch->tid) as $term) {
    $term_branch_children[] = $term->tid;
  }
  if ($term_branch->vid != $term_trunk->vid) {
    watchdog('term_merge', 'Trying to merge 2 terms (%term_branch, %term_trunk) from different vocabularies', array(
      '%term_branch' => $term_branch->name,
      '%term_trunk' => $term_trunk->name,
    ), WATCHDOG_WARNING);
    return;
  }
  if ($term_branch->tid == $term_trunk->tid) {
    watchdog('term_merge', 'Trying to merge a term %term into itself.', array(
      '%term' => $term_branch->name,
    ), WATCHDOG_WARNING);
    return;
  }
  if (in_array($term_trunk->tid, $term_branch_children)) {
    watchdog('term_merge', 'Trying to merge a term %term_branch into its child %term_trunk.', array(
      '%term_branch' => $term_branch->name,
      '%term_trunk' => $term_trunk->name,
    ), WATCHDOG_WARNING);
    return;
  }

  // Defining some default values.
  if (!isset($context['term_branch_keep'])) {

    // It's easier to manually delete the unwanted terms, rather than
    // search for your DB back up. So by default we keep the term branch.
    $context['term_branch_keep'] = TRUE;
  }
  if (!isset($context['merge_fields'])) {

    // Initializing it with an empty array if client of this function forgot to
    // provide info about what fields to merge.
    $context['merge_fields'] = array();
  }
  if (!isset($context['keep_only_unique'])) {

    // Seems logical that mostly people will prefer to keep only one value in
    // term reference field per taxonomy term.
    $context['keep_only_unique'] = TRUE;
  }
  if (!isset($context['redirect']) || !module_exists('redirect')) {

    // This behavior requires Redirect module installed and enabled.
    $context['redirect'] = TERM_MERGE_NO_REDIRECT;
  }
  if (!isset($context['synonyms']) || !module_exists('synonyms')) {

    // This behavior requires Synonyms module installed and enabled.
    $context['synonyms'] = NULL;
  }

  // Calling a hook, this way we let whoever else to react and do his own extra
  // logic when merging of terms occurs. We prefer to call it before we handle
  // our own logic, because our logic might delete $term_branch and maybe a
  // module that implements this hook needs this term not deleted yet.
  module_invoke_all('term_merge', $term_trunk, $term_branch, $context);
  if (!empty($context['merge_fields'])) {

    // "Merging" the fields from $term_branch into $term_trunk where it is
    // possible.
    foreach ($context['merge_fields'] as $field_name) {

      // Getting the list of available languages for this field.
      $languages = array();
      if (isset($term_trunk->{$field_name}) && is_array($term_trunk->{$field_name})) {
        $languages = array_merge($languages, array_keys($term_trunk->{$field_name}));
      }
      if (isset($term_branch->{$field_name}) && is_array($term_branch->{$field_name})) {
        $languages = array_merge($languages, array_keys($term_branch->{$field_name}));
      }
      $languages = array_unique($languages);

      // Merging the data of both terms into $term_trunk.
      foreach ($languages as $language) {
        if (!isset($term_trunk->{$field_name}[$language])) {
          $term_trunk->{$field_name}[$language] = array();
        }
        if (!isset($term_branch->{$field_name}[$language])) {
          $term_branch->{$field_name}[$language] = array();
        }
        $items = array_merge($term_trunk->{$field_name}[$language], $term_branch->{$field_name}[$language]);
        $unique_items = array();
        foreach ($items as $item) {
          $unique_items[serialize($item)] = $item;
        }
        $items = array_values($unique_items);
        $term_trunk->{$field_name}[$language] = $items;
      }
    }

    // And now we can save $term_trunk after shifting all the fields from
    // $term_branch.
    taxonomy_term_save($term_trunk);
  }
  $result = array();
  foreach (term_merge_fields_with_foreign_key('taxonomy_term_data', 'tid') as $field) {
    $result[$field['field_name']] = array();
    $query = new EntityFieldQuery();

    // Making sure we search in the entire scope of entities.
    $query
      ->addMetaData('account', user_load(1));
    $query
      ->fieldCondition($field['field_name'], $field['term_merge_field_column'], $term_branch->tid);
    $_result = $query
      ->execute();
    $result[$field['field_name']]['entities'] = $_result;
    $result[$field['field_name']]['column'] = $field['term_merge_field_column'];
  }

  // Now we load all entities that have fields pointing to $term_branch.
  foreach ($result as $field_name => $field_data) {
    $column = $field_data['column'];
    foreach ($field_data['entities'] as $entity_type => $v) {
      $ids = array_keys($v);
      $entities = entity_load($entity_type, $ids);

      // After we have loaded it, we alter the field to point to $term_trunk.
      foreach ($entities as $entity) {

        // What is more, we have to do it for every available language.
        foreach ($entity->{$field_name} as $language => $items) {

          // Keeping track of whether term trunk is already present in this
          // field in this language. This is useful for the option
          // 'keep_only_unique'.
          $is_trunk_added = FALSE;
          foreach ($entity->{$field_name}[$language] as $delta => $item) {
            if ($context['keep_only_unique'] && $is_trunk_added && in_array($item[$column], array(
              $term_trunk->tid,
              $term_branch->tid,
            ))) {

              // We are instructed to keep only unique references and we already
              // have term trunk in this field, so we just unset value for this
              // delta.
              unset($entity->{$field_name}[$language][$delta]);
            }
            else {

              // Merging term references if necessary, and keep an eye on
              // whether we already have term trunk among this field values.
              switch ($item[$column]) {
                case $term_trunk->tid:
                  $is_trunk_added = TRUE;
                  break;
                case $term_branch->tid:
                  $is_trunk_added = TRUE;
                  $entity->{$field_name}[$language][$delta][$column] = $term_trunk->tid;
                  break;
              }
            }
          }

          // Above in the code, while looping through all deltas of this field,
          // we might have unset some of the deltas to keep term references
          // unique. We should better keep deltas as a series of consecutive
          // numbers, because it is what it is supposed to be.
          $entity->{$field_name}[$language] = array_values($entity->{$field_name}[$language]);
        }

        // Integration with workbench_moderation module. Without this code, if
        // we save the node for which workbench moderation is enabled, then
        // it will go from "published" state into "draft". Though in fact we do
        // not change anything in the node and therefore it should persist in
        // published state.
        if (module_exists('workbench_moderation') && $entity_type == 'node') {
          $entity->workbench_moderation['updating_live_revision'] = TRUE;
        }

        // After updating all the references, save the entity.
        entity_save($entity_type, $entity);
      }
    }
  }

  // Adding term branch as synonym (Synonyms module integration).
  if ($context['synonyms']) {
    term_merge_add_entity_as_synonym($term_trunk, 'taxonomy_term', $context['synonyms'], $term_branch, 'taxonomy_term');
  }

  // It turned out we gotta go tricky with the Redirect module. If we create
  // redirection before deleting the branch term (if we are instructed to delete
  // in this action) redirect module will do its "auto-clean up" in
  // hook_entity_delete() and will delete our just created redirects. But at the
  // same time we have to get the path alias of the $term_branch before it gets
  // deleted. Otherwise the path alias will be deleted along with the term
  // itself. Similarly would be lost all redirects pointing to branch term
  // paths. We will redirect normal term path and its RSS feed.
  $redirect_paths = array();
  if ($context['redirect'] != TERM_MERGE_NO_REDIRECT) {
    $redirect_paths['taxonomy/term/' . $term_trunk->tid] = array(
      'taxonomy/term/' . $term_branch->tid,
    );
    $redirect_paths['taxonomy/term/' . $term_trunk->tid . '/feed'] = array(
      'taxonomy/term/' . $term_branch->tid . '/feed',
    );
    foreach ($redirect_paths as $redirect_destination => $redirect_sources) {

      // We create redirect from Drupal normal path, then we try to fetch its
      // alias. Lastly we collect a set of redirects that point to either of the
      // 2 former paths. Everything we were able to fetch will be redirecting to
      // the trunk term.
      $alias = drupal_get_path_alias($redirect_sources[0]);
      if ($alias != $redirect_sources[0]) {
        $redirect_sources[] = $alias;
      }
      $existing_redirects = array();
      foreach ($redirect_sources as $redirect_source) {
        foreach (redirect_load_multiple(array(), array(
          'redirect' => $redirect_source,
        )) as $v) {
          $existing_redirects[] = $v->source;
        }
      }
      $redirect_paths[$redirect_destination] = array_unique(array_merge($redirect_sources, $existing_redirects));
    }
  }
  if (!$context['term_branch_keep']) {

    // If we are going to delete branch term, we need firstly to make sure
    // all its children now have the parent of term_trunk.
    foreach (taxonomy_get_children($term_branch->tid, $vocabulary->vid) as $child) {
      $parents = taxonomy_get_parents($child->tid);

      // Deleting the parental link to the term that is being merged.
      unset($parents[$term_branch->tid]);

      // And putting the parental link to the term that we merge into.
      $parents[$term_trunk->tid] = $term_trunk;
      $parents = array_unique(array_keys($parents));
      $child->parent = $parents;
      taxonomy_term_save($child);
    }

    // Views module integration. We update all Views taxonomy filter handlers
    // configured to filter on term branch to filter on term trunk now, since
    // the former becomes the latter.
    if (module_exists('views')) {
      $views = views_get_all_views();
      foreach ($views as $view) {

        // For better efficiency, we keep track of whether we have updated
        // anything in a view, and thus whether we need to save it.
        $needs_saving = FALSE;

        // Even worse, we have to go through each display of each view.
        foreach ($view->display as $display_id => $display) {
          $view
            ->set_display($display_id);
          $filters = $view->display_handler
            ->get_handlers('filter');
          foreach ($filters as $filter_id => $filter_handler) {

            // Currently we know how to update filters only of this particular
            // class.
            if (get_class($filter_handler) == 'views_handler_filter_term_node_tid') {
              $filter = $view
                ->get_item($display_id, 'filter', $filter_id);
              if (isset($filter['value'][$term_branch->tid])) {

                // Substituting term branch with term trunk.
                unset($filter['value'][$term_branch->tid]);
                $filter['value'][$term_trunk->tid] = $term_trunk->tid;
                $view
                  ->set_item($display_id, 'filter', $filter_id, $filter);
                $needs_saving = TRUE;
              }
            }
          }
        }
        if ($needs_saving) {
          $view
            ->save();
        }
      }
    }

    // We are instructed to delete the term branch after the merge,
    // and so we do.
    taxonomy_term_delete($term_branch->tid);
  }

  // Here we do the 2nd part of integration with the Redirect module. Once the
  // branch term has been deleted (if deleted), we can add the redirects
  // without being afraid that the redirect module will delete them in its
  // hook_entity_delete().
  foreach ($redirect_paths as $redirect_destination => $redirect_sources) {
    foreach ($redirect_sources as $redirect_source) {
      $redirect = redirect_load_by_source($redirect_source);
      if (!$redirect) {

        // Seems like redirect from such URI does not exist yet, we will create
        // it.
        $redirect = new stdClass();
        redirect_object_prepare($redirect, array(
          'source' => $redirect_source,
        ));
      }
      $redirect->redirect = $redirect_destination;
      $redirect->status_code = $context['redirect'];
      redirect_save($redirect);
    }
  }
  watchdog('term_merge', 'Successfully merged term %term_branch into term %term_trunk in vocabulary %vocabulary. Context: @context', array(
    '%term_branch' => $term_branch->name,
    '%term_trunk' => $term_trunk->name,
    '%vocabulary' => $vocabulary->name,
    '@context' => var_export($context, 1),
  ));
}