¿Cómo mostrar una lista jerárquica de términos?
Tengo una taxonomía jerárquica llamada 'ubicaciones geográficas'. Contiene continentes en el primer nivel, y luego los países para cada uno. Ejemplo:
Europa
- Irlanda
- España
- Suecia
Asia
- Laos
- Tailandia
- Vietnam
etc.
Usando get_terms() logré mostrar la lista completa de términos, pero los continentes se mezclan con los países en una gran lista plana.
¿Cómo puedo mostrar una lista jerárquica como la de arriba?

Me doy cuenta de que esta es una pregunta muy antigua, pero si necesitas construir una estructura real de términos, este podría ser un método útil para ti:
/**
* Ordena recursivamente un array de términos de taxonomía jerárquicamente. Las categorías hijas se
* colocarán bajo un miembro 'children' de su término padre.
* @param Array $cats objetos de términos de taxonomía para ordenar
* @param Array $into array de resultado donde colocarlos
* @param integer $parentId el ID padre actual donde colocarlos
*/
function sort_terms_hierarchically(Array &$cats, Array &$into, $parentId = 0)
{
foreach ($cats as $i => $cat) {
if ($cat->parent == $parentId) {
$into[$cat->term_id] = $cat;
unset($cats[$i]);
}
}
foreach ($into as $topCat) {
$topCat->children = array();
sort_terms_hierarchically($cats, $topCat->children, $topCat->term_id);
}
}
El uso es el siguiente:
$categories = get_terms('my_taxonomy_name', array('hide_empty' => false));
$categoryHierarchy = array();
sort_terms_hierarchically($categories, $categoryHierarchy);
var_dump($categoryHierarchy);

