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
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.
In the screenshot above, I'm displaying a Photography Service page. I've used
get_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.
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:
get_posts()returns an array vs. an object
- I'll never confuse my initial
WP_Queryused 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.
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.