10

I have a hierarchical custom taxonomy which I can display using print_r(get_the_terms( $post->ID, 'taxonomic_rank' ));:

Array
(
    [46] => stdClass Object
        (
            [term_id] => 46
            [name] => Aplocheilidae
            [slug] => aplocheilidae
            [term_group] => 0
            [term_taxonomy_id] => 53
            [taxonomy] => taxonomic_ranks
            [description] => 
            [parent] => 39
            [count] => 1
            [object_id] => 443
        )

    [47] => stdClass Object
        (
            [term_id] => 47
            [name] => Aplocheilus
            [slug] => aplocheilus
            [term_group] => 0
            [term_taxonomy_id] => 54
            [taxonomy] => taxonomic_ranks
            [description] => 
            [parent] => 46
            [count] => 1
            [object_id] => 443
        )

    [39] => stdClass Object
        (
            [term_id] => 39
            [name] => Cyprinodontiformes
            [slug] => cyprinodontiformes
            [term_group] => 0
            [term_taxonomy_id] => 52
            [taxonomy] => taxonomic_ranks
            [description] => 
            [parent] => 0
            [count] => 1
            [object_id] => 443
        )

)

This taxonomy will always take the following form: Order (parent) > Family (child of Order) > Sub-family (child of Family)

Is there a quick and easy way of displaying these taxonomies in the correct order, so that I could print out the following line? Order: <order>, Family: <family>, Sub-family: <sub-family>

Thanks in advance

1
  • 5
    Whoever down-marked me, would you please explain why?
    – turbonerd
    Dec 26, 2011 at 13:09

7 Answers 7

8

There are probably some better ways to do this but you can always do a three simple foreach loops.

I wrote an example function that does the job well and should serve you as a good starting point:

function print_taxonomic_ranks( $terms = '' ){

    // check input
    if ( empty( $terms ) || is_wp_error( $terms ) || ! is_array( $terms ) )
        return;

    // set id variables to 0 for easy check 
    $order_id = $family_id = $subfamily_id = 0;

    // get order
    foreach ( $terms as $term ) {
        if ( $order_id || $term->parent )
            continue;
        $order_id  = $term->term_id;
        $order     = $term->name;
    }

    // get family
    foreach ( $terms as $term ) { 
        if ( $family_id || $order_id != $term->parent )
            continue;
        $family_id = $term->term_id;
        $family    = $term->name;
    }

    // get subfamily
    foreach ( $terms as $term ) { 
        if ( $subfamily_id || $family_id != $term->parent ) 
            continue;
        $subfamily_id = $term->term_id;
        $subfamily    = $term->name;
    }

    // output
    echo "Order: $order, Family: $family, Sub-family: $subfamily";

}

Let it live in your functions.php file and use it in your templates like this:

print_taxonomy_ranks( get_the_terms( $post->ID, 'taxonomic_rank' ) );

NOTE: Looping the same array three times around sounds a bit stupid but on the other hand it's a quick and easy solution which is easy to read, extend and maintain.

5
  • I couldn't be further in your debt. That is a perfect answer, and I wish I could give you more reputation!! If anyone passes this post, please +1 it so that Maugly can be rewarded as much as I'd like :)
    – turbonerd
    Dec 27, 2011 at 14:23
  • 1
    No worries. I'm glad I could help :) I've also updated the code in my answer and added a simple input check...
    – Michal Mau
    Dec 27, 2011 at 15:10
  • Maugly, thanks for your answer, it's almost exactly what I needed- do you have any thoughts on how to use it with the archive links still attached to the terms? Thanks again
    – user12391
    Jan 24, 2012 at 3:55
  • @Adam Take a look on get_term_link()
    – Michal Mau
    Jan 24, 2012 at 10:36
  • What if the post has two parent categories? I'm trying to fix it, but it's taking me too long to figure it out yet. Jun 2, 2015 at 5:20
4

Bit old topic but still relevant I think as it's still a complete pain.

I'm using this recursive function that takes in two arrays as references. It will create an array with the structure of: [term_id] => term_object->children->child_terms_array->children->child_terms_array.

