You are here

taxonomy_edge.module in Taxonomy Edge 7.2

Same filename and directory in other branches
  1. 8 taxonomy_edge.module
  2. 6 taxonomy_edge.module
  3. 7 taxonomy_edge.module

Selecting all children of a given taxonomy term can be a pain. This module makes it easier to do this, by maintaining a complete list of edges for each term using the adjecency matrix graph theory.

Example of getting all children from tid:14

SELECT cp.tid FROM taxonomy_term_edge pe INNER JOIN taxonomy_term_edge_path pp ON pe.pid = pp.pid AND pe.distance = 0 INNER JOIN taxonomy_term_edge ce ON ce.parent = pe.pid AND ce.distance > 0 INNER JOIN taxonomy_term_edge_path cp ON ce.pid = cp.pid WHERE pp.tid = 14;

@todo Materialize term id and parent term id into taxonomy_term_edge table to make lookups easier, like in 1.x? @todo Fix concurrency issue for queue rebuild and full tree rebuild (lock_may_be_available() inside transaction) @todo Rewrite SQL using PDO style notation instead? @todo Investigate WHERE temp_pid usage in non-transactional environments

See also

README.txt

File

taxonomy_edge.module
View source
<?php

/**
 * @file
 *
 * Selecting all children of a given taxonomy term can be a pain.
 * This module makes it easier to do this, by maintaining a complete list of
 * edges for each term using the adjecency matrix graph theory.
 *
 * Example of getting all children from tid:14
 *
 * SELECT cp.tid
 * FROM taxonomy_term_edge pe
 * INNER JOIN taxonomy_term_edge_path pp ON pe.pid = pp.pid AND pe.distance = 0
 * INNER JOIN taxonomy_term_edge ce ON ce.parent = pe.pid AND ce.distance > 0
 * INNER JOIN taxonomy_term_edge_path cp ON ce.pid = cp.pid
 * WHERE pp.tid = 14;
 *
 * @todo Materialize term id and parent term id into taxonomy_term_edge table to make lookups easier, like in 1.x?
 * @todo Fix concurrency issue for queue rebuild and full tree rebuild (lock_may_be_available() inside transaction)
 * @todo Rewrite SQL using PDO style notation instead?
 * @todo Investigate WHERE temp_pid usage in non-transactional environments
 *
 * @see README.txt
 */

/**
 * Fail safe for avoiding infite loops when rebuilding edges.
 */
define('TAXONOMY_EDGE_MAX_DEPTH', 100);

/**
 * Default value for realtime building of tree.
 */
define('TAXONOMY_EDGE_BUILD_REALTIME', TRUE);

/**
 * Default value for static caching.
 */
define('TAXONOMY_EDGE_STATIC_CACHING', TRUE);

/**
 * Default value for optimized tree.
 */
define('TAXONOMY_EDGE_OPTIMIZED_GET_TREE', TRUE);

// ---------- HOOKS ----------

/**
 * Implements hook_core_override().
 */
function taxonomy_edge_core_override_info() {
  return array(
    'taxonomy_get_tree' => array(
      'callback' => 'taxonomy_edge_get_tree',
    ),
  );
}

/**
 * Implements hook_help().
 */
function taxonomy_edge_help($section) {
  switch ($section) {
    case 'admin/help#taxonomy_edge':

      // Return a line-break version of the module README.txt
      return check_markup(file_get_contents(dirname(__FILE__) . "/README.txt"));
  }
}

/**
 * Implements hook_perm().
 */
