Getting WordPress Term Results that are Relative to a Different Taxonomy

Avatar of Christian Nolen
Christian Nolen on (Updated on )

The following is a guest post by Christian Nolen. Christian details a situation where data is being intelligently and reasonably organized, yet is a little tricky to query for just right. He details a solution that doesn’t go too far into the weeds to get it right.

If you’ve ever done any WordPress development, you’ve likely needed to create custom post types and register taxonomies. Working with taxonomies is straightforward when the relationship is one-to-one, i.e. “Get me all taxonomy terms, use get_terms(), or get all terms associated with the current post, use get_the_terms().”

But, what do you do when you need the returned results to be relative to another taxonomy? Neither of the typical functions help. Before I delve into the solution, let’s take a closer look at the problem. The use case is a portfolio website that offers a variety of services in multiple markets. Our setup is as follows:

  • Custom Post Type: Portfolio
  • Taxonomy: Services
  • Terms: Development, Illustration, Photography, Print Design, Web Design
  • Taxonomy: Markets
  • Terms: Civic, Corporate, Education, Health, Not for Profit

This site requires the use of a taxonomy template for both Services and Markets. When a visitor lands on one of these taxonomy pages, a tab layout showing portfolio items for associated markets (if viewing a service) or associated services (if viewing a market) is displayed.

Screenshot of tab layout with the first tab empty

In the screenshot above, I’m displaying a Photography Service page. I’ve usedget_terms('market') to retrieve the valid markets (assigned to a $markets variable). I iterate through each market to create my tab and then use WP_Query to retrieve all corresponding portfolio items. The tax_query parameter is used to narrow results to the current market and service (defined using get_query_var('term') in this case).

The first thing we notice is an embarrassing empty “Civic” tab that brings focus to the fact that services have not been provided in this market. To boot, it’s the first tab a visitor sees when landing on this page.

The Solution

I spent a significant amount of time researching a resolution. The only solution I found involved querying the database using wpdb. Yes, this was a solution, but it didn’t feel right. I knew there must be a built in way to handle this use case.

After delving deeper into the WordPress Codex, I discovered wp_get_object_terms(). The description of the function is as follows:

Retrieves the terms associated with the given object(s) in the supplied taxonomies.

The example provided is very similar to one you’d find for get_the_terms(). The key is the object parameter can be an array.

The object we want to pass to wp_get_object_terms() is an array of post IDs. The most efficient way I could think of building this array was using get_posts() with the fields parameter set to “ids.” You could use WP_Query, but I opted for get_posts() because:

  1. get_posts() returns an array vs. an object
  2. I’ll never confuse my initial get_posts() call from WP_Query used within the tabbed content.
$service_slug = get_query_var('term');	
$service_post_IDs = get_posts(array(
  'post_type' => 'portfolio',
  'posts_per_page' => -1,
  'tax_query' => array(
    array(
      'taxonomy' => 'service',
      'field' => 'slug',
      'terms' => $service_slug
    )
  ),
  'fields' => 'ids'
));

The above is how we get the right subset of post IDs. As mentioned earlier, I retrieve the current service slug by using get_query_var('term') and assign it to the $service_slug variable. This is used in tax_query to ensure we only get post IDs that are assigned to the current taxonomy term. By setting the fields parameter to ids the result is an indexed array of post IDs.

Now that I have the array of post IDs, I can replace get_terms() with the following:

$markets = wp_get_object_terms($service_post_IDs, 'market');

When I refresh the page, the “Civic” tab is gone and we have a tab populated with the correct content.

Screenshot of how the corrected tab layout - without empty tab Civic

I could stop here. But now the arguments used within WP_Query are bothering me.

$args = array(
  'post_type' => 'portfolio',
  'posts_per_page' => -1,
  'tax_query' => array(
    'relation' => 'AND',
    array(
      'taxonomy' => 'market',
      'field'    => 'slug',
      'terms'    => $m->slug,
    ),
    array(
      'taxonomy' => 'service',
      'field'    => 'slug',
      'terms'    => $service_slug,
    ),
  )
);

Yes, the above works just fine. But I feel like I’m duplicating my efforts in regards to my tax_query. What we can do is refactor the tax_query to include only the current market and leverage the post__in parameter with the array of service post IDs.

$args = array(
  'post_type' => 'portfolio',
  'posts_per_page' => -1,
  'tax_query' => array(
    array(
      'taxonomy' => 'market',
      'field'    => 'slug',
      'terms'    => $m->slug,
    )
  ),
  'post__in' => $service_post_IDs
);

The front-end result is the same, but now the query arguments are slimmer and easier to read. Most importantly, you now have a template that is showing valid information to your visitors.