Get terms by taxonomy AND post_type

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

Read More

Oh, I’m on WP 3.1.1

Related posts

Leave a Reply

7 comments

  1. 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;
    
    }
    
  2. 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;
        }
    }
    
  3. 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.

  4. 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);
    
  5. 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.

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

  7. 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;
            }