You are here

taxonomy_edge.module in Taxonomy Edge 6

Same filename and directory in other branches
  1. 8 taxonomy_edge.module
  2. 7.2 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 e.tid FROM term_edge e WHERE e.parent = 14;

@todo Fix concurrency issue for queue rebuild and full tree rebuild (lock_may_be_available() inside transaction)

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 e.tid
 * FROM term_edge e
 * WHERE e.parent = 14;
 *
 * @todo Fix concurrency issue for queue rebuild and full tree rebuild (lock_may_be_available() inside transaction)
 *
 * @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', FALSE);

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

/**
 * Default value for optimized tree.
 */
define('TAXONOMY_EDGE_USE_CUSTOM_TRANSACTIONS', FALSE);

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

/**
 * Implements hook_init().
 */
function taxonomy_edge_init() {
  if (!empty($_SESSION['taxonomy_edge_rebuild_module_enabled'])) {
    unset($_SESSION['taxonomy_edge_rebuild_module_enabled']);
    module_load_include('rebuild.inc', 'taxonomy_edge');
    $vids = array();
    foreach (taxonomy_get_vocabularies() as $vocabulary) {
      $vids[] = $vocabulary->vid;
    }
    taxonomy_edge_rebuild_all_batch($vids);
    batch_process();
  }
}

/**
 * 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 filter_filter('process', 1, NULL, file_get_contents(dirname(__FILE__) . "/README.txt"));
  }
}

/**
 * Implements hook_perm().
 */
function taxonomy_edge_perm() {
  return array(
    'administer taxonomy edge',
  );
}

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

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

  // settings page
  $items['admin/content/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/content/taxonomy/rebuild/%'] = array(
    'title' => 'Rebuild edges',
    'description' => 'Rebuild taxonomy edges',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'taxonomy_edge_rebuild_page_confirm',
      4,
      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)) {
      $context = array();
      taxonomy_edge_rebuild_order($vocabulary->vid, $context);
      taxonomy_edge_rebuild_finished($context['success'], $context['results'], array());
    }
  }
}

/**
 * Implements hook_cronapi().
 * Regularly rebuild the edge list and resort trees.
 */
function taxonomy_edge_cronapi($op, $job = NULL) {
  switch ($op) {
    case 'list':
      return array(
        'taxonomy_edge_cron_rebuild' => 'Rebuild taxonomy edges',
        'taxonomy_edge_cron' => 'Resort invalid trees',
      );
    case 'rule':
      switch ($job) {
        case 'taxonomy_edge_cron_rebuild':
          return '0 2 * * *';
        case 'taxonomy_edge_cron':
          return '* * * * *';
      }
    case 'settings':
      switch ($job) {
        case 'taxonomy_edge_cron_rebuild':
          return array(
            'enabled' => FALSE,
          );
        case 'taxonomy_edge_cron':
          return array(
            'enabled' => TRUE,
          );
      }
  }
}

/**
 * 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_vocabulary_insert().
 */
function taxonomy_edge_taxonomy_vocabulary_insert($vocabulary) {
  taxonomy_edge_taxonomy_vocabulary_delete($vocabulary);
  db_query("INSERT INTO {term_edge} (vid) VALUES(%d)", $vocabulary->vid);
}

/**
 * Implements hook_taxonomy_vocabulary_delete().
 */
function taxonomy_edge_taxonomy_vocabulary_delete($vocabulary) {
  db_query("DELETE FROM {term_edge} WHERE vid = %d", $vocabulary->vid);
}

/**
 * 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');
  }
}

/**
 * Implementation of hook_taxonomy().
 *
 * Maintain edges upon taxonomy manipulation
 */
function taxonomy_edge_taxonomy($op, $type, $array = NULL) {
  switch ($type) {
    case 'term':
      $term = (object) $array;
      switch ($op) {
        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;
      }
      break;
    case 'vocabulary':
      $vocabulary = (object) $array;
      switch ($op) {
        case 'insert':
          taxonomy_edge_taxonomy_vocabulary_insert($vocabulary);
          break;
        case 'delete':
          taxonomy_edge_taxonomy_vocabulary_delete($vocabulary);
          break;
      }
      break;
  }
}

