19

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

If I use:

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

  • Could you be a little clearer? – TheDeadMedic Jul 4 '12 at 16:42
16

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');
  • It works, but what can I do with my $args? I mean... parent=0&orderby=name&hide_empty=0 – user1443216 Jul 4 '12 at 19:11
  • no - this has to be an array: $args = array('parent'=>0,'orderby'=>'name','hide_empty'=>0);. I will edit this to allow query strings... – Stephen Harris Jul 4 '12 at 19:44
  • Where can I put my $args in this example: $terms = wpse57444_filter_terms_by_cpt('flag',array('country','city'));? – user1443216 Jul 4 '12 at 19:59
  • You can't in that one, only in the new solution:wpse57444_get_terms() – Stephen Harris Jul 4 '12 at 20:21
  • @user1443216 $args is the second argument. There you just put wpse57444_get_terms( 'flag', array( 'country', 'city' ) ); – kaiser Jul 4 '12 at 20:21
2

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

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' ) ); ?>
1

[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

1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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