diff --git hierarchical_select.module hierarchical_select.module index d565d14..3081945 100755 --- hierarchical_select.module +++ hierarchical_select.module @@ -1662,9 +1662,19 @@ function _hierarchical_select_hierarchy_generate($config, $selection, $required, } } // Reconstruct all sublevels. + // Try high-performance hook first. + $multi_hook = 'hierarchical_select_children_multiple'; + if (function_exists($config['module'] . '_' . $multi_hook)) { + $children_multiple = module_invoke($config['module'], $multi_hook, $selection, $config['params']); + } for ($i = 0; $i < count($selection); $i++) { - $children = array_keys(module_invoke($config['module'], 'hierarchical_select_children', $selection[$i], $config['params'])); - + if (isset($children_multiple)) { + $children = array_keys($children_multiple[$selection[$i]]); + } + else { + // Fallback to the old hook. + $children = array_keys(module_invoke($config['module'], 'hierarchical_select_children', $selection[$i], $config['params'])); + } // Ensure the next item in the selection is a child of the current item. for ($j = $i + 1; $j < count($selection); $j++) { if (in_array($selection[$j], $children)) { @@ -1987,10 +1997,29 @@ function _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config) { foreach ($hierarchy->levels as $depth => $level) { foreach (array_keys($level) as $item) { if (!preg_match('/(none|label_\d+|create_new_item)/', $item)) { - $hierarchy->childinfo[$depth][$item] = count(module_invoke($config['module'], 'hierarchical_select_children', $item, $config['params'])); + $depth_items[$depth][] = $items[] = $item; } } } + // Count children for _all_ items in the hierarchy. + // Use high-performance hook if possible. + $hook = 'hierarchical_select_children_count'; + if (function_exists($config['module'] . '_' . $hook)) { + $counts = module_invoke($config['module'], $hook, $items, $config['params']); + } + else { + // Fallback to the old hook. + foreach($items as $item) { + $counts[$item] = count(module_invoke($config['module'], 'hierarchical_select_children', $item, $config['params'])); + } + } + + // Update hierarchy. + foreach ($hierarchy->levels as $depth => $level) { + foreach ($depth_items[$depth] as $item) { + $hierarchy->childinfo[$depth][$item] = $counts[$item]; + } + } return $hierarchy; } diff --git modules/hs_content_taxonomy.module modules/hs_content_taxonomy.module index 486e60c..f5d99b2 100755 --- modules/hs_content_taxonomy.module +++ modules/hs_content_taxonomy.module @@ -386,20 +386,102 @@ function hs_content_taxonomy_hierarchical_select_children($parent, $params) { $tid = $params['tid']; $depth = $params['depth']; - // Keep a static cache of the entire tree, this allows us to quickly look up - // if a term is not too deep – because if it's too deep, we don't want to - // return any children. - if (!isset($tree[$vid][$tid])) { - $raw_tree = _hs_taxonomy_hierarchical_select_get_tree($vid, $tid); - foreach ($raw_tree as $term) { - $tree[$vid][$tid][$term->tid] = $term->depth; + // If depth is unlimited every level is valid. + if ($depth == 0) { + $terms = _hs_taxonomy_hierarchical_select_get_tree($vid, $parent, -1, 1); + } + else { + // Otherwise, return only terms not deeper than $depth. + // $depth > 0 should be really considered as very low performance option + // and should be avoided when possible. + + // Keep a static cache of the entire tree, this allows us to quickly look up + // if a term is not too deep – because if it's too deep, we don't want to + // return any children. + if (!isset($tree[$vid][$tid])) { + $raw_tree = _hs_taxonomy_hierarchical_select_get_tree($vid, $tid); + foreach ($raw_tree as $term) { + $tree[$vid][$tid][$term->tid] = $term->depth; + } + } + if ($tree[$vid][$tid][$parent] + 1 >= $depth) { + $terms = array(); + } + else { + $terms = _hs_taxonomy_hierarchical_select_get_tree($vid, $parent, -1, 1); } } - $terms = ($depth > 0 && $tree[$vid][$tid][$parent] + 1 >= $depth) ? array() : _hs_taxonomy_hierarchical_select_get_tree($vid, $parent, -1, 1); + return _hs_taxonomy_hierarchical_select_terms_to_options($terms); } /** + * Implementation of hook_hierarchical_select_children_multiple(). + * + * Special high-performance hook to optimize fetching of children for multiple + * parents. + */ +function hs_content_taxonomy_hierarchical_select_children_multiple($parents, $params) { + $vid = $params['vid']; + $tid = $params['tid']; + $depth = $params['depth']; + + // If depth is unlimited every level is valid. + if ($depth == 0) { + $terms = _hs_taxonomy_hierarchical_select_get_tree($vid, $parents, -1, 1); + $result = array(); + // Group up terms by their parents. + foreach ($terms as $term) { + // A term can have multiple parents and present in $result multiple times. + foreach ($parents as $parent) { + if (in_array($parent, $term->parents)) { + $result[$parent][] = $term; + } + } + } + foreach ($result as $parent => $children_terms) { + $result[$parent] = _hs_taxonomy_hierarchical_select_terms_to_options($children_terms); + } + } + else { + // Fallback to the low-performance function. + foreach($parents as $parent) { + $result[$parent] = hs_content_taxonomy_hierarchical_select_children($parent, $params); + } + } + + return $result; +} + +/** + * Implementation of hook_hierarchical_select_children_count(). + */ +function hs_content_taxonomy_hierarchical_select_children_count($parents, $params) { + $vid = $params['vid']; + $tid = $params['tid']; + $depth = $params['depth']; + + // If depth is unlimited every level is valid. + if ($depth == 0) { + $query = 'SELECT t.tid, COUNT(h.tid) AS children FROM {term_data} t LEFT JOIN {term_hierarchy} h ON t.tid = h.parent'; + $query .= ' WHERE t.vid = %d AND t.tid IN (' . db_placeholders($parents) . ')'; + $query .= ' GROUP BY t.tid'; + $result = db_query(db_rewrite_sql($query, 't', 'tid'), array_merge(array($vid), $parents)); + while ($row = db_fetch_object($result)) { + $counts[$row->tid] = $row->children; + } + } + else { + // Fallback to the low-performance way: select and count children. + foreach($parents as $parent) { + $counts[$parent] = count(hs_content_taxonomy_hierarchical_select_children($parent, $params)); + } + } + + return $counts; +} + +/** * Implementation of hook_hierarchical_select_lineage(). */ function hs_content_taxonomy_hierarchical_select_lineage($item, $params) { diff --git modules/hs_taxonomy.module modules/hs_taxonomy.module index b9eb95f..f99c958 100755 --- modules/hs_taxonomy.module +++ modules/hs_taxonomy.module @@ -731,45 +731,77 @@ function hierarchical_select_taxonomy_form_vocabulary_submit($form, &$form_state * two calls to it. */ function _hs_taxonomy_hierarchical_select_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL, $reset = FALSE) { - static $children, $parents, $terms; - + static $children, $parents, $terms; if ($reset) { $children = $parents = $terms = array(); } + + // Transform parent to array. Instead of single parent we pass multiple ones + // to the next step so that we can process them together in a single query. + if (!is_array($parent)) { + $parent = array($parent); + } + // Because we load tree step by step we can't know max number of children + // so we set it to a safe unreachable limit in case it's not provided. + $max_depth = (is_null($max_depth)) ? 99999999 : $max_depth; $depth++; - - // We cache trees, so it's not CPU-intensive to call get_tree() on a term - // and its children, too. - if (!isset($children[$vid])) { - $children[$vid] = array(); - - $result = db_query(db_rewrite_sql('SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d ORDER BY weight, name', 't', 'tid'), $vid); + $tree = array(); + + // Do we need current step ? + if ($max_depth <= $depth) { + return $tree; + } + + // Prepare queue for the "IN ( .. )" part of query. + $queue = array(); + foreach ($parent as $single_parent) { + // Queue branch for processing if it's not cached yet. + if (!isset($children[$vid][$single_parent])) { + $queue[] = $single_parent; + // Use an empty array to distinguish between a stub (without children) + // term and a branch that is not loaded yet. + $children[$vid][$single_parent] = array(); + } + } + // Run the query. + if (!empty($queue)) { + $query = 'SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid'; + $query .= ' WHERE t.vid = %d AND parent IN (' . db_placeholders($queue) . ')'; + // We may or may not need this: + //$query .= ' ORDER BY weight, name'; + $result = db_query(db_rewrite_sql($query, 't', 'tid'), array_merge(array($vid), $queue)); while ($term = db_fetch_object($result)) { $children[$vid][$term->parent][] = $term->tid; $parents[$vid][$term->tid][] = $term->parent; $terms[$vid][$term->tid] = $term; } } - - $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth; - if (isset($children[$vid][$parent])) { - foreach ($children[$vid][$parent] as $child) { - if ($max_depth > $depth) { - $term = drupal_clone($terms[$vid][$child]); - $term->depth = $depth; - // The "parent" attribute is not useful, as it would show one parent only. - unset($term->parent); - $term->parents = $parents[$vid][$child]; - $tree[] = $term; - - if (isset($children[$vid][$child])) { - $tree = array_merge($tree, _hs_taxonomy_hierarchical_select_get_tree($vid, $child, $depth, $max_depth)); + + $next_parent = array(); + foreach ($parent as $single_parent) { + foreach ($children[$vid][$single_parent] as $child) { + $term = drupal_clone($terms[$vid][$child]); + $term->depth = $depth; + // The "parent" attribute is not useful, as it would show one parent only. + unset($term->parent); + $term->parents = $parents[$vid][$child]; + $tree[] = $term; + + // Need more steps ? + if ($max_depth > $depth + 1) { + // Queue children for the next step down the tree. Do not process + // children which we already know as stub ones. + if (!isset($children[$vid][$child]) || !empty($children[$vid][$child])) { + $next_parent[] = $child; } } } + } + if (!empty($next_parent)) { + // Process multiple children together i.e. next level. + $tree = array_merge($tree, _hs_taxonomy_hierarchical_select_get_tree($vid, $next_parent, $depth, $max_depth)); } - return isset($tree) ? $tree : array(); }