Esto es realmente bueno. Cambiaría una cosa: $into[$cat->term_id] = $cat;
por $into[] = $cat;
Tener el ID del término como clave del array es molesto (no puedes obtener el primer elemento fácilmente usando la clave 0) e innecesario (ya estás almacenando el objeto $cat
y puedes obtener el id usando la propiedad term_id
.

Si como yo estás intentando aplicar esta función a un subnivel de categorías, necesitarás pasar el ID del nivel en el que te encuentras actualmente para que funcione. Pero funciona muy bien, gracias @popsi.

Usa wp_list_categories
con el argumento 'taxonomy' => 'taxonomía'
, está diseñado para crear listas jerárquicas de categorías pero también soporta el uso de taxonomías personalizadas.
Ejemplo del Codex:
Mostrar términos en una taxonomía personalizada
Si la lista aparece plana, es posible que solo necesites un poco de CSS para agregar relleno a las listas y así poder ver su estructura jerárquica.

No conozco ninguna función que haga exactamente lo que buscas, pero puedes construir algo como esto:
<ul>
<?php $hiterms = get_terms("my_tax", array("orderby" => "slug", "parent" => 0)); ?>
<?php foreach($hiterms as $key => $hiterm) : ?>
<li>
<?php echo $hiterm->name; ?>
<?php $loterms = get_terms("my_tax", array("orderby" => "slug", "parent" => $hiterm->term_id)); ?>
<?php if($loterms) : ?>
<ul>
<?php foreach($loterms as $key => $loterm) : ?>
<li><?php echo $loterm->name; ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
No he probado este código pero puedes entender la idea. Lo que hace el código anterior es mostrarte solo dos niveles de términos.
EDITADO: ah sí, puedes usar wp_list_categories() para lograr lo que buscas.

De hecho, esto es bastante útil, ya que necesito tener enlaces personalizados (con un parámetro GET) en los enlaces de términos, lo cual no parece posible con la forma de hacerlo de wp_list_categories()
.

Sí, este método te dará más control sobre tu salida. Pero podrías hacer un buen trabajo de buscar y reemplazar en la salida de wp_list_categories()
para añadir tus parámetros GET. O incluso mejor, construir un filtro para la función y añadir las partes que quieras. No me preguntes cómo se hace eso porque todavía no he podido entenderlo :(

Yo sugeriría usar un walker de categorías personalizado con wp_list_categories
si quieres mayor control sobre la salida, hará que tu código sea mucho más reutilizable.

El siguiente código generará un menú desplegable con términos, pero también puede generar cualquier otro elemento/estructura editando la variable $outputTemplate y modificando las líneas de str_replace:
function get_terms_hierarchical($terms, $output = '', $parent_id = 0, $level = 0) {
//Plantilla de salida
$outputTemplate = '<option value="%ID%">%PADDING%%NAME%</option>';
foreach ($terms as $term) {
if ($parent_id == $term->parent) {
//Reemplazando las variables de la plantilla
$itemOutput = str_replace('%ID%', $term->term_id, $outputTemplate);
$itemOutput = str_replace('%PADDING%', str_pad('', $level*12, ' '), $itemOutput);
$itemOutput = str_replace('%NAME%', $term->name, $itemOutput);
$output .= $itemOutput;
$output = get_terms_hierarchical($terms, $output, $term->term_id, $level + 1);
}
}
return $output;
}
$terms = get_terms('taxonomy', array('hide_empty' => false));
$output = get_terms_hierarchical($terms);
echo '<select>' . $output . '</select>';

Utilicé el código de @popsi que funcionaba muy bien y lo hice más eficiente y fácil de leer:
/**
* Ordena recursivamente un array de términos de taxonomía de forma jerárquica. Las categorías hijas se
* colocarán bajo un miembro 'children' de su término padre.
* @param Array $cats objetos de términos de taxonomía para ordenar
* @param integer $parentId el ID padre actual para colocarlos
*/
function sort_terms_hierarchicaly(Array $cats, $parentId = 0)
{
$into = [];
foreach ($cats as $i => $cat) {
if ($cat->parent == $parentId) {
$cat->children = sort_terms_hierarchicaly($cats, $cat->term_id);
$into[$cat->term_id] = $cat;
}
}
return $into;
}
Uso:
$sorted_terms = sort_terms_hierarchicaly($terms);

Mientras buscaba lo mismo pero para obtener los términos de una entrada, finalmente compilé esto y me funciona.
Lo que hace:
• Obtiene todos los términos de una taxonomía específica para una entrada determinada.
• Para una taxonomía jerárquica con dos niveles (ej: nivel1:'país' y nivel2:'ciudades'), crea un h4 con el nivel1 seguido de una lista ul de nivel2, y esto para todos los elementos de nivel1.
• Si la taxonomía no es jerárquica, creará solo una lista ul con todos los elementos.
Aquí está el código (lo escribí para mí, así que intenté que fuera lo más genérico posible, pero...):
function finishingLister($heTerm){
$myterm = $heTerm;
$terms = get_the_terms($post->ID,$myterm);
if($terms){
$count = count($terms);
echo '<h3>'.$myterm;
echo ((($count>1)&&(!endswith($myterm, 's')))?'s':"").'</h3>';
echo '<div class="'.$myterm.'Wrapper">';
foreach ($terms as $term) {
if (0 == $term->parent) $parentsItems[] = $term;
if ($term->parent) $childItems[] = $term;
};
if(is_taxonomy_hierarchical( $heTerm )){
foreach ($parentsItems as $parentsItem){
echo '<h4>'.$parentsItem->name.'</h4>';
echo '<ul>';
foreach($childItems as $childItem){
if ($childItem->parent == $parentsItem->term_id){
echo '<li>'.$childItem->name.'</li>';
};
};
echo '</ul>';
};
}else{
echo '<ul>';
foreach($parentsItems as $parentsItem){
echo '<li>'.$parentsItem->name.'</li>';
};
echo '</ul>';
};
echo '</div>';
};
};
Finalmente, llamas a la función con esto (obviamente, reemplazas my_taxonomy por el tuyo): finishingLister('my_taxonomy');
No pretendo que sea perfecto, pero como dije, funciona para mí.

Tuve este problema y ninguna de las respuestas aquí funcionó para mí, por una razón u otra.
Aquí está mi versión actualizada y que funciona.
function locationSelector( $fieldName ) {
$args = array('hide_empty' => false, 'hierarchical' => true, 'parent' => 0);
$terms = get_terms("locations", $args);
$html = '';
$html .= '<select name="' . $fieldName . '"' . 'class="chosen-select ' . $fieldName . '"' . '>';
foreach ( $terms as $term ) {
$html .= '<option value="' . $term->term_id . '">' . $term->name . '</option>';
$args = array(
'hide_empty' => false,
'hierarchical' => true,
'parent' => $term->term_id
);
$childterms = get_terms("locations", $args);
foreach ( $childterms as $childterm ) {
$html .= '<option value="' . $childterm->term_id . '">' . $term->name . ' > ' . $childterm->name . '</option>';
$args = array('hide_empty' => false, 'hierarchical' => true, 'parent' => $childterm->term_id);
$granchildterms = get_terms("locations", $args);
foreach ( $granchildterms as $granchild ) {
$html .= '<option value="' . $granchild->term_id . '">' . $term->name . ' > ' . $childterm->name . ' > ' . $granchild->name . '</option>';
}
}
}
$html .= "</select>";
return $html;
}
Y su uso:
$selector = locationSelector('locationSelectClass');
echo $selector;

Esta solución es menos eficiente que el código de @popsi, ya que realiza una nueva consulta por cada término, pero también es más fácil de usar en una plantilla. Si tu sitio web utiliza caché, puede que, como yo, no te importe la pequeña sobrecarga en la base de datos.
No necesitas preparar un array que se llenará recursivamente con términos. Simplemente lo llamas de la misma forma que llamarías a get_terms() (la forma no obsoleta con solo un array como argumento). Devuelve un array de objetos WP_Term
con una propiedad adicional llamada children
.
function get_terms_tree( Array $args ) {
$new_args = $args;
$new_args['parent'] = $new_args['parent'] ?? 0;
$new_args['fields'] = 'all';
// Los términos para este nivel
$terms = get_terms( $new_args );
// Los hijos de cada término en este nivel
foreach( $terms as &$this_term ) {
$new_args['parent'] = $this_term->term_id;
$this_term->children = get_terms_tree( $new_args );
}
return $terms;
}
El uso es sencillo:
$terms = get_terms_tree([ 'taxonomy' => 'my-tax' ]);

Un último golpe final a este caballo (similar a las respuestas anteriores) con un poco más de refinamiento... permitiéndote también transformar la lista en una lista jerárquicamente plana con un parámetro depth
.
Adicionalmente, esta función utiliza clone
para evitar modificar el arreglo original.
/**
* Ordena una lista de objetos de términos jerárquicamente o de forma jerárquico-plana.
*/
function sort_terms_hierarchically( $terms, $parent_id = 0, $flat = FALSE, $depth = 0 ) {
$data = [];
foreach ( $terms as $term ) {
if ( $parent_id != $term->parent ) continue;
$data_term = clone $term;
$data_term->depth = $depth;
$children = sort_terms_hierarchically( $terms, $term->term_id, $flat, $depth + 1 );
if ( ! $flat ) $data_term->children = $children;
$data[] = $data_term;
if ( $flat ) foreach( $children as $child ) $data[] = $child;
}
return $data;
}
function sort_terms_hierarchically_flat( $terms, $parent_id = 0 ) {
return sort_terms_hierarchically( $terms, $parent_id, TRUE );
}
El uso es el siguiente:
$terms = get_terms( [
'taxonomy' => 'regiones',
'hide_empty' => FALSE,
] );
$terms_sorted = sort_terms_hierarchically( $terms );
$terms_sorted_flat = sort_terms_hierarchically_flat( $terms );

Asegúrate de que hierarchical=true
se pase en tu llamada a get_terms()
.
Ten en cuenta que hierarchical=true
es el valor por defecto, así que en realidad, solo asegúrate de que no haya sido sobrescrito para que sea false
.

¿Comentando una respuesta dejada hace casi dos años? ¿En serio? En realidad, sí es una respuesta propuesta, aunque esté formulada como una pregunta. ¿Debería editarla para que sea una afirmación en lugar de una pregunta?

Aquí tengo una lista desplegable de cuatro niveles con el primer elemento oculto
<select name="lokalizacja" id="ucz">
<option value="">Todas las ubicaciones</option>
<?php
// Obtener término excluido por slug
$excluded_term = get_term_by('slug', 'podroze', 'my_travels_places');
$args = array(
'orderby' => 'slug',
'hierarchical' => 'true',
'exclude' => $excluded_term->term_id,
'hide_empty' => '0',
'parent' => $excluded_term->term_id,
);
// Obtener términos de nivel superior
$hiterms = get_terms("my_travels_places", $args);
foreach ($hiterms AS $hiterm) :
echo "<option value='".$hiterm->slug."'".($_POST['my_travels_places'] == $hiterm->slug ? ' selected="selected"' : '').">".$hiterm->name."</option>\n";
// Obtener términos de segundo nivel
$loterms = get_terms("my_travels_places", array("orderby" => "slug", "parent" => $hiterm->term_id,'hide_empty' => '0',));
if($loterms) :
foreach($loterms as $key => $loterm) :
echo "<option value='".$loterm->slug."'".($_POST['my_travels_places'] == $loterm->slug ? ' selected="selected"' : '')."> - ".$loterm->name."</option>\n";
// Obtener términos de tercer nivel
$lo2terms = get_terms("my_travels_places", array("orderby" => "slug", "parent" => $loterm->term_id,'hide_empty' => '0',));
if($lo2terms) :
foreach($lo2terms as $key => $lo2term) :
echo "<option value='".$lo2term->slug."'".($_POST['my_travels_places'] == $lo2term->slug ? ' selected="selected"' : '')."> - ".$lo2term->name."</option>\n";
endforeach;
endif;
endforeach;
endif;
endforeach;
?>
</select>
<label>Selecciona el tipo de lugar</label>
<select name="rodzaj_miejsca" id="woj">
<option value="">Todos los tipos</option>
<?php
// Obtener todos los términos del tipo de lugar
$theterms = get_terms('my_travels_places_type', 'orderby=name');
foreach ($theterms AS $term) :
echo "<option value='".$term->slug."'".($_POST['my_travels_places_type'] == $term->slug ? ' selected="selected"' : '').">".$term->name."</option>\n";
endforeach;
?>
</select>

Creo que la lógica es que es un problema relacionado. Encontré esta publicación intentando descubrir cómo obtener una lista de verificación jerárquica al estilo de categorías y estoy tentado a agregar una respuesta aquí ahora que lo he descubierto. No lo haré porque, como señalas, no responde a la pregunta original.
