J.me

Filter get_terms to return only terms with published post

So you are using custom taxonomy. You add terms to the taxonomy, and add the terms to your posts. Then, you use get_terms and list all your terms. Everything works fine, terms that doesn’t have any posts will not be returned if you set hide_empty argument to true. Now you move some of your posts to draft and you get a problem. Terms that doesn’t have any published posts still returned. This lead to 404 error when you click on the link of this term. It doesn’t look good now, we need to remove term that doesn’t have any published post.

So how to do that? The answer is by using WordPress filter. This piece of code below will solve the problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function get_terms_filter( $terms, $taxonomies, $args )
{
	global $wpdb;
	$taxonomy = $taxonomies[0];
	if ( ! is_array($terms) && count($terms) < 1 )
		return $terms;
	$filtered_terms = array();
	foreach ( $terms as $term )
	{
		$result = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts p JOIN $wpdb->term_relationships rl ON p.ID = rl.object_id WHERE rl.term_taxonomy_id = $term->term_id AND p.post_status = 'publish' LIMIT 1");
		if ( intval($result) > 0 )
			$filtered_terms[] = $term;
	}
	return $filtered_terms;
}
add_filter('get_terms', 'get_terms_filter', 10, 3);

The code work like this. We add a filter to the get_terms hook. This hook is called before the terms is returned, so it is perfect to place the filter here. Next, we check each returned terms and see if they have any published post, by using our own query. Why didn’t I use the WordPress WP_Query instead? The answer is I can’t get it to work, beside, it builds a pretty complicated query that might increase your WordPress site load time if it is used too much. Using our own query, we make sure it only use the query that we need. If our query returned that the term has a published post, we add this term to our new array which we use to store the filtered terms. Finally, we return the filtered terms.

Now, when you are using get_terms, you will filter out all terms that doesn’t have any published post. 🙂

Hope that helps. I have a hard time to explain the situation, so let me know if it doesn’t clear.

Thank you.

8 comments | Leave a comment

  1. Paul January 14, 2011 01:02

    Thanks for this – it’s helped me solve this very problem. However, it wasn’t quite right as it was filtering too aggressively and not showing all the tags in my taxonomy. I tracked down why, and have updated the SQL query to work as follows:

    $result = $wpdb->get_var(“SELECT COUNT(*) FROM $wpdb->posts p, $wpdb->term_relationships rl, $wpdb->term_taxonomy wpt WHERE p.ID = rl.object_id AND rl.term_taxonomy_id = wpt.term_taxonomy_id AND wpt.term_id = $term->term_id AND p.post_status = ‘publish’ LIMIT 1”);

    This isn’t a perfect solution, as it works on the admin area too, so hides any tags which you might have added but haven’t been used in published posts. I might continue to have a crack at it, but have a solution for my needs just now.

    • keaglez January 14, 2011 01:54

      Hi Paul,

      To not filter it on admin area, you can use is_admin() function to check if you are in admin area or not. In this way, you can make sure to not do any filter in the admin area.

  2. AEG June 25, 2012 04:30

    Great post. I have only this to add:

    1
    2
    3
    4
    
    if ( intval($result) > 0 )
    	$term->count = $result;
    	$filtered_terms[] = $term;
    }

    the second line was a small modification/addition that allowed me to use this function to correct my item counts for custom taxonomy. Thanks for the simple instructions!

    • keaglez June 30, 2012 17:09

      Hi AEG,

      A nice addition there. 🙂

      • AEG August 11, 2012 05:25

        I found myself once again making modifications to this code to address new concerns – like adding the count for posts that include children terms. So I thought I would post an update of the code that I am currently using for reference, should anyone else be looking for the same:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        
        if ( is_admin() || (!is_array($terms) && count($terms)get_var("SELECT COUNT(DISTINCT p.ID) FROM $wpdb->posts p, $wpdb->term_relationships rl, $wpdb->term_taxonomy wpt WHERE p.ID = rl.object_id AND rl.term_taxonomy_id = wpt.term_taxonomy_id AND wpt.term_id IN (SELECT term_id FROM $wpdb->term_taxonomy WHERE term_id=$term->term_id OR parent=$term->term_id) AND p.post_status = 'publish' LIMIT 1");
        	} else {
        		$result = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts p, $wpdb->term_relationships rl, $wpdb->term_taxonomy wpt WHERE p.ID = rl.object_id AND rl.term_taxonomy_id = wpt.term_taxonomy_id AND wpt.term_id = $term->term_id AND p.post_status = 'publish' LIMIT 1");
        	}
        	if ( !$args['hide_empty'] || intval($result)>0  ) {
        		$term->count = $result; //reset count to correct value
        		$filtered_terms[] = $term; //add the term to the new filtered set
        	}
        }

        For obvious reason, running a subquery for the children terms is less efficient, so you’ll notice that I added several more conditional checks for $args passed into the function. First, I set a ‘cache_domain’=’published’ value for any get_terms calls that I want to filter (thereby leaving the defaults alone unless I specify). Second, I look for the ‘pad_counts’ arg to determine whether children should be included in the count or not. And lastly, I also check the ‘hide_empty’ args to make determine whether or not to filter out queries that returned zero results.

        Thanks for the lead on this keaglez! It’s been very helpful.

  3. AEG August 11, 2012 05:29

    Something seems to have happened to the above code, so I’m trying again. (Please delete this post or ignore it, if the code once again gets tweaked)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
        if ( is_admin() || (!is_array($terms) && count($terms)get_var("SELECT COUNT(DISTINCT p.ID) FROM $wpdb->posts p, $wpdb->term_relationships rl, $wpdb->term_taxonomy wpt WHERE p.ID = rl.object_id AND rl.term_taxonomy_id = wpt.term_taxonomy_id AND wpt.term_id IN (SELECT term_id FROM $wpdb->term_taxonomy WHERE term_id=$term->term_id OR parent=$term->term_id) AND p.post_status = 'publish' LIMIT 1");
            } else {
                $result = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts p, $wpdb->term_relationships rl, $wpdb->term_taxonomy wpt WHERE p.ID = rl.object_id AND rl.term_taxonomy_id = wpt.term_taxonomy_id AND wpt.term_id = $term->term_id AND p.post_status = 'publish' LIMIT 1");
            }
            if ( !$args['hide_empty'] || intval($result)>0  ) {
                $term->count = $result; //reset count to correct value
                $filtered_terms[] = $term; //add the term to the new filtered set
            }
        }
    • keaglez August 11, 2012 06:00

      That looked really nice AEG! 🙂

      And just knew that code highlighting also work for the comment section!

  4. phil December 10, 2013 15:13

    hi

    i need to retrieve taxonomy for publish and future posts, i did this but does not seem to work…
    p.post_status = ‘publish,future’

    thanks a lot !

Leave a comment