taxonomy_edge.module in Taxonomy Edge 7.2
Same filename and directory in other branches
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
File
taxonomy_edge.moduleView 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
Name | 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
Name | 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. |