<?php
function sort_terms_hierarchically( array &$terms, array &$into, $parent_id = 0 ) {
    foreach ( $terms as $i => $term ) {
        if ( $term->parent == $parent_id ) {
            $into[$term->term_id] = $term;
            unset( $terms[ $i ] );
        }
    }

    foreach ( $into as $top_term ) {
        $top_term->children = array();
        $this->sort_terms_hierarchically( $terms, $top_term->children, $top_term->term_id );
    }

}

$terms = get_the_terms( 'taxslug', $post );
$sorted_terms = array();
sort_terms_hierarchically( $terms, $sorted_terms );

// Will log the nested arrays of term objects.
error_log( print_r( $sorted_terms, true ) );

This is the only solution I've ever found that maintain the term objects and works with any amount of nesting.

1
  • Great answer, thank you. Still helpful in 2022! There's a slight syntax error with a $this-> mismatch when calling the function but the logic is perfect
    – Ian
    Apr 7, 2022 at 14:18
3

Though Maugly's approach seem a bit more readable but running a loop 3 times over the array doesn't seem right to me. So here is just another approach which might be less readable for some but works without running loop 3 times.

function print_taxonomy_ranks( $terms ) {
    // if terms is not array or its empty don't proceed
    if ( ! is_array( $terms ) || empty( $terms ) ) {
        return false;
    }

    foreach ( $terms as $term ) {
        // if the term have a parent, set the child term as attribute in parent term
        if ( $term->parent != 0 )  {
            $terms[$term->parent]->child = $term;   
        } else {
            // record the parent term
            $parent = $term;
        }
    }

    echo "Order: $parent->name, Family: {$parent->child->name}, Sub-Family: {$parent->child->child->name}";
}
4
  • Nice work! I knew that it is possible, I just couldn't wrap my head around it back then :) I like your solution!
    – Michal Mau
    Jan 24, 2012 at 12:34
  • Downvote Warning: Creating default object from empty value Sep 25, 2020 at 10:32
  • @Dev can you please elaborate? Which object are you talking about. Although its 9 years old answer but I would love to know which object are you referring to Sep 27, 2020 at 4:14
  • That's the error message i get when testing. It doesn't state which object. Sep 27, 2020 at 4:35
3

I had a situation where a post could be tagged with multiple category groups, and multiple children within parent categories, so I wanted my hierarchy to reflect that. I also just wanted a few lines of code:

$terms = get_the_terms($id, 'department_categories');
foreach($terms as $key => $term){
    if($term->parent != 0){
        $terms[$term->parent]->children[] = $term;
        unset($terms[$key]);
    }
}

Basically after it finds a category's parent, it moves it to the parent's obj as a child, and then removes it from it's original position in the array. I've tested this using multiple siblings, children, and different levels of categories.

Hope someone else finds this useful in case they're just looking for some logic guidance rather than a "plugin"!

2
  • Can you please explain how to use that? Jun 2, 2015 at 5:22
  • @LucasB I'll try to recall :P We have a post, and we know it's $post->ID is $id and "department_categories" would be the taxonomy. It simply gets all terms associated to the post and restructures the array of results to make more hierarchical sense. Once restructured, you're free to output it as you see fit. Jun 2, 2015 at 13:22
2

Inspired by Michal Mau answer and Manny Fleurmond answer here is my solution: I have the same issue as @Lucas Bustamante: What if the post has two parent categories?

My solution was to create another array of objects, comparing the term_id and checking the parent key

