18

I have 2 custom post types 'bookmarks' and 'snippets' and a shared taxonomy 'tag'. I can generate a list of all terms in the taxonomy with get_terms(), but I can't figure out how to limit the list to the post type. What I'm basically looking for is something like this:

get_terms(array('taxonomy' => 'tag', 'post_type' => 'snippet'));

Is there a way to achieve this? Ideas are greatly appreciated!!

Oh, I'm on WP 3.1.1

7 Answers 7

12

Here is another way to do something similar, with one SQL query:

static public function get_terms_by_post_type( $taxonomies, $post_types ) {

    global $wpdb;

    $query = $wpdb->prepare(
        "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 p.post_type IN('%s') AND tt.taxonomy IN('%s')
        GROUP BY t.term_id",
        join( "', '", $post_types ),
        join( "', '", $taxonomies )
    );

    $results = $wpdb->get_results( $query );

    return $results;

}
3
  • Yes! This does exactly what I want. Commented Nov 7, 2011 at 1:33
  • print_r(get_terms_by_post_type(array('category') , array('event') )); shows Warning: Missing argument 2 for wpdb::prepare() Commented Jun 20, 2014 at 2:20
  • I could be wrong, but off the top of my head, I don't think those 'join' statements will work - i.e., they'd only work if passed single-value arrays. This is because the prepare function would escape all the generated single-quotes, and consider each whole 'join' one string.
    – Codesmith
    Commented Mar 12, 2019 at 17:19
14

So it just happens that I needed something like that for a project I'm working on. I simply wrote a query to select all posts of a custom type, then I check what are the actual terms of my taxonomy they are using.

Then I got all terms of that taxonomy using get_terms() and then I only used those that were in both of the lists, wrapped it up in a function and I was done.

But then I needed more then just the ID's: I needed the names so I added a new argument named $fields so I could tell the function what to return. Then I figured that get_terms accepts many arguments and my function was limited to simply terms that are being used by a post type so I added one more if statement and there you go:

The Function:

/* get terms limited to post type 
 @ $taxonomies - (string|array) (required) The taxonomies to retrieve terms from. 
 @ $args  -  (string|array) all Possible Arguments of get_terms http://codex.wordpress.org/Function_Reference/get_terms
 @ $post_type - (string|array) of post types to limit the terms to
 @ $fields - (string) What to return (default all) accepts ID,name,all,get_terms. 
 if you want to use get_terms arguments then $fields must be set to 'get_terms'
*/
function get_terms_by_post_type($taxonomies,$args,$post_type,$fields = 'all'){
    $args = array(
        'post_type' => (array)$post_type,
        'posts_per_page' => -1
    );
    $the_query = new WP_Query( $args );
    $terms = array();
    while ($the_query->have_posts()){
        $the_query->the_post();
        $curent_terms = wp_get_object_terms( $post->ID, $taxonomy);
        foreach ($curent_terms as $t){
          //avoid duplicates
            if (!in_array($t,$terms)){
                $terms[] = $c;
            }
        }
    }
    wp_reset_query();
    //return array of term objects
    if ($fields == "all")
        return $terms;
    //return array of term ID's
    if ($fields == "ID"){
        foreach ($terms as $t){
            $re[] = $t->term_id;
        }
        return $re;
    }
    //return array of term names
    if ($fields == "name"){
        foreach ($terms as $t){
            $re[] = $t->name;
        }
        return $re;
    }
    // get terms with get_terms arguments
    if ($fields == "get_terms"){
        $terms2 = get_terms( $taxonomies, $args );
        foreach ($terms as $t){
            if (in_array($t,$terms2)){
                $re[] = $t;
            }
        }
        return $re;
    }
}

Usage:

If you only need a list of term id's then:

$terms = get_terms_by_post_type('tag','','snippet','ID');

If you only need a list of term names then:

$terms = get_terms_by_post_type('tag','','snippet','name');

If you only need a list of term objects then:

$terms = get_terms_by_post_type('tag','','snippet');

And if you need to use extra arguments of get_terms like: orderby, order, hierarchical ...