/**
 * 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(
        '#value' => l('rebuild edges', "admin/content/taxonomy/rebuild/edges/{$vocabulary->vid}"),
      );
      $form[$vocabulary->vid]['rebuild_order'] = array(
        '#value' => l('rebuild order', "admin/content/taxonomy/rebuild/order/{$vocabulary->vid}"),
      );
      $form[$vocabulary->vid]['rebuild_all'] = array(
        '#value' => l('rebuild everything', "admin/content/taxonomy/rebuild/all/{$vocabulary->vid}"),
      );
    }
    $form['#theme'] = 'taxonomy_edge_overview_vocabularies';
  }
}

/**
 * Implements hook_theme().
 */
function taxonomy_edge_theme() {
  return array(
    'taxonomy_edge_overview_vocabularies' => array(
      'arguments' => array(
        'form' => array(),
      ),
      'file' => 'taxonomy_edge.theme.inc',
    ),
  );
}

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

/**
 * 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)) {
    drupal_queue_include();
    $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);
  }
}

/**
 * Rebuild edges
 */
function taxonomy_edge_cron_rebuild() {
  module_load_include('rebuild.inc', 'taxonomy_edge');
  foreach (taxonomy_get_vocabularies() as $vocabulary) {
    $context = array();
    taxonomy_edge_rebuild_edges($vocabulary->vid, $context);
    taxonomy_edge_rebuild_finished($context['success'], $context['results'], array());
  }
}

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

/**
 * Delete subtree
 *
 * @param $vid
 *   Vocabulary ID
 * @param $tids
 *   Array of term ids to delete subtree from
 * @param $type
 *   Type of delete
 *     0: Delete entire subtree
 *     1: Delete ancestors (detach children from parents in $tids)
 */
function _taxonomy_edge_delete_subtree($vid, $tids, $type = 0) {

  // @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.
  //        With MySQL 5.6 we might be able to switch to a subquery method only.
  global $db_type;
  $type = $type == 0 ? '' : 'AND e2.distance > e.distance';
  $args = $tids;
  $args[] = $vid;
  $args[] = $vid;
  $placeholders = db_placeholders($tids);
  switch ($db_type) {
    case 'mysql':
    case 'mysqli':
      db_query("DELETE e2.*\n        FROM {term_edge} e\n        INNER JOIN {term_edge} e2 ON e2.tid = e.tid\n        WHERE e.parent IN ({$placeholders})\n        {$type}\n        AND e.vid = %d AND e2.vid = %d\n      ", $args);
      break;
    default:
      db_query("DELETE FROM\n      {term_edge} WHERE eid IN (\n        SELECT e2.eid\n        FROM {term_edge} e\n        INNER JOIN {term_edge} e2 ON e2.tid = e.tid\n        WHERE e.parent IN ({$placeholders})\n        {$type}\n        AND e.vid = %d AND e2.vid = %d\n      )", $args);
      break;
  }
}

/**
 * Detach children from parents in $tids (delete ancestors)
 */
function _taxonomy_edge_detach_subtree($vid, $tids) {
  return _taxonomy_edge_delete_subtree($vid, $tids, 1);
}

/**
 * Attach path and children to new parents
 *
 * @param $vid
 *   Vocabulary ID
 * @param $tid
 *   Term ID
 * @param $parents
 *   Term ID of new parents
 */
function _taxonomy_edge_attach_subtree($vid, $tid, $parents) {

  // Build new parents
  $args = $parents;
  array_unshift($args, $vid);
  $args[] = $tid;
  $args[] = $vid;
  $args[] = $vid;
  $placeholders = db_placeholders($parents);
  db_query("INSERT INTO {term_edge} (vid, tid, parent, distance)\n  SELECT %d, e2.tid, e.parent, e.distance + e2.distance + 1 AS distance\n  FROM {term_edge} e\n  INNER JOIN {term_edge} e2 ON e.tid IN ({$placeholders}) AND e2.parent = %d\n  WHERE e.vid = %d AND e2.vid = %d\n  ", $args);
  taxonomy_edge_invalidate_order($vid);
}

/**
 * Detach path and children from current parent and attach to new parents
 *
 * @param $vid
 *   Vocabulary ID
 * @param $tid
 *   Term ID
 * @param $parents
 *   Term ID of new parents
 */