function print_taxonomic_ranks( $terms ){

    if ( ! is_array( $terms ) || empty( $terms ) ) {
        return false;
    }

    $parent_terms = array();

    // get only parents
    foreach ( $terms as $term ) {
        if ($term->parent === 0) {
            $term->child = Array();
            $parent_terms[] = $term;
        }
    }

    // compare and nested
    foreach ( $terms as $term ) {
        if ($term->parent != 0) {
            foreach ($parent_terms as $key => $value) {
                if ($term->parent === $value->term_id) {
                    $parent_terms[$key]->child[] = $term;
                }
            }
        }
    }

    // output results
    foreach ( $parent_terms as $term ) {

        //parent term
        echo '<span class="d-block">'.$term->name.'';

        if ($term->child) {
            $i = 1;
            foreach ( $term->child as $child ) {
                //echo '<span class="text-danger">'.$i.'</span>';
                echo ($i <= 1)? ": " : "";
                echo '<span class="font-weight-normal">'.$child->name.'</span>';
                echo ($i < count($term->child))? ", " : "";
            $i++;                
            }          
        }
        echo '.<span>';
    }
}

and it will look something like this:

get terms in parent order

Cheers!

1
  • The <span> was not closed in end of code echo '.<span>' => echo '.</span>' Feb 3, 2021 at 13:13
1

Thanks Maugly,

Here's my modified version of your code that includes the term permalinks if anyone needs it

function print_show_location( $terms = '' ){

// check input
if ( empty( $terms ) || is_wp_error( $terms ) || ! is_array( $terms ) )
    return;

// set id variables to 0 for easy check 
$country_id = $state_id = $city_id = 0;

// get country
foreach ( $terms as $term ) {
    if ( $country_id || $term->parent )
        continue;
    $country_id  = $term->term_id;
    $country_slug = $term->slug;
    $country = '<a href="'.get_term_link($country_slug, 'location').'">'.$term->name.'</a>';
}

// get state
foreach ( $terms as $term ) { 
    if ( $state_id || $country_id != $term->parent )
        continue;
    $state_id = $term->term_id;
    $state_slug = $term->slug;
    $state = '<a href="'.get_term_link($state_slug, 'location').'">'.$term->name.'</a>';
}

// get city
foreach ( $terms as $term ) { 
    if ( $city_id || $state_id != $term->parent ) 
        continue;
    $city_id = $term->term_id;
    $city_slug = $term->slug;
    $city = '<a href="'.get_term_link($city_slug, 'location').'">'.$term->name.'</a>';
}

// output
echo "$city, $state - $country";

}
0

There are a couple of recursive functions I use depending on my needs. Both need the list of terms, a starting parent id and a starting (generally empty) array where to put the resulting list.

FIRST VERSION

$terms = [a list of terms objects, use get_terms of whatever]
$ordered_terms = array(); // here you'll find your ordered terms, from root to final child

function list_terms_by_parent($parent_id = 0, &$terms, &$ordered_terms){
  $root_parent = $parent_id;

  foreach($terms as $index => $term){
    if($term->parent == (int) $parent_id){
      $ordered_terms[$term->term_id] = $term;
      $root_parent = $term->term_id;
      unset($terms[$index]);
    }
  }

  if(!empty($terms)) list_terms_by_parent($root_parent, $terms, $ordered_terms);
}

SECOND VERSION

$term_ids = [should be a list of ids in the form of child_id => parent_id]

// a fast way to get that kind of list is to use WP_Term_query, example:
//$terms_query = new WP_Term_Query(array(
//  'taxonomy' => 'product_categories'
//  ,'object_ids' => $post->ID
//  ,'hide_empty' => false
//  ,'fields' => 'id=>parent'
//));


$ordered_terms = array(); // here you'll find your ordered terms, from root to final child

function list_term_ids_by_parent($parent_id = 0, &$term_ids, &$ordered_terms){
  $child_id = array_search($parent_id, $term_ids);

  if($child_id){
    $ordered_terms[] = $child_id;
    unset($term_ids[$child_id]);
  }

  if(!empty($term_ids)) order_terms($child_id, $term_ids, $ordered_terms);
}

THIRD VERSION

This is not mine, I found it somewhere, probably here or in stackoverflow. This is a little different in output, since it'll generate a cascading terms list, with only one root, which has a children property, which will contain the next term with its children property and so on..

// $ordered_terms is supposed to be an empty array as above

function sort_terms_hierarchically(&$terms, &$ordered_terms, $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);
  }
}

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