49

I have a hierarchical taxonomy called 'geographical locations'. It contains continents on a first level, and then the countries for each one. Example :

Europe
- Ireland
- Spain
- Sweden
Asia
- Laos
- Thailand
- Vietnam

etc.

Using get_terms() I managed to output the full list of terms, but the continents get mixed up with the countries, in one big flat list.

How can I output a hierarchical list like above?

1
  • 3
    In case anyone needs a hierarchical CHECKLIST (not the question here but related for people building custom UI for hierarchical taxonomies), the best answer is to use wp_terms_checklist() with your custom taxonomy.
    – jerclarke
    Commented Feb 23, 2016 at 16:27

12 Answers 12

24

Use wp_list_categories with the 'taxonomy' => 'taxonomy' argument, it's built for creating hierarchical category lists but will also support using a custom taxonomy..

Codex Example:
Display terms in a custom taxonomy

If the list comes back looking flat, it's possible you just need a little CSS to add padding to the lists, so you can see their hierarchical structure.

1
  • Could this be reversed? Display children first..
    – Arg Geo
    Commented Mar 6, 2016 at 7:57
64

I realize, this is a very old question, but if you have a need to build up an actual structure of terms, this might be a useful method for you:

/**
 * Recursively sort an array of taxonomy terms hierarchically. Child categories will be
 * placed under a 'children' member of their parent term.
 * @param Array   $cats     taxonomy term objects to sort
 * @param Array   $into     result array to put them in
 * @param integer $parentId the current parent ID to put them in
 */
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);
    }
}

Usage is as follows:

$categories = get_terms('my_taxonomy_name', array('hide_empty' => false));
$categoryHierarchy = array();
sort_terms_hierarchically($categories, $categoryHierarchy);

var_dump($categoryHierarchy);
3
  • 6
    This is actually really good. I would change one thing: $into[$cat->term_id] = $cat; into $into[] = $cat; Having the ID of the term as the array key is annoying (you can't get the first element easily using the 0 key) and useless (you're already storing the $cat object and you can get the id using the term_id property.
    – Nahuel
    Commented Mar 7, 2017 at 15:03
  • If like me you're trying to apply this function to a sub-level of categories, you will need to pass in the ID of the level you're currently at for this to work. But work nicely it does, thanks @popsi. Commented Aug 16, 2018 at 10:19
  • that works, thank you Commented Nov 12, 2019 at 16:35
14

I dont know of any function that does what you want but you can build up something like this:

<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>

I haven't tested this but you can see what I'm getting at. What the above code will do is give you only two levels

EDIT: ahh yes you can use wp_list_categories() to do what you after.

3
  • Actually this is quite useful, as I need to have custom links (with a GET param) on the term links, which doesn't seem possible with the wp_list_categories() way of doing it.
    – mike23
    Commented Apr 13, 2011 at 15:04
  • 1
    Yes this method will give more control over your output. But you could do some nice bit of find and replace on the output of wp_list_categories() to add in your GET parameters. Or even better build a filter for the function to add in the bits you want. Don't ask me how you do that as I've not yet been able to get my head around it :(
    – Scott
    Commented Apr 13, 2011 at 15:19
  • 3
    I'd suggest using a custom category walker with wp_list_categories if you want greater control over the output, it'll make your code much more reusable..
    – t31os
    Commented Apr 13, 2011 at 16:09
6

You can use wp_list_categories(), with a 'taxonomy' argument.

5

The following code will generate drop-down with terms, but also can generate any other element/structure by editing the $outputTemplate variable, and editing str_replace lines:

function get_terms_hierarchical($terms, $output = '', $parent_id = 0, $level = 0) {
    //Out Template
    $outputTemplate = '<option value="%ID%">%PADDING%%NAME%</option>';

    foreach ($terms as $term) {
        if ($parent_id == $term->parent) {
            //Replacing the template variables
            $itemOutput = str_replace('%ID%', $term->term_id, $outputTemplate);
            $itemOutput = str_replace('%PADDING%', str_pad('', $level*12, '&nbsp;&nbsp;'), $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>';  
5

I used @popsi code that was working really well and I made it a more efficient and easy to read:

/**
 * Recursively sort an array of taxonomy terms hierarchically. Child categories will be
 * placed under a 'children' member of their parent term.
 * @param Array   $cats     taxonomy term objects to sort
 * @param integer $parentId the current parent ID to put them in
 */
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;
}

Usage :

$sorted_terms = sort_terms_hierarchicaly($terms);
3

As I was looking for the same but to get terms of one post, finally I compiled this, and it works for me.

What it does :
• it gets all terms of a taxonomy name for a specific post.
• for a hierachical taxonomy with two levels (ex: level1:'country' and level2:'cities'), it creates a h4 with the level1 followed by an ul list of level2 and this for all level1 items.
• if the taxonomy is not hierarchical, it will create only an ul list of all items. here is the code (I write it for me so I tried to be as generic as I can but...) :

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>';
    };
};

So finally you call the function with this (obviously, you replace my_taxonomy by yours) : finishingLister('my_taxonomy');

I don't pretend it's perfect but as I said it works for me.

1

I had this problem and none of the answers here worked for me, for one reason or another.

Here is my updated and working version.

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;
}

And usage:

$selector = locationSelector('locationSelectClass');
echo $selector;
0

This solution is less efficient than @popsi's code, since it makes a new query for every term, but it's also easier to use in a template. If your website uses caching, you may, like me, not mind the slight database overhead.

You don't need to prepare an array that'll be recursively filled with terms. You just call it the same way you would call get_terms() (the non-deprecated form with only an array for an argument). It returns an array of WP_Term objects with an extra property called children.

function get_terms_tree( Array $args ) {
    $new_args = $args;
    $new_args['parent'] = $new_args['parent'] ?? 0;
    $new_args['fields'] = 'all';

    // The terms for this level
    $terms = get_terms( $new_args );

    // The children of each term on this level
    foreach( $terms as &$this_term ) {
        $new_args['parent'] = $this_term->term_id;
        $this_term->children = get_terms_tree( $new_args );
    }

    return $terms;
}

Usage is simple:

$terms = get_terms_tree([ 'taxonomy' => 'my-tax' ]);
0

One final death-blow to this horse (similar to the above answers) with a bit more refinement ... allowing you to also transform the list into a hierarchically-flat list with a depth param.

Additionally, this function uses clone to avoid modifying the original array.

/**
 * Sort terms object list hierarchically or hierarchically-flat.
 */
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 );
}