function _taxonomy_edge_move_subtree($vid, $tid, $parents) {
  _taxonomy_edge_detach_subtree($vid, array(
    $tid,
  ));
  _taxonomy_edge_attach_subtree($vid, $tid, $parents);
}

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

  // Derive proper parents.
  $parents = _taxonomy_edge_unify_parents($term->parent);
  if ($term->tid > 0) {
    $tx = _taxonomy_edge_db_transaction();
    db_query("INSERT INTO {term_edge} (vid, tid, parent) VALUES(%d, %d, %d)", $term->vid, $term->tid, $term->tid);
    $args = $parents;
    array_unshift($args, $term->tid);
    array_unshift($args, $term->vid);
    $args[] = $term->vid;
    $placeholders = db_placeholders($parents);
    db_query("INSERT INTO {term_edge} (vid, tid, parent, distance)\n      SELECT %d AS vid, %d AS tid, e.parent, e.distance + 1 AS distance\n      FROM {term_edge} e\n      WHERE e.tid IN ({$placeholders})\n      AND e.vid = %d\n    ", $args);
    taxonomy_edge_invalidate_order($term->vid);
  }
  else {
    watchdog('taxonomy_edge', 'Invalid term-id (%tid) received', array(
      '%tid' => $term->tid,
    ), WATCHDOG_ERROR);
  }
}

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

  // Invalidate sorted tree in case of name/weight change
  if (isset($term->original) && ($term->original->name != $term->name || $term->original->weight != $term->weight)) {
    taxonomy_edge_invalidate_order($term->vid);
  }
  if (!isset($term->parent)) {

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

  // Derive proper parents.
  $parents = _taxonomy_edge_unify_parents($term->parent);
  _taxonomy_edge_move_subtree($term->vid, $term->tid, $parents);
}

/**
 * Delete a term from the edge tree.
 *
 *  * @param $tid
 *   Term ID.
 */