$args = array('orderby' => 'count', 'order' => 'DESC',  'hide_empty' => 1);
$terms = get_terms_by_post_type('tag',$args,'snippet','get_terms');

Enjoy!

Update:

To fix the term count to specific post type change:

foreach ($current_terms as $t){
          //avoid duplicates
            if (!in_array($t,$terms)){
                $terms[] = $t;
            }
        }

to:

foreach ($current_terms as $t){
    //avoid duplicates
    if (!in_array($t,$terms)){
        $t->count = 1;
        $terms[] = $t;
    }else{
        $key = array_search($t, $terms);
        $terms[$key]->count = $terms[$key]->count + 1;
    }
}
5
  • wouldn't it be better if you use (array) $args instead of a list of 4 $vars? This would allow you to not care about order you throw in the arguments, so something like get_terms_by_post_type( $args = array( 'taxonomies', 'args', 'post_type', 'fields' => 'all') ) and then call them inside the function with $args['taxonomies']. This would help you staying away from adding empty values and having to remember the order of your arguments. I'd also suggest to use single quotes instead of double. I saw them beeing up to five times faster.
    – kaiser
    Commented Apr 9, 2011 at 10:48
  • 1
    @kaiser - Double quoted strings have to be parsed, where as single quoted values are always treated as literal. When you're using variables in a string it makes sense and it's perfectly fine to use double quotes, but for non-variable string values single quotes are more ideal(because they won't need to be parsed) and slightly faster(we're talking about milliseconds in most cases).
    – t31os
    Commented Apr 9, 2011 at 11:35
  • @t31os - Absolutely correct. I still prefer 'this is my mood: '.$value over "this is my mood: $value", because of readability. When it comes to speed: It's not slightly - i meassured up to five times. And when you use double quotes in your whole theme everywhere, they'll quickly sum up when you got a lot of requests. Anyway good you made that clear.
    – kaiser
    Commented Apr 9, 2011 at 12:25
  • @t31os Out of a discussion I re-meassured the speed of " vs. ' and I was wrong. The difference is far outside anything anybody would notice.
    – kaiser
    Commented Jul 24, 2011 at 15:24
  • 1
    +1 nice function! 2 typos: $taxonomies is used in the function $taxonomy and $terms[] = $c; has to be $terms[] = $t; Commented Feb 5, 2012 at 17:45
13

Great question and solid answers.

I really liked the approach by @jessica using the terms_clauses filter, because it extends the get_terms function in a very reasonable way.

My code is a continuation of her idea, with some sql from @braydon to reduce duplicates. It also allows for an array of post_types:

/**
 * my_terms_clauses
 *
 * filter the terms clauses
 *
 * @param $clauses array
 * @param $taxonomy string
 * @param $args array
 * @return array
 **/
