1

I need to list all custom terms (brands) that are associated with products from the category that is currently being viewed. E.g. I have these brands created:

  • Shirt Brand A
  • Shirt Brand B
  • Shirt Brand C
  • Jeans Brand A
  • Jeans Brand B

On 'Shirts' category page, only Shirts Brands A, B and C are displayed.

This is how I did it:

$args = array(
    'taxonomy' => 'brand',
    'orderby' => 'count',
    'order' => 'DESC',
    'hide_empty' => false
);
$brands = get_terms($args);

foreach($brands as $brand) {
    if(is_product_category()) {
        $cat_id = get_queried_object_id();
        $count_args = array(
            'post_type'             => 'product',
            'post_status'           => 'publish',
            'tax_query'             => array(
                array(
                    'taxonomy'      => 'product_cat',
                    'field'         => 'term_id',
                    'terms'         => $cat_id,
                    'operator'      => 'IN'
                ),
                array(
                    'taxonomy'      => 'brand',
                    'field'         => 'slug',
                    'terms'         => $brand->slug,
                    'operator'      => 'IN'
                )
            )
        );
        $count_products = new WP_Query($count_args);
        if($count_products->post_count <= 0) continue; // Skip if this query contains zero products

        // Display brand that has at least 1 product in this category
        display_brand();
    }
}

Of course it works, however with 300+ brands created it sometimes takes 0,5 - 1,5 seconds for this loop to get processed, which is unnecessary lot. AFAIK there is no direct database relation between terms and product categories (only number of posts for each term), but is there a better way to do this with better performance?

3

To fine-tune DevelJoe's answer a bit more, you could access the queried posts from the global $wp_query instead of doing an extra WP_Query.

// make the current query available from global variable
global $wp_query;
// helper variable
$brands = array();
// loop queried posts
foreach ( $wp_query->posts as $queried_post ) {
    // check if they have terms from custom taxonomy
    $current_post_brands = get_the_terms( $queried_post, 'brands' ); // post object accepted here also, not just ID
    if ( ! is_wp_error( $current_post_brands ) && $current_post_brands ) {
        // push found brands to helper variable
        foreach ( $current_post_brands as $current_post_brand ) {
            // avoid having same brands multiple time in the helper variable
            if ( ! isset( $brands[$current_post_brand->term_id] ) ) {
                $brands[$current_post_brand->term_id] = $current_post_brand;
            }
        }
    }
}

// do something with the brand terms (WP_Term)
foreach ( $brands as $brand_id => $brand_term ) {
    // yay?
}

This is probably a faster way, because WP automatically caches the terms of the queried posts - to my undestanding. This caching is dicussed for example here, Explanation of update_post_(meta/term)_cache


EDIT 23.9.2020

(Facepalm) Let me try this again...

One way could be to first query all the posts in the current category. Then loop found posts to check their brand terms. Finally spit out an array of unique brand terms. Optionally you could also save the result into a transient so that the querying and looping doesn't run on every page load, thus saving some server resources.

function helper_get_brands_in_category( int $cat_id ) {
    $transient = get_transient( 'brands_in_category_' . $cat_id );
    if ( $transient ) {
        return $transient;
    }
    
    $args = array(
        'post_type' => 'product',
        'posts_per_page' => 1000, // is this enough?
        'post_status' => 'publish',
        'no_found_rows' => true,
        'update_post_meta_cache' => false, // not needed in this case
        'fields' => 'ids', // not all post data needed here
        'tax_query' => array(
            array(
                'taxonomy' => 'product_cat',
                'field' => 'term_id',
                'terms' => $cat_id,
            )
        ),
    );
    $query = new WP_Query( $args );
    $brands = array();
    
    foreach ($query->posts as $post_id) {
        $post_brands = get_the_terms( $post_id, 'brands' );
        if ( ! is_wp_error( $post_brands ) && $post_brands ) {
            foreach ( $post_brands as $post_brand ) {
                if ( ! isset( $brands[$post_brand->term_id] ) ) {
                    $brands[$post_brand->term_id] = $post_brand;
                }
            }
        }
    }

    set_transient( 'brands_in_category_' . $cat_id, $brands, WEEK_IN_SECONDS ); // change expiration date as needed

    return $brands;
}
// Usage
$brands_in_category = helper_get_brands_in_category( get_queried_object_id() );

If you save the brands into transients, then you may also want to invalidate the transients whenever new brands are added. Something along these lines,

function clear_brands_in_category_transients( $term_id, $tt_id ) {
    $product_cats = get_terms( array(
        'taxonomy' => 'product_cat',
        'hide_empty' => false,
        'fields' => 'ids',
    ) );
    if ( ! is_wp_error( $product_cats ) && $product_cats ) {
        foreach ($product_cats as $cat_id) {
            delete_transient( 'brands_in_category_' . $cat_id );
        }
    }
}
add_action( 'create_brands', 'clear_brands_in_category_transients', 10, 2 );