function _taxonomy_edge_taxonomy_term_delete($term) {
  db_query("DELETE FROM {term_edge} WHERE vid = %d AND tid = %d", $term->vid, $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)) {
    drupal_queue_include();
    $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 $tid
 *   Term ID to generate dynamic path from.
 * @return
 *   String containing query.
 */
function _taxonomy_edge_generate_term_path_query($tid) {
  $args = func_get_args();
  global $db_type;
  switch ($db_type) {
    case 'pgsql':
      return "(SELECT array_to_string(array_agg(((tpx.weight + 1500) || tpx.name || '    ' || tpx.tid)), '    ') FROM (SELECT tpd.* FROM {term_edge} tpe INNER JOIN {term_data} tpd ON tpd.tid = tpe.parent WHERE tpe.tid = {$tid} ORDER BY tpe.distance DESC) tpx)";
    case 'mysql':
    case 'mysqli':
      return "(SELECT GROUP_CONCAT(tpd.weight + 1500, tpd.name, '    ', tpd.tid ORDER BY tpe.distance DESC SEPARATOR '    ') FROM {term_edge} tpe INNER JOIN {term_data} tpd ON tpd.tid = tpe.parent WHERE tpe.tid = {$tid})";
    case 'sqlite':
      return "(SELECT GROUP_CONCAT(val, '    ') FROM (SELECT (tpd.weight + 1500) || tpd.name || '    ' || tpd.tid AS val FROM {term_edge} tpe INNER JOIN {term_data} tpd ON tpd.tid = tpe.parent WHERE tpe.tid = {$tid} ORDER BY tpe.distance DESC))";
    case 'sqlsrv':
      return "(SELECT RTRIM((\n                     SELECT CAST(tpd.weight + 1500 as nvarchar) + tpd.name + '    ' + CAST(tpd.tid as nvarchar) + '    '\n                     FROM {term_edge} tpe\n                     JOIN {term_data} tpd ON tpd.tid = tpe.parent\n                     WHERE tpe.tid = {$tid}\n                     ORDER BY tpe.distance DESC\n                     FOR XML PATH ('')))\n                  )";
    default:
  }
}

/**
 * Transaction handler wrapper
 */
function _taxonomy_edge_db_transaction() {
  if (function_exists('db_start_transaction')) {
    return new TaxonomyEdgeDatabaseTransaction('db_start_transaction', 'db_end_transaction');
  }
  elseif (variable_get('taxonomy_edge_use_custom_transactions', TAXONOMY_EDGE_USE_CUSTOM_TRANSACTIONS)) {
    return new TaxonomyEdgeDatabaseTransaction();
  }
}

/**
 * Class for simulating DBTNG transactions
 */
class TaxonomyEdgeDatabaseTransaction {
  private $end;
  private static $counter = 0;
  function __construct($begin = NULL, $end = NULL) {
    if ($begin && $end) {
      $this->end = $end;
      call_user_func($begin);
    }
    else {
      self::begin();
    }
  }
  function __destruct() {
    $this
      ->commit();
  }
  private static function begin() {
    if (self::$counter == 0) {
      db_query('BEGIN');
    }
    self::$counter++;
  }
  private static function end($commit = TRUE) {
    self::$counter--;
    if (self::$counter == 0) {
      if ($commit) {
        db_query('COMMIT');
      }
      else {
        db_query('ROLLBACK');
      }
    }
  }
  function commit() {
    if ($this->end) {
      return call_user_func($this->end, TRUE);
    }
    else {
      return self::end(TRUE);
    }
  }
  function rollback() {
    if ($this->end) {
      return call_user_func($this->end, FALSE);
    }
    else {
      return self::end(FALSE);
    }
  }

}

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

/**
 * Get max depth of a tree..
 *
 * @param $vid
 *   Vocabulary ID
 * @param $parent (optional)
 *   Parent to max depth of
 * @return
 *   Max depth (distance)
 */
function taxonomy_edge_get_max_depth($vid, $parent = 0) {
  return db_result(db_query("SELECT MAX(distance) FROM {term_edge} WHERE vid = %d AND parent = %d", $vid, $parent));
}

/**
 * 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 (!module_exists('drupal_queue')) {

    // Only realtime build is supported, if Drupal Queue is missing
    return TRUE;
  }
  if (variable_get('taxonomy_edge_build_realtime', TAXONOMY_EDGE_BUILD_REALTIME)) {
    drupal_queue_include();
    $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_result(db_query_range("SELECT 1 FROM {term_edge_order} WHERE vid = %d", -$vid, 0, 1));
  }
  return $valid_orders[$vid];
}

/**
 * Check if our sorted tree is still valid
 */
function taxonomy_edge_is_order_invalid($reset = FALSE) {
  static $invalid;
  if ($reset || !isset($valid)) {
    $invalid = db_result(db_query_range("SELECT 1\n      FROM {vocabulary} v\n      LEFT JOIN {term_edge_order} o ON o.vid = -v.vid AND o.vid < 0\n      WHERE o.oid IS NULL\n    ", 0, 1));
  }
  return $invalid;
}

/**
 * Invalidate order for a vocabulary
 */
function taxonomy_edge_invalidate_order($vid) {
  db_query("DELETE FROM {term_edge_order} WHERE vid = %d", -$vid);
}

/**
 * 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) {
  $result = db_query("SELECT e.distance, e.parent\n    FROM {term_data} d\n    INNER JOIN {term_edge} e ON e.parent = d.tid\n    WHERE e.tid = %d\n    AND e.tid > 0\n    AND e.distance > 0\n    ORDER BY e.distance\n  ", $tid);
  $parents = array();
  while ($row = db_fetch_object($result)) {
    $parents[$row->distance] = $row->parent;
  }
  return $parents;
}

/**
 * Get top term ids.
 *
 * @param $tid
 *   Term ID to get top term ID from.
 * @return array
 *   Top term IDs.
 */
function taxonomy_edge_get_top_tids($tid) {
  $result = db_query("SELECT DISTINCT e2.parent\n    FROM {term_edge} e\n    JOIN {term_edge} e2 ON e2.tid = e.tid AND e2.distance = e.distance - 1 AND e2.parent <> e.parent\n    WHERE e.tid = %d\n    AND e.parent = 0\n    AND e.vid = e2.vid\n  ", $tid);
  $toptids = array();
  while ($row = db_fetch_object($result)) {
    $toptids[$row->parent] = $row->parent;
  }
  return $toptids;
}

/**
 * Get top term id.
 *
 * @param $tid
 *   Term ID to get top term ID from.
 * @return int
 *   Top term ID.
 */
function taxonomy_edge_get_top_tid($tid) {
  $tids = taxonomy_edge_get_top_tids(array(
    $tid,
  ));
  return reset($tids);
}

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

/**
 * BC wrappers after function name changes
 */
function taxonomy_edge_taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
  return taxonomy_edge_get_tree($vid, $parent, $max_depth);
}
function taxonomy_edge_taxonomy_term_count_nodes($tid, $type = 0) {
  return taxonomy_edge_term_count_nodes($tid, $type);
}
function taxonomy_edge_taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC, n.nid DESC') {
  return taxonomy_edge_select_nodes($tids, $operator, $depth, $pager, $order);
}

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

  // @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);
  }
  else {
    return taxonomy_edge_get_tree_generic($vid, $parent, $max_depth);
  }
}