function my_terms_clauses($clauses, $taxonomy, $args)
{
  global $wpdb;

  if ($args['post_types'])
  {
    $post_types = implode("','", array_map('esc_sql', (array) $args['post_types']));

    // allow for arrays
    if ( is_array($args['post_types']) ) {
      $post_types = implode( "','", $args['post_types'] );
    }
    $clauses['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";
    $clauses['where'] .= " AND p.post_type IN ('". $post_types. "') GROUP BY t.term_id";
  }
  return $clauses;
}
add_filter('terms_clauses', 'my_terms_clauses', 99999, 3);

Because get_terms doesn't have a clause for GROUPY BY, I had to add it to the end of the WHERE clause. Notice that I have the filter priority set very-high, in hopes it will always go last.

3
  • 1
    You should replace $post_types = $args['post_types']; by $post_types = implode("','", array_map('esc_sql', (array) $args['post_types'])); and remove esc_sql() from IN clause or this clause ends up with \' between post_types when providing multiple post types in $args['post_types'] Commented Sep 28, 2020 at 7:58
  • 1
    Only one that let me use get_terms and worked for me in 2022
    – LBF
    Commented Oct 27, 2022 at 21:03
  • Will the hide_empty argument still be applied to all posts, not just the selected post type?
    – Adrian B
    Commented Oct 3, 2023 at 21:43
10

I wrote a function that allows you to pass post_type in the $args array to the get_terms() function:

HT to @braydon for writing the SQL.

 /**
 * terms_clauses
 *
 * filter the terms clauses
 *
 * @param $clauses array
 * @param $taxonomy string
 * @param $args array
 * @return array
**/
function terms_clauses($clauses, $taxonomy, $args)
{
    global $wpdb;

    if ($args['post_type'])
    {
        $clauses['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";
        $clauses['where'] .= " AND p.post_type='{$args['post_type']}'"; 
    }
    return $clauses;
}
add_filter('terms_clauses', 'terms_clauses', 10, 3);
3

I was unable to make the get_terms arguments to work with Gavin's version of the code above, but finally did by changing

$terms2 = get_terms( $taxonomy );

to

$terms2 = get_terms( $taxonomy, $args );

as it was in the original function from Bainternet.

1
  • 1
    Fixed it in the current version Commented Apr 10, 2011 at 11:06
0

@Bainternet: Thanks! I had to alter the function slightly because it wasn't working (some typos). The only problem now is that the term count is off. The count isn't taking the post type into consideration so I don't think you can use get_terms() in this.

function get_terms_by_post_type($post_type,$taxonomy,$fields='all',$args){
    $q_args = array(
        'post_type' => (array)$post_type,
        'posts_per_page' => -1
    );
    $the_query = new WP_Query( $q_args );

    $terms = array();

    while ($the_query->have_posts()) { $the_query->the_post();

        global $post;

        $current_terms = get_the_terms( $post->ID, $taxonomy);

        foreach ($current_terms as $t){
            //avoid duplicates
            if (!in_array($t,$terms)){
                $t->count = 1;
                $terms[] = $t;
            }else{
                $key = array_search($t, $terms);
                $terms[$key]->count = $terms[$key]->count + 1;
            }
        }
    }
    wp_reset_query();

    //return array of term objects
    if ($fields == "all")
        return $terms;
    //return array of term ID's
    if ($fields == "ID"){
        foreach ($terms as $t){
            $re[] = $t->term_id;
        }
        return $re;
    }
    //return array of term names
    if ($fields == "name"){
        foreach ($terms as $t){
            $re[] = $t->name;
        }
        return $re;
    }
    // get terms with get_terms arguments
    if ($fields == "get_terms"){
        $terms2 = get_terms( $taxonomy, $args );

        foreach ($terms as $t){
            if (in_array($t,$terms2)){
                $re[] = $t;
            }
        }
        return $re;
    }
}

EDIT: Added the fix(es). But somehow it's still not working for me. The count still shows the incorrect value.

5
  • That's a different story, but you can count when avoiding duplicates in the while loop.
    – Bainternet
    Commented Apr 9, 2011 at 13:34
  • I updated my answer with a term count fix.
    – Bainternet
    Commented Apr 9, 2011 at 14:25
  • 1
    Please don't add follow-ups as answers, unless you are specifically answering your own question, additions should instead be made to the original question.
    – t31os
    Commented Apr 10, 2011 at 10:06
  • 1
    @t31os: Ah yes, I was wondering how to add an addition. Didn't think of editing my question. Thanks! Commented Apr 10, 2011 at 10:58
  • How can I call this? print_r(get_terms_by_post_typea(array('event','category','',array())); this one gives Warning: Invalid argument supplied for foreach() for the line foreach ($current_terms as $t){ Commented Jun 20, 2014 at 2:32
0

Avoid Duplicates:

//avoid duplicates
    $mivalor=$t->term_id;
    $arr=array_filter($terms, function ($item) use ($mivalor) {return isset($item->term_id) && $item->term_id == $mivalor;});

    if (empty($arr)){
    $t->count=1;
            $terms[] = $t;
        }else{
            $key = array_search($t, $terms);
            $terms[$key]->count = $terms[$key]->count + 1;
        }
1
  • 1
    Can you explain why this solves the problem? See How to Answer.
    – brasofilo
    Commented Feb 26, 2013 at 17:38

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