Hierarchical Taxonomy Query Example

The Problem

There are many examples on performing queries in WordPress using taxonomies provided by WordPress and developers.  What is lacking is dealing with the situations where the taxonomy is hierarchical and children terms have the same name.  WordPress allows this so long as the child terms have different parents and one of those parents has a different name.  If it is allowed, it will happen.

So how do you query by a hierarchical taxonomy term when multiple terms can have the same name?  The simple taxonomy queries can yield wrong results, returning posts that match the other child terms instead of the one that you are interested in.  A query by name or slug is not guaranteed to return the correct results.  Only a query using the term’s ID is guaranteed to return the correct results.

Who wants to determine the ID of a term when what is being displayed is usually the term name?

A Solution

My solution was to use a modification of the term name that allows one or more parent to be included.  The format that I chose was the same format used for file systems.  The format uses parent term names as directories with the child term name as the file.  For example:

  • book/fantasy/author
  • book/science fiction/author
  • book/steampunk/author


In order to define these children terms as unique only the first parent is necessary.  For more complicated taxonomies, multiple levels of parents are also allowed.  In the cases where the child term name is unique, no parents would be required.

The next section shows the code that I used to implement this method of defining taxonomy terms.  The first part of the file defines the taxonomy that is used in the rest of the code.  The example begins at line 90 with the function do_query($term_name).  The code converts the term name into an array of term names, finds the child term name in the taxonomy and then follows the parent ids to verify the parent names.  Once a match is made, the query can proceed using the child term ID instead of its name, thus guaranteeing the returned results will be correct.

Source Code

A link to the source code is available after the code box below.

