24

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?

1
  • Could you be a little clearer? Commented Jul 4, 2012 at 16:42

5 Answers 5

20

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');
10
  • It works, but what can I do with my $args? I mean... parent=0&orderby=name&hide_empty=0 Commented Jul 4, 2012 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... Commented Jul 4, 2012 at 19:44
  • Where can I put my $args in this example: $terms = wpse57444_filter_terms_by_cpt('flag',array('country','city'));? Commented Jul 4, 2012 at 19:59
  • You can't in that one, only in the new solution:wpse57444_get_terms() Commented Jul 4, 2012 at 20:21
  • @user1443216 $args is the second argument. There you just put wpse57444_get_terms( 'flag', array( 'country', 'city' ) );
    – kaiser
    Commented Jul 4, 2012 at 20:21
9

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
  • 2
    This seems to be the recommended way according to this trac ticket: core.trac.wordpress.org/ticket/18106#comment:7 . Do note that this is potentially very slow, so you will likely need to implement some caching if your site has a lot of posts.
    – Jules
    Commented Nov 11, 2020 at 14:25
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.

1
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 and acknowledge you have read our privacy policy.

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