/**
 * Reimplementation of taxonomy_term_count_nodes().
 * Avoid GROUP BY and recursive calls.
 * @see taxonomy_term_count_nodes()
 */
function taxonomy_edge_term_count_nodes($tid, $type = 0) {
  static $count;
  if (!isset($count[$type])) {

    // $type == 0 always evaluates TRUE if $type is a string
    if (is_numeric($type)) {
      $result = db_query("SELECT COUNT(1) AS c FROM node n JOIN term_node tn ON n.vid = tn.vid JOIN term_edge e ON tn.tid = e.tid WHERE n.status = 1 AND e.parent = %d", $tid);
    }
    else {
      $result = db_query("SELECT COUNT(1) AS c FROM node n JOIN term_node tn ON n.vid = tn.vid JOIN term_edge e ON tn.tid = e.tid WHERE n.status = 1 AND e.parent = %d AND n.type = '%s'", $tid, $type);
    }
    $count[$type][$tid] = db_result($result);
  }
  return isset($count[$type][$tid]) ? $count[$type][$tid] : 0;
}

/**
 * Reimplementation of taxonomy_select_nodes().
 * Uses the Taxonomy Edge table instead of cpu/mem (mostly mem) hungry taxonomy_get_tree().
 * @note This function also fixes the "broken" default order by from the taxonomy_select_nodes() function,
 *       by added "n.nid" to the ORDER BY clause. This is ensure consistent results on each query.
 *
 * @see taxonomy_select_nodes()
 */
function taxonomy_edge_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC, n.nid DESC') {

  // Fix broken default order by the Taxonomy Module.
  // Ordering without a unique key could result in incosistent result sets,
  // depending on which index (if any) the database decides to use for the query.
  switch ($order) {
    case 'n.sticky DESC, n.created DESC':
      $order .= ', n.nid DESC';
      break;
    case 'n.sticky ASC, n.created ASC':
      $order .= ', n.nid ASC';
      break;
  }

  // If possible, use optimize by using GROUP BY instead of DISTINCT.
  // This is what the idx_taxonomy_edge index is used for.
  $group_by = '';
  $distinct = 'DISTINCT(n.nid)';
  global $db_type;
  if ($db_type == 'mysql' || $db_type == 'mysqli') {
    switch ($order) {
      case 'n.sticky DESC, n.created DESC, n.nid DESC':
      case 'n.sticky ASC, n.created ASC, n.nid ASC':
        $group_by = ' GROUP BY n.sticky, n.created, n.nid';
        $distinct = 'n.nid';
        break;
      case 'n.created DESC, n.nid DESC':
      case 'n.created ASC, n.nid ASC':
        $group_by = ' GROUP BY n.created, n.nid';
        $distinct = 'n.nid';
        break;
    }
  }
  if (count($tids) > 0) {
    if ($operator == 'or') {
      $args = $tids;
      $placeholders = db_placeholders($args, 'int');
      if ($depth === 'all') {
        $sql = 'SELECT ' . $distinct . ', n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid JOIN {term_edge} te ON tn.tid = te.tid WHERE te.parent IN(' . $placeholders . ') AND n.status = 1 ' . $group_by . ' ORDER BY ' . $order;
        $sql_count = 'SELECT COUNT(DISTINCT n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid JOIN {term_edge} te ON tn.tid = te.tid WHERE te.parent IN(' . $placeholders . ') AND n.status = 1';
      }
      else {
        $sql = 'SELECT ' . $distinct . ', n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid JOIN {term_edge} te ON tn.tid = te.tid WHERE te.parent IN(' . $placeholders . ') AND te.distance <= %d AND n.status = 1 ' . $group_by . ' ORDER BY ' . $order;
        $sql_count = 'SELECT COUNT(DISTINCT n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid JOIN {term_edge} te ON tn.tid = te.tid WHERE te.parent IN(' . $placeholders . ') AND te.distance <= %d AND n.status = 1';
        $args[] = $depth;
      }
    }
    else {
      $join = '';
      $args = array();
      foreach ($tids as $index => $tid) {
        $join .= ' INNER JOIN {term_node} tn' . $index . ' ON n.vid = tn' . $index . '.vid';
        $join .= ' INNER JOIN {term_edge} te' . $index . ' ON tn' . $index . '.tid = te' . $index . '.tid AND te' . $index . '.parent = %d';
        $args[] = $tid;
        if ($depth !== 'all') {
          $join .= ' AND te' . $index . '.distance <= %d';
          $args[] = $depth;
        }
      }
      $sql = 'SELECT ' . $distinct . ', n.sticky, n.title, n.created FROM {node} n ' . $join . ' WHERE n.status = 1 ' . $group_by . ' ORDER BY ' . $order;
      $sql_count = 'SELECT COUNT(DISTINCT n.nid) FROM {node} n ' . $join . ' WHERE n.status = 1';
    }
    $sql = db_rewrite_sql($sql);
    if ($pager) {
      $sql_count = db_rewrite_sql($sql_count);
      $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count, $args);
    }
    else {
      $result = db_query_range($sql, $args, 0, variable_get('feed_default_items', 10));
    }
  }
  return $result;
}