function taxonomy_edge_permission() {
  return array(
    'administer taxonomy edge' => array(
      'title' => t('Administer Taxonomy Edge'),
      'description' => t('Perform administration tasks for Taxonomy Edge.'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function taxonomy_edge_menu() {
  $items = array();

  // Bring /level and /all back into taxonomy pages and feeds
  $items['taxonomy/term/%taxonomy_term/%'] = array(
    'title' => 'Taxonomy term',
    'title callback' => 'taxonomy_term_title',
    'title arguments' => array(
      2,
    ),
    'page callback' => 'taxonomy_edge_term_page',
    'page arguments' => array(
      2,
      3,
    ),
    'access arguments' => array(
      'access content',
    ),
    'file' => 'taxonomy_edge.pages.inc',
  );
  $items['taxonomy/term/%taxonomy_term/%/feed'] = array(
    'title' => 'Taxonomy term',
    'title callback' => 'taxonomy_term_title',
    'title arguments' => array(
      2,
    ),
    'page callback' => 'taxonomy_edge_term_feed',
    'page arguments' => array(
      2,
      3,
    ),
    'access arguments' => array(
      'access content',
    ),
    'file' => 'taxonomy_edge.pages.inc',
  );

  // settings page
  $items['admin/structure/taxonomy/edge'] = array(
    'title' => 'Edge',
    'description' => 'Administer taxonomy edges',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'taxonomy_edge_settings_form',
    ),
    'access arguments' => array(
      'administer taxonomy edge',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'taxonomy_edge.admin.inc',
  );
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/rebuild/%'] = array(
    'title' => 'Rebuild edges',
    'description' => 'Rebuild taxonomy edges',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'taxonomy_edge_rebuild_page_confirm',
      3,
      5,
    ),
    'access arguments' => array(
      'administer taxonomy edge',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'taxonomy_edge.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_cron().
 *
 * Rebuild sorted tree if invalid
 */
function taxonomy_edge_cron() {
  module_load_include('rebuild.inc', 'taxonomy_edge');
  foreach (taxonomy_get_vocabularies() as $vocabulary) {
    if (!taxonomy_edge_is_order_valid($vocabulary->vid)) {
      taxonomy_edge_rebuild_order_batch($vocabulary->vid);
      $batch =& batch_get();
      $batch['progressive'] = FALSE;
      batch_process('admin/content/taxonomy/edge');
    }
  }
}

/**
 * Implements hook_cron_queue_info().
 */
function taxonomy_edge_cron_queue_info() {
  $queues['taxonomy_edge'] = array(
    'worker callback' => 'taxonomy_edge_process_queue_item',
    'time' => 60,
  );
  return $queues;
}

/**
 * Implements hook_taxonomy_term_presave().
 */
function taxonomy_edge_taxonomy_term_presave($term) {
  if (!$term->tid) {
    return;
  }
  $org = !empty($term->original) ? $term->original : taxonomy_term_load($term->tid);
  $modified =& drupal_static('taxonomy_edge_save_check_modified', TRUE);
  $modified = $org->name != $term->name || $org->weight != $term->weight;
}

/**
 * Implements hook_taxonomy_term_insert().
 */
function taxonomy_edge_taxonomy_term_insert($term) {
  if (taxonomy_edge_is_build_realtime($term->vid)) {
    return _taxonomy_edge_taxonomy_term_insert($term);
  }
  else {
    _taxonomy_edge_taxonomy_term_queue($term, 'insert');
  }
}

/**
 * Implements hook_taxonomy_term_update().
 */
function taxonomy_edge_taxonomy_term_update($term) {
  if (taxonomy_edge_is_build_realtime($term->vid)) {
    return _taxonomy_edge_taxonomy_term_update($term);
  }
  else {
    _taxonomy_edge_taxonomy_term_queue($term, 'update');
  }
}

/**
 * Implements hook_taxonomy_term_delete().
 */
function taxonomy_edge_taxonomy_term_delete($term) {
  if (taxonomy_edge_is_build_realtime($term->vid)) {
    return _taxonomy_edge_taxonomy_term_delete($term);
  }
  else {
    _taxonomy_edge_taxonomy_term_queue($term, 'delete');
  }
}

/**
 * Hook into the drag'n'drop interface of terms in order to update tree when
 * terms are reordered.
 *
 * @param array $form
 * @param type $form_state
 */
function taxonomy_edge_form_taxonomy_overview_terms_alter(&$form, &$form_state) {
  $form['#submit'][] = 'taxonomy_edge_reorder_submit';
}

/**
 * Hook into the overview of vocabularies to provide rebuild actions.
 */
function taxonomy_edge_form_taxonomy_overview_vocabularies_alter(&$form, &$form_state) {
  if (user_access('administer taxonomy edge')) {
    foreach (taxonomy_get_vocabularies() as $vocabulary) {
      $form[$vocabulary->vid]['rebuild_edges'] = array(
        '#type' => 'link',
        '#title' => t('rebuild edges'),
        '#href' => "admin/structure/taxonomy/{$vocabulary->machine_name}/rebuild/edges",
      );
      $form[$vocabulary->vid]['rebuild_order'] = array(
        '#type' => 'link',
        '#title' => t('rebuild order'),
        '#href' => "admin/structure/taxonomy/{$vocabulary->machine_name}/rebuild/order",
      );
    }
    $form['#theme'] = 'taxonomy_edge_overview_vocabularies';
  }
}

/**
 * Implementation of hook_theme().
 */
function taxonomy_edge_theme() {
  return array(
    'taxonomy_edge_overview_vocabularies' => array(
      'render element' => 'form',
      'file' => 'taxonomy_edge.theme.inc',
    ),
  );
}

// ---------- HANDLERS ----------

/**
 * Copy/paste from core taxonomy module.
 * This is the penalty for not having a proper abstraction layer!
 * And for not invoking update hook on terms when changing their parents!
 *
 * @param type $form
 * @param type $form_state
 * @return type
 */
function taxonomy_edge_reorder_submit($form, &$form_state) {
  $vocabulary = $form['#vocabulary'];
  $hierarchy = 0;

  // Update the current hierarchy type as we go.
  $changed_terms = array();
  $tree = taxonomy_get_tree($vocabulary->vid);
  if (empty($tree)) {
    return;
  }

  // Build a list of all terms that need to be updated on previous pages.
  $weight = 0;
  $term = (array) $tree[0];
  while ($term['tid'] != $form['#first_tid']) {
    if ($term['parents'][0] == 0 && $term['weight'] != $weight) {
      $term['parent'] = $term['parents'][0];
      $term['weight'] = $weight;
      $changed_terms[$term['tid']] = $term;
    }
    $weight++;
    $hierarchy = $term['parents'][0] != 0 ? 1 : $hierarchy;
    $term = (array) $tree[$weight];
  }

  // Renumber the current page weights and assign any new parents.
  $level_weights = array();
  foreach ($form_state['values'] as $tid => $values) {
    if (isset($form[$tid]['#term'])) {
      $term = $form[$tid]['#term'];

      // Give terms at the root level a weight in sequence with terms on previous pages.
      if ($values['parent'] == 0 && $term['weight'] != $weight) {
        $term['weight'] = $weight;
        $changed_terms[$term['tid']] = $term;
      }
      elseif ($values['parent'] > 0) {
        $level_weights[$values['parent']] = isset($level_weights[$values['parent']]) ? $level_weights[$values['parent']] + 1 : 0;
        if ($level_weights[$values['parent']] != $term['weight']) {
          $term['weight'] = $level_weights[$values['parent']];
          $changed_terms[$term['tid']] = $term;
        }
      }

      // Update any changed parents.
      if ($values['parent'] != $term['parent']) {
        $term['parent'] = $values['parent'];
        $changed_terms[$term['tid']] = $term;
      }
      $hierarchy = $term['parent'] != 0 ? 1 : $hierarchy;
      $weight++;
    }
  }

  // Build a list of all terms that need to be updated on following pages.
  for ($weight; $weight < count($tree); $weight++) {
    $term = (array) $tree[$weight];
    if ($term['parents'][0] == 0 && $term['weight'] != $weight) {
      $term['parent'] = $term['parents'][0];
      $term['weight'] = $weight;
      $changed_terms[$term['tid']] = $term;
    }
    $hierarchy = $term['parents'][0] != 0 ? 1 : $hierarchy;
  }

  // Save all updated terms.
  foreach ($changed_terms as $changed) {
    $term = (object) $changed;
    $term->parent = array(
      $term->parent,
    );
    _taxonomy_edge_taxonomy_term_update($term);
  }
}

/**
 * Cron queue worker
 * Process edge for a queued term.
 * @param integer $vid
 *   Vocabulary ID
 */
function taxonomy_edge_process_queue_item($vid) {
  if (lock_acquire('taxonomy_edge_rebuild_edges_' . $vid)) {
    $queue = DrupalQueue::get('taxonomy_edge_items_' . $vid, TRUE);
    $max = 1000;
    while ($max-- > 0 && ($item = $queue
      ->claimItem())) {
      $term = $item->data;
      switch ($term->operation) {
        case 'insert':
          _taxonomy_edge_taxonomy_term_insert($term);
          break;
        case 'update':
          _taxonomy_edge_taxonomy_term_update($term);
          break;
        case 'delete':
          _taxonomy_edge_taxonomy_term_delete($term);
          break;
      }
      $queue
        ->deleteItem($item);
    }
    lock_release('taxonomy_edge_rebuild_edges_' . $vid);
  }
}

// ---------- PRIVATE HELPER FUNCTIONS ----------

/**
 * Build parent paths for a term.
 *
 * @param integer $tid
 *   Term ID to build parent paths
 * @param array $parents
 *   Parent term IDs for this term
 */
function _taxonomy_edge_build_parents($vid, $tid, $parents = array()) {

  // Add root node if we have parent = 0
  $addroot = FALSE;
  foreach ($parents as $idx => $parent) {
    if ($parent == 0) {
      $addroot = TRUE;
      unset($parents[$idx]);
    }
  }
  $addroot = $addroot || empty($parents);
  if ($parents) {
    $tx = db_transaction();

    // Add paths
    db_query("INSERT INTO {taxonomy_term_edge_path} (tid, vid, temp_pid) SELECT :tid, :vid, p.pid FROM {taxonomy_term_edge_path} p WHERE p.tid IN (:parents)", array(
      ':tid' => $tid,
      ':vid' => $vid,
      ':parents' => $parents,
    ));

    // Add edges
    db_query("INSERT INTO {taxonomy_term_edge} (vid, pid, parent, distance) SELECT :vid, p.pid, p.pid, 0 FROM {taxonomy_term_edge_path} p WHERE p.tid = :tid", array(
      ':tid' => $tid,
      ':vid' => $vid,
    ));
    db_query("INSERT INTO {taxonomy_term_edge} (vid, pid, parent, distance) SELECT :vid, p.pid, e.parent, e.distance + 1 FROM {taxonomy_term_edge_path} p INNER JOIN {taxonomy_term_edge} e ON p.temp_pid = e.pid WHERE p.tid = :tid", array(
      ':tid' => $tid,
      ':vid' => $vid,
    ));
    db_query("UPDATE {taxonomy_term_edge_path} SET temp_pid = 0 WHERE tid = :tid", array(
      ':tid' => $tid,
    ));
  }

  // Use optimized way of inserting for root nodes ...
  if ($addroot) {

    // Add path
    $path = new stdClass();
    $path->tid = $tid;
    $path->vid = $vid;
    drupal_write_record('taxonomy_term_edge_path', $path);

    // Add edge
    db_insert('taxonomy_term_edge')
      ->fields(array(
      'pid' => $path->pid,
      'parent' => $path->pid,
      'distance' => 0,
      'vid' => $vid,
    ))
      ->execute();
    db_insert('taxonomy_term_edge')
      ->fields(array(
      'pid' => $path->pid,
      'parent' => taxonomy_edge_get_root_pid(),
      'distance' => 1,
      'vid' => $vid,
    ))
      ->execute();
  }
}

/**
 * Detach path and children from current parent and attach to new parent
 *
 * @param $vid
 *   Vocabulary ID
 * @param $pid
 *   Path ID
 * @param $new_parent_pid
 *   Path ID of new parent
 */
function _taxonomy_edge_move_subtree($vid, $pid, $new_parent_pid) {

  // Remove old parents
  // @fixme MySQL can not use EXISTS or IN without temp tables when deleting.
  //        SQLite cannot perform join delete.
  //        MySQL and Postgres differ in joined delete syntax.
  //        Use optimized joined delete for MySQL and subquery for others.
  $db_type = Database::getConnection()
    ->databaseType();
  switch ($db_type) {
    case 'mysql':
    case 'mysqli':
      db_query("DELETE e2.*\n      FROM {taxonomy_term_edge} e\n      JOIN {taxonomy_term_edge} e2 ON e2.pid = e.pid\n      WHERE e.parent = :parent\n      AND e2.distance > e.distance\n      AND e.vid = :vid AND e2.vid = :vid\n      ", array(
        ':parent' => $pid,
        ':vid' => $vid,
      ));
      break;
    default:
      db_query("DELETE FROM \n      {taxonomy_term_edge} WHERE eid IN (\n        SELECT e2.eid\n        FROM {taxonomy_term_edge} e\n        JOIN {taxonomy_term_edge} e2 ON e2.pid = e.pid\n        WHERE e.parent = :parent\n        AND e2.distance > e.distance\n        AND e.vid = :vid AND e2.vid = :vid\n      )", array(
        ':parent' => $pid,
        ':vid' => $vid,
      ));
      break;
  }

  // Build new parents
  db_query("INSERT INTO {taxonomy_term_edge} (vid, pid, parent, distance)\n  SELECT :vid, e2.pid, e.parent, e.distance + e2.distance + 1 AS distance\n  FROM {taxonomy_term_edge} e\n  INNER JOIN {taxonomy_term_edge} e2 ON e.pid  = :parent AND e2.parent = :pid\n  WHERE e.vid = :vid AND e2.vid = :vid\n  ", array(
    ':pid' => $pid,
    ':parent' => $new_parent_pid,
    ':vid' => $vid,
  ));
}

/**
 * Copy path and children from to new parent
 *
 * @param $vid
 *   Vocabulary ID
 * @param $pid
 *   Path ID
 * @param $new_parent_pid
 *   Path ID of new parent
 */
function _taxonomy_edge_copy_subtree($vid, $pid, $new_parent_pid) {

  // Copy paths of all children
  db_query("INSERT INTO {taxonomy_term_edge_path} (tid, vid, temp_pid)\n  SELECT p.tid, :vid, p.pid AS temp_pid\n  FROM {taxonomy_term_edge_path} p\n  JOIN {taxonomy_term_edge} e ON e.pid = p.pid\n  WHERE e.parent = :pid\n  AND e.vid = :vid\n  ", array(
    ':pid' => $pid,
    ':vid' => $vid,
  ));

  // Copy subtree
  db_query("INSERT INTO {taxonomy_term_edge} (vid, pid, parent, distance)\n  SELECT :vid, p.pid, p2.pid AS parent, e2.distance\n  FROM {taxonomy_term_edge} e\n  JOIN {taxonomy_term_edge} e2 ON e2.pid = e.pid\n  JOIN {taxonomy_term_edge_path} p ON p.temp_pid = e2.pid AND p.vid = :vid\n  JOIN {taxonomy_term_edge_path} p2 ON p2.temp_pid = e2.parent AND p2.vid = :vid\n  WHERE e.parent = :pid\n  AND e2.distance <= e.distance\n  AND e.vid = :vid AND e2.vid = :vid\n  ", array(
    ':pid' => $pid,
    ':vid' => $vid,
  ));

  // Build parents for new subtree
  db_query("INSERT INTO {taxonomy_term_edge} (vid, pid, parent, distance)\n  SELECT :vid, p.pid, e.parent, e.distance + e2.distance + 1 AS distance\n  FROM {taxonomy_term_edge} e\n  INNER JOIN {taxonomy_term_edge} e2 ON e.pid  = :parent AND e2.parent = :pid\n  INNER JOIN {taxonomy_term_edge_path} p ON p.temp_pid = e2.pid AND p.vid = :vid\n  WHERE e.vid = :vid AND e2.vid = :vid\n  ", array(
    ':pid' => $pid,
    ':parent' => $new_parent_pid,
    ':vid' => $vid,
  ));

  // Cleanup
  db_query("UPDATE {taxonomy_term_edge_path} SET temp_pid = 0 WHERE vid = :vid AND temp_pid > 0", array(
    ':vid' => $vid,
  ));
}

/**
 * Insert a term into the edge tree.
 *
 * @param type $term
 */
function _taxonomy_edge_taxonomy_term_insert($term) {
  $tid = $term->tid;

  // Derive proper parent.
  $parents = _taxonomy_edge_unify_parents($term->parent);

  // watchdog('taxonomy_edge', 'Inserting taxonomy-edge for %tid [%parent]', array('%tid' => $tid, '%parent' => join(',', $parents)), WATCHDOG_DEBUG);
  if ($tid > 0) {
    $tx = db_transaction();
    _taxonomy_edge_build_parents($term->vid, $tid, $parents);
    taxonomy_edge_invalidate_order($term->vid);
  }
  else {
    watchdog('taxonomy_edge', 'Invalid term-id (%tid) received', array(
      '%tid' => $tid,
    ), WATCHDOG_ERROR);
  }
}

/**
 * Update a term in the edge tree.
 *
 * @param type $term
 */
function _taxonomy_edge_taxonomy_term_update($term) {
  $tx = db_transaction();

  // Invalidate sorted tree in case of name/weight change
  $modified =& drupal_static('taxonomy_edge_save_check_modified', TRUE);
  if ($modified) {
    taxonomy_edge_invalidate_order($term->vid);
  }
  if (!isset($term->parent)) {

    // Parent not set, no need to update hierarchy.
    return;
  }

  // Derive proper parents.
  // After this $new contains parents to be added
  // and $del contains parents to removed.
  $parents = _taxonomy_edge_unify_parents($term->parent);
  $old_parents = db_query("\n    SELECT p2.tid\n    FROM {taxonomy_term_edge_path} p\n    INNER JOIN {taxonomy_term_edge} e ON p.pid = e.pid AND e.distance = 1\n    INNER JOIN {taxonomy_term_edge_path} p2 ON e.parent = p2.pid\n    WHERE p.tid = :tid\n  ", array(
    ':tid' => $term->tid,
  ))
    ->fetchAll(PDO::FETCH_NUM);
  $old_parents = _taxonomy_edge_unify_parents($old_parents);
  $new = array_diff($parents, $old_parents);
  $del = array_diff($old_parents, $parents);

  // If hierarchy hasn't changed, then don't do anything
  if (!$del && !$new) {
    return;
  }

  // Move is easier/cheaper than delete/insert. Find trees that can be moved.
  $move = array();
  while ($new && $del) {
    $move[array_shift($del)] = array_shift($new);
  }

  // watchdog('taxonomy_edge', 'Updating taxonomy-edge for %tid [%parent]', array('%tid' => $tid, '%parent' => join(',', $parents)), WATCHDOG_DEBUG);
  if ($new) {
    $old_pids = taxonomy_edge_locate_paths($term->tid, array(
      reset($old_parents),
    ));
    $old_pid = reset($old_pids);
    foreach ($new as $tid) {
      $new_pids = db_query("SELECT p.pid FROM taxonomy_term_edge_path p WHERE p.tid = :tid", array(
        ':tid' => $tid,
      ))
        ->fetchAll(PDO::FETCH_ASSOC);
      foreach ($new_pids as $new_pid) {
        _taxonomy_edge_copy_subtree($term->vid, $old_pid, $new_pid['pid']);
      }
    }
  }
  if ($move) {

    // We don't move from old tids to new tids. We move from old pids to new pids.
    // Because of multiple parents, both old tids and new tids can have multiple pids.
    $old_pids = taxonomy_edge_locate_paths($term->tid, array_keys($move));
    foreach ($old_pids as $parent => $old_pid) {
      $new_pids = db_query("SELECT p.pid FROM taxonomy_term_edge_path p WHERE p.tid = :tid", array(
        ':tid' => $move[$parent],
      ))
        ->fetchAll(PDO::FETCH_ASSOC);
      foreach ($new_pids as $new_pid) {
        _taxonomy_edge_move_subtree($term->vid, $old_pid, $new_pid['pid']);
      }
    }
  }

  // Remove invalid edges.
  if ($del) {
    if ($pids = taxonomy_edge_locate_paths($term->tid, $del)) {
      $table = db_query_temporary("SELECT e.eid, e.pid FROM {taxonomy_term_edge} e WHERE e.parent IN (:pid)", array(
        ':pid' => $pids,
      ));
      $db_type = Database::getConnection()
        ->databaseType();
      switch ($db_type) {
        case 'mysql':
        case 'mysqli':
          db_query("DELETE e.* FROM {taxonomy_term_edge} e JOIN {$table} d ON d.eid = e.eid");
          db_query("DELETE p.* FROM {taxonomy_term_edge_path} p JOIN {$table} d ON d.pid = p.pid");
          break;
        default:
          db_query("DELETE FROM {taxonomy_term_edge} WHERE eid IN (SELECT t.eid FROM {$table} t)");
          db_query("DELETE FROM {taxonomy_term_edge_path} WHERE pid IN (SELECT t.pid FROM {$table} t)");
          break;
      }
    }
  }

  // Don't invalidate again if we have already done so.
  if (!$modified) {
    taxonomy_edge_invalidate_order($term->vid);
  }
}

/**
 * Delete a term from the edge tree.
 *
 *  * @param $tid
 *   Term ID.
 */
function _taxonomy_edge_taxonomy_term_delete($term) {
  db_query("DELETE FROM {taxonomy_term_edge} WHERE pid IN (SELECT p.pid FROM {taxonomy_term_edge_path} p WHERE p.tid = :tid)", array(
    ':tid' => $term->tid,
  ));
  db_query("DELETE FROM {taxonomy_term_edge} WHERE parent IN (SELECT p.pid FROM {taxonomy_term_edge_path} p WHERE p.tid = :tid)", array(
    ':tid' => $term->tid,
  ));
  db_query("DELETE FROM {taxonomy_term_edge_path} WHERE tid = :tid", array(
    ':tid' => $term->tid,
  ));
}

/**
 * Queue an operation for the edge tree.
 *
 * @param object $term
 *   Term object
 * @param string $op
 *   insert, update or delete
 */
function _taxonomy_edge_taxonomy_term_queue($term, $op) {

  // Wait for rebuild to clear queue and initiate snapshot of term_hierarchy
  if (lock_may_be_available('taxonomy_edge_rebuild_edges_' . $term->vid)) {
    $queue = DrupalQueue::get('taxonomy_edge_items_' . $term->vid, TRUE);
    $term->operation = $op;
    $queue
      ->createItem($term);
    $queue = DrupalQueue::get('taxonomy_edge', TRUE);
    $queue
      ->createItem($term->vid);
  }
}

/**
 * Unify parents
 *
 * @param mixed $parents
 * @return array
 *   Flattened array of parent term IDs
 */
function _taxonomy_edge_unify_parents($parents) {
  $parents = is_array($parents) ? $parents : array(
    $parents,
  );
  $new_parents = array();
  foreach ($parents as $parent) {
    if (is_array($parent)) {
      foreach ($parent as $new) {
        $new_parents[] = $new;
      }
    }
    else {
      $new_parents[] = $parent;
    }
  }
  return $new_parents;
}

/**
 * Generate query for ordering by dynamically compiled path
 *
 * @param $pid
 *   Path ID to generate dynamic path from.
 * @return
 *   String containing query.
 */
function _taxonomy_edge_generate_term_path_query($pid) {
  $args = func_get_args();
  $db_type = Database::getConnection()
    ->databaseType();
  switch ($db_type) {
    case 'pgsql':
      return "(SELECT array_to_string(array_agg(((tpx.weight + 1500) || '-' || tpx.name || '-' || tpx.tid)), '/') FROM (SELECT tpd.* FROM taxonomy_term_edge tpe INNER JOIN taxonomy_term_edge_path tpp ON tpe.parent = tpp.pid INNER JOIN taxonomy_term_data tpd ON tpp.tid = tpd.tid WHERE tpe.pid = {$pid} ORDER BY tpe.distance DESC) tpx)";
    case 'mysql':
    case 'mysqli':
      return "(SELECT GROUP_CONCAT(CONCAT(tpd.weight + 1500, '    ', tpd.name, '    ', tpd.tid) ORDER BY tpe.distance DESC SEPARATOR '    ') FROM taxonomy_term_edge tpe INNER JOIN taxonomy_term_edge_path tpp ON tpe.parent = tpp.pid INNER JOIN taxonomy_term_data tpd ON tpp.tid = tpd.tid WHERE tpe.pid = {$pid})";
    case 'sqlite':
      return "(SELECT GROUP_CONCAT(CONCAT(tpd.weight + 1500, '    ', tpd.name, '    ', tpd.tid), '    ') FROM taxonomy_term_edge tpe INNER JOIN taxonomy_term_edge_path tpp ON tpe.parent = tpp.pid INNER JOIN taxonomy_term_data tpd ON tpp.tid = tpd.tid WHERE tpe.pid = {$pid} ORDER BY tpe.distance DESC)";
    default:
  }
}

// ---------- PUBLIC HELPER FUNCTIONS ----------

/**
 * Get max depth of vocabulary.
 *
 * @param $vid
 *   Vocabulary ID
 * @return
 *   Max depth (distance)
 */
function taxonomy_edge_get_max_depth($vid) {
  return db_query("SELECT MAX(distance) FROM {taxonomy_term_edge} WHERE vid = :vid", array(
    ':vid' => $vid,
  ))
    ->fetchField();
}

/**
 * Locate path IDs for a term (unique term = term + parent)
 *
 * @param integer $tid
 *   Term ID
 * @param array $parents
 *   List of parent term IDs
 *
 * @return array
 *   List of path IDs
 */
function taxonomy_edge_locate_paths($tid, $parents) {
  $pids = db_query("\n    SELECT p2.tid AS parent, p.pid\n    FROM {taxonomy_term_edge_path} p\n    INNER JOIN {taxonomy_term_edge} e ON e.pid = p.pid AND e.distance = 1\n    INNER JOIN {taxonomy_term_edge_path} p2 ON p2.pid = e.parent\n    WHERE p.tid = :tid AND p2.tid IN (:parents)\n  ", array(
    ':tid' => $tid,
    ':parents' => $parents,
  ))
    ->fetchAllAssoc('parent', PDO::FETCH_ASSOC);

  // @fixme Using _taxonomy_edge_unify_parents() for flattening the array.
  foreach ($pids as &$pid) {
    $pid = $pid['pid'];
  }
  return $pids;
}

/**
 * Get the path id for the root node in the tree.
 * If root node is not present, create it and return that
 *
 * @param integer $vid
 *   Vocabulary ID
 * @return integer
 *   Path ID
 */
function taxonomy_edge_get_root_pid() {
  static $pid = NULL;
  if (!$pid) {
    $pid = db_query("SELECT pid FROM {taxonomy_term_edge_path} WHERE tid = 0")
      ->fetchField();
    if (!$pid) {

      // Initial path
      $root = new stdClass();
      $root->tid = 0;
      $root->parent = 0;
      $root->vid = 0;
      drupal_write_record('taxonomy_term_edge_path', $root);
      $pid = $root->pid;
    }
  }
  return $pid;
}

/**
 * Checks if it's possible to build an edge realtime.
 *
 * @return boolean
 *   TRUE if possible, FALSE if not.
 */
function taxonomy_edge_is_build_realtime($vid) {
  if (variable_get('taxonomy_edge_build_realtime', TAXONOMY_EDGE_BUILD_REALTIME)) {
    $queue = DrupalQueue::get('taxonomy_edge_items_' . $vid, TRUE);
    if ($queue
      ->numberOfItems()) {

      // Don't build realtime, if there are still items left in the queue.
      return FALSE;
    }
    if (!lock_may_be_available('taxonomy_edge_rebuild_edges_' . $vid)) {

      // Don't build realtime, if entire tree rebuild is in progress.
      return FALSE;
    }
    return TRUE;
  }
  return FALSE;
}

/**
 * Check if our sorted tree is still valid
 */
function taxonomy_edge_is_order_valid($vid, $reset = FALSE) {
  static $valid_orders = array();
  if ($reset || !isset($valid_orders[$vid])) {
    $valid_orders[$vid] = !db_query_range("SELECT oid FROM {taxonomy_term_edge_order} WHERE oid = :oid", 0, 1, array(
      ':oid' => -$vid,
    ))
      ->fetchField();
  }
  return $valid_orders[$vid];
}
function taxonomy_edge_invalidate_order($vid) {
  db_merge('taxonomy_term_edge_order')
    ->key(array(
    'oid' => -$vid,
  ))
    ->fields(array(
    'vid' => $vid,
  ))
    ->execute();
}

/**
 * Get parent from edge list.
 *
 * @param $tid
 *   term id to get parent from.
 * @return array
 *   array of term ids.
 */
function taxonomy_edge_get_parents($tid) {
  return db_query("\n    SELECT d.*\n    FROM {taxonomy_term_edge} ce\n    INNER JOIN {taxonomy_term_edge_path} cp ON ce.pid = cp.pid AND ce.distance > 0\n    INNER JOIN {taxonomy_term_edge_path} pp ON pp.pid = ce.parent\n    INNER JOIN {taxonomy_term_data} d ON d.tid = pp.tid\n    WHERE cp.tid = :tid AND pp.tid > 0\n    ORDER BY ce.distance\n  ", array(
    ':tid' => $tid,
  ))
    ->fetchAll(PDO::FETCH_OBJ);
}

/**
 * Get top term id.
 *
 * @param $tid
 *   Term ID to get top term ID from.
 * @return integer
 *   Top term ID.
 */
function taxonomy_edge_get_top_tid($tid) {
  return db_query("\n    SELECT h.tid\n    FROM {taxonomy_term_edge} e\n    INNER JOIN {taxonomy_term_edge_path} p ON p.pid = e.pid\n    INNER JOIN {taxonomy_term_edge} pe ON pe.pid = e.pid AND pe.distance = e.distance - 1\n    INNER JOIN {taxonomy_term_edge_path} pp ON pp.pid = pe.parent\n    INNER JOIN {taxonomy_term_hierarchy} h ON h.tid = pp.tid\n    WHERE p.tid = :tid\n    AND h.parent = 0\n  ", array(
    ':tid' => $tid,
  ))
    ->fetchField();
}

// ---------- CORE OVERRIDES ----------

/**
 * Reimplementation of taxonomy_get_tree().
 * Limit db fetch to only specified parent.
 * @see taxonomy_get_tree()
 */
function taxonomy_edge_get_tree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {

  // @todo Use regular taxonomy_get_tree if realtime build is disabled,
  //       as this function might be unreliable.
  module_load_include('core.inc', 'taxonomy_edge');

  // Use optimized version if possible
  if (variable_get('taxonomy_edge_optimized_get_tree', TAXONOMY_EDGE_OPTIMIZED_GET_TREE)) {
    return taxonomy_edge_get_tree_optimized($vid, $parent, $max_depth, $load_entities);
  }
  else {
    return taxonomy_edge_get_tree_generic($vid, $parent, $max_depth, $load_entities);
  }
}

/**
 * Reimplementation of taxonomy_select_nodes() re-allowing depth modifier.
 */
function taxonomy_edge_select_nodes($tid, $pager = TRUE, $limit = FALSE, $depth = 0, $order = array(
  't.sticky' => 'DESC',
  't.created' => 'DESC',
)) {
  if (!variable_get('taxonomy_maintain_index_table', TRUE)) {
    return array();
  }

  // Locate tids to search in
  $subquery = db_select('taxonomy_term_edge', 'e');
  $subquery
    ->join('taxonomy_term_edge_path', 'p', 'p.pid = e.pid');
  $subquery
    ->join('taxonomy_term_edge_path', 'p2', 'p2.pid = e.parent');
  $subquery
    ->condition('p2.tid', $tid);
  $subquery
    ->condition('e.distance', $depth, '<=');
  $subquery
    ->fields('p', array(
    'tid',
  ));

  // Find nodes
  $query = db_select('taxonomy_index', 't');
  $query
    ->addTag('node_access');
  $query
    ->condition('t.tid', $subquery, 'IN');
  if ($pager) {
    $count_query = db_select('taxonomy_index', 't');
    $count_query
      ->join('taxonomy_term_edge_path', 'p', 'p.tid = t.tid');
    $count_query
      ->join('taxonomy_term_edge', 'e', 'e.pid = p.pid');
    $count_query
      ->join('taxonomy_term_edge_path', 'p2', 'p2.pid = e.parent');
    $count_query
      ->condition('p2.tid', $tid);
    $count_query
      ->condition('e.distance', $depth, '<=');
    $count_query
      ->addExpression('COUNT(1)');
    $query = $query
      ->extend('PagerDefault');
    if ($limit !== FALSE) {
      $query = $query
        ->limit($limit);
    }
    $query
      ->setCountQuery($count_query);
  }
  else {
    if ($limit !== FALSE) {
      $query
        ->range(0, $limit);
    }
  }
  $query
    ->addField('t', 'nid');
  $query
    ->addField('t', 'tid');
  foreach ($order as $field => $direction) {
    $query
      ->orderBy($field, $direction);

    // ORDER BY fields need to be loaded too, assume they are in the form
    // table_alias.name
    list($table_alias, $name) = explode('.', $field);
    $query
      ->addField($table_alias, $name);
  }
  return $query
    ->execute()
    ->fetchCol();
}

Functions

Namesort descending Description
taxonomy_edge_core_override_info Implements hook_core_override().
taxonomy_edge_cron Implements hook_cron().
taxonomy_edge_cron_queue_info Implements hook_cron_queue_info().
taxonomy_edge_form_taxonomy_overview_terms_alter Hook into the drag'n'drop interface of terms in order to update tree when terms are reordered.
taxonomy_edge_form_taxonomy_overview_vocabularies_alter Hook into the overview of vocabularies to provide rebuild actions.
taxonomy_edge_get_max_depth Get max depth of vocabulary.
taxonomy_edge_get_parents Get parent from edge list.
taxonomy_edge_get_root_pid Get the path id for the root node in the tree. If root node is not present, create it and return that
taxonomy_edge_get_top_tid Get top term id.
taxonomy_edge_get_tree Reimplementation of taxonomy_get_tree(). Limit db fetch to only specified parent.
taxonomy_edge_help Implements hook_help().
taxonomy_edge_invalidate_order
taxonomy_edge_is_build_realtime Checks if it's possible to build an edge realtime.
taxonomy_edge_is_order_valid Check if our sorted tree is still valid
taxonomy_edge_locate_paths Locate path IDs for a term (unique term = term + parent)
taxonomy_edge_menu Implements hook_menu().
taxonomy_edge_permission Implements hook_perm().
taxonomy_edge_process_queue_item Cron queue worker Process edge for a queued term.
taxonomy_edge_reorder_submit Copy/paste from core taxonomy module. This is the penalty for not having a proper abstraction layer! And for not invoking update hook on terms when changing their parents!
taxonomy_edge_select_nodes Reimplementation of taxonomy_select_nodes() re-allowing depth modifier.
taxonomy_edge_taxonomy_term_delete Implements hook_taxonomy_term_delete().
taxonomy_edge_taxonomy_term_insert Implements hook_taxonomy_term_insert().
taxonomy_edge_taxonomy_term_presave Implements hook_taxonomy_term_presave().
taxonomy_edge_taxonomy_term_update Implements hook_taxonomy_term_update().
taxonomy_edge_theme Implementation of hook_theme().
_taxonomy_edge_build_parents Build parent paths for a term.
_taxonomy_edge_copy_subtree Copy path and children from to new parent
_taxonomy_edge_generate_term_path_query Generate query for ordering by dynamically compiled path
_taxonomy_edge_move_subtree Detach path and children from current parent and attach to new parent
_taxonomy_edge_taxonomy_term_delete Delete a term from the edge tree.
_taxonomy_edge_taxonomy_term_insert Insert a term into the edge tree.
_taxonomy_edge_taxonomy_term_queue Queue an operation for the edge tree.
_taxonomy_edge_taxonomy_term_update Update a term in the edge tree.
_taxonomy_edge_unify_parents Unify parents

Constants

Namesort descending Description
TAXONOMY_EDGE_BUILD_REALTIME Default value for realtime building of tree.
TAXONOMY_EDGE_MAX_DEPTH Fail safe for avoiding infite loops when rebuilding edges.
TAXONOMY_EDGE_OPTIMIZED_GET_TREE Default value for optimized tree.
TAXONOMY_EDGE_STATIC_CACHING Default value for static caching.