/** * Querying by hierarchical taxonomy when children terms have the same * name but have different parents. * * Copyright 2015 Steven F. LeBrun (email: steven@lebruns.com) * using the GNU General Public License * http://www.gnu.org/licenses/gpl-2.0.html */ // Defining Text Domain for Internationalization. define( 'TEXT_DOMAIN', 'my_topic' ); // Class defining constants class Topic { const TAX_NAME = 'topic'; const SLUG = 'topic'; } /** * Action Function that registers our Topic taxonomy. * * This taxonomy is associated with one custom post type that we shall * call my_post_type that is not defined here. * * The key part of this registration function is that the Topic taxonomy * is hierarchical. */ function topic_create() { // Define Labels to be used for different aspects of displaying // This taxonomy. $labels = array( 'name' => _x('Topics', 'plural name', TEXT_DOMAIN), 'single_name' => _x('Topic', 'single name', TEXT_DOMAIN), ); // Setting up arguments for registering taxonomy $args = array( 'labels' => $labels, 'public' => true, 'description' => __('Topic', TEXT_DOMAIN), 'hierarchical' => true, 'rewrite' => array( 'slug' => Topic::SLUG, 'hierarchical' => true, ), ); // List all the Custom Post Types that use this taxonomy. $types = array( 'my_post_type'); // Register the Taxonomy register_taxonomy( Topic::TAX_NAME, $types, $args ); // All Done return; } // end of topic_create() // Add function as INIT Hook add_action( 'init', 'topic_create'); /** * Function that builds a query of the custom post type 'my_post_type' * querying for all posts with the specified Topic taxonomy term are * selected. * * @param $term_name The name of the Topic Taxonomy term to query by. * * @return Returns the WP_Query object based on the Topic Taxonomy query. * * The $term_name may be a child term that is not unique by itself. Combine * the child term with its parent terms does result in a unique name. * Therefore, we will allow the $term to take the form of: * * '.../grandparent/parent/child' * * Where the number of parents or ancestors required are what ever is * sufficient to define a unique string. In other words, the term does * not have to be an absolute path. */ function do_query( $term_name ) { // Post Type part of Query arguments $query_args = array( 'post_type' => 'my_post_type', 'orderby' => 'title', 'order' => 'ASC', ); // Add the Taxonomy Query argument to the rest of the query arguments. $query_args['tax_query'] = build_taxonomy_query( $term_name ); // Run query $query = new WP_Query( $query_args ); return $query; } // end of do_query() /** * Function to build the taxonomy portion of a query argument array. * * @param $term_name The Taxonomy term to query by. The term may includes * one or more parent terms separated by slashes. * * @return Returns an array that can be added to the full query argument * array using the key 'tax_query'. */ function build_taxonomy_query( $term_name ) { // Obtain an array of all the taxonomy terms indexed by ID // Note, we do this here so that it is done once during the query // to minimize database access. $tax_terms = get_all_terms_by_id( Topic::TAX_NAME ); // Build the taxonomy query array. $topic_args = array( 'taxonomy' => Topic::TAX_NAME, 'field' => 'id', 'terms' => get_term_id_by_name( $term_name, $tax_terms ), ); $tax_args = array( $topic_args ); return $tax_args; } // end of build_taxonomy_query() /** * Function that fetches all the terms in a taxonomy. * * @param $tax_name The name of the taxonomy. * * @return Returns an array of taxonomy term objects indexed by the * taxonomy term ids. */ function get_all_terms_by_id( $tax_name ) { // Remember that the taxonomy is hierarchical $all_terms = get_terms( $tax_name, array( 'hierarchical' => '1', // true 'fields' => 'all', ) ); $terms = array(); // Build array indexed by Term ID. foreach ( $all_terms as $term ) { $terms[$term->term_id] = $term; } return $terms; } // get_all_terms_by_id() /** * Function that obtains the term id of taxonomy term that may be * a child term. * * @param $term The Taxonomy term to query by. The term may includes * one or more parent terms separated by slashes. * * @param $tax_terms An array of taxonomy terms as produced by * get_all_terms_by_id() * * The term is considered to be a match for $term_name if the term name * matches a taxonomy term and, if present, all the parent terms match * the parents in the taxonomy of that term. */ function get_term_id_by_name( $term_name, $tax_terms ) { // Break up the string into an array and then reverse the order // of the array so that the child term is first, followed by its // parent, followed by its grandparent, ... $parent_names = array_reverse(explode('/', $term_term_name)); // Get child term, leaving the array with just the parents $parent_name = array_shift( $parent_names ); $nelements = count( $tax_terms ); foreach( $tax_terms as $term_id => $term_object ) { if ( $parent_name == $term_object->name ) { if ( do_parents_match( $term_object, $parent_names, $tax_terms) ) { // We have a match return $term_object->term_id; } } } // If we reached this point, no match was found return 0; } // end of get_term_id_by_name() /** * Recursive function used to verify that the parent terms all match * the parents of the taxonomy term. * * @param $child The taxonomy object for the child term. * * @param $parents An array of the names of the parents, starting with the * closest parent. * * @param $tax_terms An array of taxonomy terms as produced by * get_all_terms_by_id() * * Each call to this function is called with the first element in the * $terms array shifted out. Resulting in the $terms array is one * element shorter with each recursive call. If all the parents * match, we end up calling this function with an empty $terms array. */ function do_parents_match( $child, $parent_names, $tax_terms ) { // If $parent_names is an empty array, than all the previous parents // matched, we are done and the term matches child and all its // parents. if ( count($parent_names) == 0 ) { // All parents listed match return true; } // If $term has no parent, and we still have parent terms, // Or if the parent ID does not exist in the taxonomy, // we also have no match. if ( $child->parent == 0 || !array_key_exists( $child->parent, $tax_terms ) ) { // No Match. return false; } $parent_name = array_shift($parent_names); if ( $parent_name == $tax_terms[$child->parent]->name ) { // We have a match, go deeper. return do_parents_match( $tax_terms[$term->parent], $parent_names, $tax_terms ); } // If we reached this point, no match return false; } // end of do_parents_match()

Source code is available at Hierarchical Taxonomy Query.php

Comments are closed