I used the create_{$taxonomy} hook above.

6
  • Even though DevelJoe came first with the logic, I've marked your answer as a solution just because it is more fine-tuned, in case someone needs it. Thank you both, the performance increased roughly 50 times. – Kristián Filo Sep 22 '20 at 10:41
  • Update: I've noticed the global $wp_query only gets products from the current page, and not all products from the category. That is probably why the performance got so much better. However I tried to create a new WP_Query, similar as DevelJoe did, but it returns 0 results. Is there any way to edit your function to work with all products, and not just current page? – Kristián Filo Sep 23 '20 at 11:01
  • @KristiánFilo Looks like reading comprehension isn't my strongest point :D Yes, that is correct the first example only checks the terms for current query posts. I added another example to my answer, which should handle all products from a category. I didn't test the code, but I think it should do the job. Notice also the use of transients. – Antti Koskinen Sep 23 '20 at 17:11
  • Your solution seems to be working like a charm, thank you very much. The performance is equally good as before when loading the brands from transients, but significantly slower when loading them all over again. I will need to read more about transients and how they work. Are they basically native WC's cache system? Meaning once they get preloaded and stored, everyone else can use them, or are they stored only locally and each visitor must encounter at least one bigger loading time? – Kristián Filo Sep 24 '20 at 14:01
  • @KristiánFilo, Transients are WP native functionality, developer.wordpress.org/apis/handbook/transients. Basically they are just options stored in wp_options with custom key and optional expiration time. They are a good way to "cache" expensive queries into the database. Yes, they are global and are available everywhere when loading them with get_transient(). – Antti Koskinen Sep 24 '20 at 16:06
1

Define your terms as hierarchical custom taxonomies, as child taxonomies from the corresponding parent taxonomy you want them to inherit from...? Like this you would only need to query for your parent tax, and simply echo out the respective child taxonomies, if required.

** UPDATE **

What you do is query all the terms of a given taxonomy, and use them to query posts having them, so you're creating a WP_Query object for every single brand tax, which is unnecessarily expensive. You should do it the other way around; simply query posts which have terms of the brand taxonomy associated to it (and whichever additional feature you may want to use for the query) ONCE only, and while retrieving these posts (inside your wp_query loop), access the id of the currently queried post in the loop, and retrieve the brand terms associated to it. Sth like this:

$product_query_args = array(
  'post_type' => 'your_post_type',
  'tax_query' => array(
    array(
      'taxonomy' => 'brand'
    )
  )
);

$products_request = new WP_Query( $product_query_args );

if ( $products_request->have_posts() ) {
  
  while ( $products_request->have_posts() ) {

    $products_request->the_post();

    // Get ID of currently iterated post
    $id_of_iterated_product = get_the_ID();

    // Retrieve associated brand terms
    $brands = get_the_terms( $id_of_iterated_product, 'brand' );

    // Check if there are brand terms associated to iterated post
    if ( ! empty( $brands ) ) {

      // If so, iterate through them and get the name of each
      foreach( $brands as $brand_key => $brand_object ) {

        $brand_name = $brand_object->name;

        // Do whatever you want with it

      }

    }

  }

}

Like this, you actually only make one full query instead of countless, so performance should be better, hopefully. Let me know how this solution suits you.

To explain it a little further to you why I thought of hierarchical taxonomies; you wrote:

"I need to list all custom terms (brands) that are associated with products from the category that is currently being viewed."

  • From this I understand that there's something like a category filter on your website, and you wanna display all the posts of that category, and then echo out the brands for each of these posts. For this, what you could actually do is simply query posts in function of the selected category (makes your query a lot easier), and then for each post retrieve and spit out the associated brands (which are all child tax's of the queried category, if you defined them to be so). You can however reach the same in querying your products in function of the given category, and then retrieve all the brand tags associated to each retrieved post as I did in my code; maybe that's even better when it comes to performance. But from a logical point of view, using taxonomy hierarchics and one single category query appears to be the easiest and most logical way of doing this.
4
  • Hmm, I don't think I understand what you are saying. Those terms are simply brands, they are not hierarchical and they have no relation between each other whatsoever. Or am i missing something? – Kristián Filo Sep 18 '20 at 8:08
  • 'brand' looks like a custom taxonomy you created, or am I wrong? if so, you can easily define them as being hierarchical, and define their parent tax. Or how did u define the 'brand' taxonomy? We should know that, then I can tell ya how to proceed :) – DevelJoe Sep 18 '20 at 9:52
  • Yes, of course it is a custom taxonomy, registered with register_taxonomy(). I know I can set it to be hierarchical, I just don't understand why would I need my brands to have parent-child relationships in regards of this problem. :) They are specifically designed NOT to be hierarchical (unlike e.g. product categories). – Kristián Filo Sep 18 '20 at 10:20
  • check my update; let me know if this helped ya – DevelJoe Sep 18 '20 at 11:30

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.