Usage is as follows:

$terms = get_terms( [
    'taxonomy' => 'regions',
    'hide_empty' => FALSE,
] );

$terms_sorted = sort_terms_hierarchically( $terms );
$terms_sorted_flat = sort_terms_hierarchically_flat( $terms );
-1

Be sure that hierarchical=true is passed to your get_terms() call.

Note that hierarchical=true is the default, so really, just be sure that it hasn't been overridden to be false.

4
  • Hi Chip, yes 'hierarchical' is 'true' by default.
    – mike23
    Commented Apr 13, 2011 at 14:37
  • Can you provide a link to a live example of the output? Commented Apr 13, 2011 at 14:44
  • Commenting on an answer left almost two years ago? Really? Actually, it is a proposed answer, even if worded as a question. Shall I edit it to be a statement, rather than a question? Commented Feb 11, 2013 at 23:22
  • get_terms() will return a full list of the terms (as the OP stated) but not a hierarchical list showing parent / child relationship as requested.
    – jdm2112
    Commented Mar 17, 2016 at 5:00
-1

Here I have four level dropdown select list with hidden first item

<select name="lokalizacja" id="ucz">
            <option value="">Wszystkie lokalizacje</option>
            <?php
            $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,
            );              
            $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";

                $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"' : '').">&nbsp;-&nbsp;".$loterm->name."</option>\n";

                    $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"' : '').">&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;".$lo2term->name."</option>\n";



                        endforeach;
                    endif;

                    endforeach;
                endif;

            endforeach;
            ?>
         </select>
        <label>Wybierz rodzaj miejsca</label>
        <select name="rodzaj_miejsca" id="woj">
            <option value="">Wszystkie rodzaje</option>
            <?php
            $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>
2
  • 2
    Please explain why that could solve the problem.
    – fuxia
    Commented Mar 25, 2013 at 10:00
  • I think the logic is that it's a related problem. I found this post trying to figure out how to get a category-style hierarchical checklist and am tempted to add an answer here now that I've figured it out. I won't though because as you point out it doesn't answer the OQ.
    – jerclarke
    Commented Feb 23, 2016 at 16:26

Not the answer you're looking for? Browse other questions tagged or ask your own question.