Functions

Namesort descending Description
taxonomy_edge_cron Implements hook_cron().
taxonomy_edge_cronapi Implements hook_cronapi(). Regularly rebuild the edge list and resort trees.
taxonomy_edge_cron_queue_info Implements hook_cron_queue_info().
taxonomy_edge_cron_rebuild Rebuild edges
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 a tree..
taxonomy_edge_get_parents Get parent from edge list.
taxonomy_edge_get_top_tid Get top term id.
taxonomy_edge_get_top_tids Get top term ids.
taxonomy_edge_get_tree Reimplementation of taxonomy_get_tree(). Limit db fetch to only specified parent.
taxonomy_edge_help Implements hook_help().
taxonomy_edge_init Implements hook_init().
taxonomy_edge_invalidate_order Invalidate order for a vocabulary
taxonomy_edge_is_build_realtime Checks if it's possible to build an edge realtime.
taxonomy_edge_is_order_invalid Check if our sorted tree is still valid
taxonomy_edge_is_order_valid Check if our sorted tree is still valid
taxonomy_edge_menu Implements hook_menu().
taxonomy_edge_perm Implements hook_perm().
taxonomy_edge_process_queue_item Cron queue worker Process edge for a queued term.
taxonomy_edge_select_nodes Reimplementation of taxonomy_select_nodes(). Uses the Taxonomy Edge table instead of cpu/mem (mostly mem) hungry taxonomy_get_tree(). @note This function also fixes the "broken" default order by from the taxonomy_select_nodes() function, by…
taxonomy_edge_taxonomy Implementation of hook_taxonomy().
taxonomy_edge_taxonomy_get_tree BC wrappers after function name changes
taxonomy_edge_taxonomy_select_nodes
taxonomy_edge_taxonomy_term_count_nodes
taxonomy_edge_taxonomy_term_delete Implements hook_taxonomy_term_delete().
taxonomy_edge_taxonomy_term_insert Implements hook_taxonomy_term_insert().
taxonomy_edge_taxonomy_term_update Implements hook_taxonomy_term_update().
taxonomy_edge_taxonomy_vocabulary_delete Implements hook_taxonomy_vocabulary_delete().
taxonomy_edge_taxonomy_vocabulary_insert Implements hook_taxonomy_vocabulary_insert().
taxonomy_edge_term_count_nodes Reimplementation of taxonomy_term_count_nodes(). Avoid GROUP BY and recursive calls.
taxonomy_edge_theme Implements hook_theme().
_taxonomy_edge_attach_subtree Attach path and children to new parents
_taxonomy_edge_db_transaction Transaction handler wrapper
_taxonomy_edge_delete_subtree Delete subtree
_taxonomy_edge_detach_subtree Detach children from parents in $tids (delete ancestors)
_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 parents
_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.
TAXONOMY_EDGE_USE_CUSTOM_TRANSACTIONS Default value for optimized tree.

Classes

Namesort descending Description
TaxonomyEdgeDatabaseTransaction Class for simulating DBTNG transactions