get_terms by custom post type

I have two custom post types ‘country’ and ‘city’ and a shared taxonomy ‘flag’.

If I use:

Read More
<?php $flags = get_terms('flag', 'orderby=name&hide_empty=0');

I get a list of all terms in the taxonomy, but I want to limit the list to the post type ‘country’.

How can I do it?


Using the new solution

<?php 
$flags = wpse57444_get_terms('flags',array('parent' => 0,'hide_empty' => 1,'post_types' =>array('country')));
foreach ($flags as $flag) {
    $childTerms = wpse57444_get_terms('flags',array('parent' => $flag->term_id,'hide_empty' => 1,'post_types' =>array('country')));
    foreach ($childTerms as $childTerm) {
        echo $childTerm->name.'<br />';
    }
}
?>

I can’t echo $childTerm->name. Why?

Related posts

Leave a Reply

5 comments

  1. I’m afraid this isn’t possible natively (yet?). See this trac: http://core.trac.wordpress.org/ticket/18106

    Similarly on the taxonomy admin page the post count reflects all post types. (I’m pretty sure there is a trac ticket for that too)
    http://core.trac.wordpress.org/ticket/14084

    See also, this related post.


    New solution

    Having written the one below, I’ve released a much better way (alteast in the sense that you can do more) is to use the filters provided in the get_terms() call. You can create a wrapper function that uses get_terms and (conditionally) adds a filter to manipulate the SQL query (to restrict by post type).

    The function takes the same arguments as get_terms($taxonomies, $args). $args takes the additional argument of post_types which takes an array|string of post types.

    But I can’t gurantee that everything works ‘as expected’ (I’m thinking padding the count). It does appear to work using just default the $args for get_terms.

    function wpse57444_get_terms( $taxonomies, $args=array() ){
        //Parse $args in case its a query string.
        $args = wp_parse_args($args);
    
        if( !empty($args['post_types']) ){
            $args['post_types'] = (array) $args['post_types'];
            add_filter( 'terms_clauses','wpse_filter_terms_by_cpt',10,3);
    
            function wpse_filter_terms_by_cpt( $pieces, $tax, $args){
                global $wpdb;
    
                // Don't use db count
                $pieces['fields'] .=", COUNT(*) " ;
    
                //Join extra tables to restrict by post type.
                $pieces['join'] .=" INNER JOIN $wpdb->term_relationships AS r ON r.term_taxonomy_id = tt.term_taxonomy_id 
                                    INNER JOIN $wpdb->posts AS p ON p.ID = r.object_id ";
    
                // Restrict by post type and Group by term_id for COUNTing.
                $post_types_str = implode(',',$args['post_types']);
                $pieces['where'].= $wpdb->prepare(" AND p.post_type IN(%s) GROUP BY t.term_id", $post_types_str);
    
                remove_filter( current_filter(), __FUNCTION__ );
                return $pieces;
            }
        } // endif post_types set
    
        return get_terms($taxonomies, $args);           
    }
    

    Usage

    $args =array(
        'hide_empty' => 0,
        'post_types' =>array('country','city'),
    );
    
    $terms = wpse57444_get_terms('flag',$args);
    

    Original work-around

    Inspired from the above trac ticket, (tested, and it works for me)

    function wpse57444_filter_terms_by_cpt($taxonomy, $post_types=array() ){
        global $wpdb;
    
        $post_types=(array) $post_types;
        $key = 'wpse_terms'.md5($taxonomy.serialize($post_types));
        $results = wp_cache_get($key);
    
        if ( false === $results ) {
           $where =" WHERE 1=1";
           if( !empty($post_types) ){
                $post_types_str = implode(',',$post_types);
                $where.= $wpdb->prepare(" AND p.post_type IN(%s)", $post_types_str);
           }
    
           $where .= $wpdb->prepare(" AND tt.taxonomy = %s",$taxonomy);
    
           $query = "
              SELECT t.*, COUNT(*) 
              FROM $wpdb->terms AS t 
              INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id 
              INNER JOIN $wpdb->term_relationships AS r ON r.term_taxonomy_id = tt.term_taxonomy_id 
              INNER JOIN $wpdb->posts AS p ON p.ID = r.object_id 
              $where
              GROUP BY t.term_id";
    
           $results = $wpdb->get_results( $query );
           wp_cache_set( $key, $results );
        }        
    
        return $results;
    }
    

    Usage

     $terms = wpse57444_filter_terms_by_cpt('flag',array('country','city'));
    

    or

     $terms = wpse57444_filter_terms_by_cpt('flag','country');
    
  2. Two custom post types ‘country’ and ‘city’ and a shared taxonomy ‘flag’. You want to limit the list to the post type ‘country’.

    Here is a more simple solution:

    $posts_in_post_type = get_posts( array(
        'fields' => 'ids',
        'post_type' => 'country',
        'posts_per_page' => -1,
    ) );
    $terms = wp_get_object_terms( $posts_in_post_type, 'flag', array( 'ids' ) ); ?>
    
  3. @stephen-harris’s answer above only worked for me partially. If I tried to use it twice on the page, it didn’t work. Also, the idea of burying mysql queries like that worries me – I think its better practice to use core methods to achieve a solution, to avoid conflicts with future WP updates. Here is my solution, based on some comment #7 on on the Trac ticket he reference

    function get_terms_by_custom_post_type( $post_type, $taxonomy ){
      $args = array( 'post_type' => $post_type);
      $loop = new WP_Query( $args );
      $postids = array();
      // build an array of post IDs
      while ( $loop->have_posts() ) : $loop->the_post();
        array_push($postids, get_the_ID());
      endwhile;
      // get taxonomy values based on array of IDs
      $regions = wp_get_object_terms( $postids,  $taxonomy );
      return $regions;
    }
    

    Usage:

    $terms = get_terms_by_custom_post_type('country','flag');
    

    This works for only one post type and one taxonomy, because that is what I needed, but it wouldn’t be too hard to modify this to accept multiple values.

    There was some mention on that Trac thread that this may not scale well, but I’m working on a pretty small scale and have had no problems with speed.

  4. [edit] This is a comment on the excellent answer by Stephen Harris.

    It doesn’t return any terms if used with multiple post types like this $flags = wpse57444_get_terms('flags', array('post_types' => array('country','city')));. This is because $wpdb->prepare sanitizes the $post_types_str string to p.post_type IN('country,city') while it should be p.post_type IN('country','city'). See this ticket: 11102. Use the solution from this topic to get around this: https://stackoverflow.com/a/10634225

  5. I also tried to use @Stephen Harris’s answer, but the query I needed was quite hard to write as a single query and using the filter pieces.

    Furthermore, I also needed to use that function multiple times in the same page and I solved the problem declaring the wpse_filter_terms_by_cpt function outside the wrapper function.

    Anyways @Mark Pruce’s answer in my opinion fits better, for the same reasons he said, even though it needs you to make one more query (and the related loop) to prepare the args for the wp_get_object_terms function.