Alright, it’s time to continue our journey building our awesome WP_Query_Builder class! So far, we’ve only designed it to build simple WordPress queries. But not all WordPress queries are simple or easy to model using just a cascading method.

It’s even possible that you’ve run into the limits of the WP_Query_Builder already. The class that we created then wasn’t a complete solution. It had some serious limitations if you were a WP_Query expert. You couldn’t use it to perform complex WordPress queries.

What do we mean by complex WordPress queries? We mean queries that use a complex WP_Query query parameters. At the moment, there are three of them: date_query , meta_query and tax_query .

So, for this article, we’re going to back to our WP_Query_Builder class. We’ll add support for one of these complex WP_Query query parameters. We won’t go over all three due to how complex they are. But you can apply what you’ve seen for this one query parameter to design a fluent interface for the other two.

The story so far…

But first, let’s do a small recap of what we’ve built so far. In our previous episode (linking a second time for effect), we saw two concepts. These were the fluent interface and the domain-specific language. With them, we built the WP_Query_Builder class that you can see below.

class WP_Query_Builder { /** * The query arguments collected by the query builder. * * @var array */ private $query_arguments; /** * Constructor. * * @param array $query_arguments */ public function __construct(array $query_arguments = array()) { $this->query_arguments = array_merge(array( 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ), $query_arguments); } /** * Specify the post types that the query will retrieve. * * Can be a comma separated string or an array. Overwrites previous * specification criteria if called multiple times. * * @param string|array $from * * @return self */ public function from($from) { if (is_string($from)) { $from = array_map('trim', explode(',', $from)); } elseif (!is_array($from)) { return $this; } $this->query_arguments['post_type'] = $from; return $this; } /** * Query WordPress using the current specifications of the builder. * * @return WP_Post[] */ public function get_results() { $query = new WP_Query($this->query_arguments); return $query->posts; } /** * Specify the maximum number of results that the query will retrieve. * * Overwrites previous specification criteria if called multiple times. * * @param int $limit * * @return self */ public function limit($limit) { if (!is_numeric($limit)) { return $this; } $this->query_arguments['posts_per_page'] = (int) $limit; return $this; } /** * Specify the order of the query results. * * Overwrites previous specification criteria if called multiple times. * * @param string|array $sort * @param string $order * * @return self */ public function order_by($sort, $order = 'DESC') { if (empty($sort) || (!is_array($sort) && !is_string($sort))) { return $this; } elseif (!is_string($order) || !in_array(strtoupper($order), array('ASC', 'DESC'))) { $order = 'DESC'; } $this->query_arguments['orderby'] = $sort; $this->query_arguments['order'] = $order; return $this; } /** * Specify the columns that the query will retrieve. * * Overwrites previous specification criteria if called multiple times. * * @param string $select * * @return self */ public function select($select) { if (empty($select) || !is_string($select)) { return $this; } $this->query_arguments['fields'] = $select; return $this; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 class WP_Query_Builder { /** * The query arguments collected by the query builder. * * @var array */ private $query_arguments ; /** * Constructor. * * @param array $query_arguments */ public function __construct ( array $query_arguments = array ( ) ) { $this -> query_arguments = array_merge ( array ( 'no_found_rows' = > true , 'update_post_meta_cache' = > false , 'update_post_term_cache' = > false , ) , $query_arguments ) ; } /** * Specify the post types that the query will retrieve. * * Can be a comma separated string or an array. Overwrites previous * specification criteria if called multiple times. * * @param string|array $from * * @return self */ public function from ( $from ) { if ( is_string ( $from ) ) { $from = array_map ( 'trim' , explode ( ',' , $from ) ) ; } elseif ( ! is_array ( $from ) ) { return $this ; } $this -> query_arguments [ 'post_type' ] = $from ; return $this ; } /** * Query WordPress using the current specifications of the builder. * * @return WP_Post[] */ public function get_results ( ) { $query = new WP_Query ( $this -> query_arguments ) ; return $query -> posts ; } /** * Specify the maximum number of results that the query will retrieve. * * Overwrites previous specification criteria if called multiple times. * * @param int $limit * * @return self */ public function limit ( $limit ) { if ( ! is_numeric ( $limit ) ) { return $this ; } $this -> query_arguments [ 'posts_per_page' ] = ( int ) $limit ; return $this ; } /** * Specify the order of the query results. * * Overwrites previous specification criteria if called multiple times. * * @param string|array $sort * @param string $order * * @return self */ public function order_by ( $sort , $order = 'DESC' ) { if ( empty ( $sort ) || ( ! is_array ( $sort ) && ! is_string ( $sort ) ) ) { return $this ; } elseif ( ! is_string ( $order ) || ! in_array ( strtoupper ( $order ) , array ( 'ASC' , 'DESC' ) ) ) { $order = 'DESC' ; } $this -> query_arguments [ 'orderby' ] = $sort ; $this -> query_arguments [ 'order' ] = $order ; return $this ; } /** * Specify the columns that the query will retrieve. * * Overwrites previous specification criteria if called multiple times. * * @param string $select * * @return self */ public function select ( $select ) { if ( empty ( $select ) || ! is_string ( $select ) ) { return $this ; } $this -> query_arguments [ 'fields' ] = $select ; return $this ; } }

All our methods except for get_results used a cascading method template. Using it, we created cascading methods for a few WP_Query query parameters. This also gave you the power to create your own cascading methods.

Complex query parameters

That said, we haven’t reached the end of our story yet! As we saw earlier, WP_Query_Builder can’t deal with complex WP_Query query parameters. But why is that?

It’s because these query parameters are more like mini-queries. They’re not just a simple query parameter that you assign a value to. No, they’re arrays with their own unique set of query parameters. You can even nest them to form even more complex queries. (Or query inception!)

This makes it near impossible (or a bad idea at the very least) to model using a simple cascading method. We need more tools for designing fluent interfaces. And we also need to expand our domain-specific language to support them.

Focus on the “tax_query” query parameter

This is also why we won’t see all three complex WP_Query query parameter for this article. Each of them requires in-depth analysis of their inner workings. That way we can create a relevant domain-specific language for them.

This is the hardest part of designing a fluent interface. And we can’t quite fallback on the SQL domain-specific language like we did before. This means that we have a lot more work cut out for us.

So we’re going to pick one complex WP_Query query parameter and go with it. As you might have noticed, the section header already gave away the surprise. (Stupid section headers ruining the fun for everyone!) We’re going to go with the tax_query query parameter. But why did we pick that specific one?

It’s because tax_query is special. You might not know it, but all category and tag query parameters map to it. WP_Query converts these query arguments to elements of tax_query query argument. It then passes tax_query to the WP_Tax_Query class which handles taxonomy query.

The inner workings of the “tax_query” query parameter

Now, let’s dive into the tax_query query parameter. How does it work? We need to understand that before we can begin working on our fluent interface for it.

Taxonomy array

Let’s start with the fundamental question, “What is tax_query ?” Well, it’s an array. (That was easy!) And, inside that array, there are smaller arrays which we’ll call “taxonomy arrays”.

These smaller taxonomy arrays are what we’re most interested in. They’re the main component of the tax_query array. They each describe a section of the taxonomy query that WP_Tax_Query will generate.

array( 'taxonomy' => 'category', 'terms' => array( 1, 4 ), 'field' => 'term_id', 'operator' => 'NOT IN', 'include_children' => false, ); 1 2 3 4 5 6 7 array ( 'taxonomy' = > 'category' , 'terms' = > array ( 1 , 4 ) , 'field' = > 'term_id' , 'operator' = > 'NOT IN' , 'include_children' = > false , ) ;

Above is an example of a taxonomy array with all the possible array keys that you can use. We’ll need our expanded domain-specific language to take them all into account. So let’s look at what they do:

taxonomy is the taxonomy that we want to query. It’s mandatory.

is the taxonomy that we want to query. It’s mandatory. terms is a single or array of terms that you want to query for this taxonomy. A term can be an ID or string depending on the chosen field . It’s also mandatory.

is a single or array of terms that you want to query for this taxonomy. A term can be an ID or string depending on the chosen . It’s also mandatory. field controls which database field the query will use to compare the given terms . The default value is term_id , but you can also use name , slug or term_taxonomy_id .

controls which database field the query will use to compare the given . The default value is , but you can also use , or . operator is the SQL operator that the query will use to test the terms that you pass to it. The default value is IN , but you can also use NOT IN , AND , EXISTS and NOT EXISTS .

is the SQL operator that the query will use to test the that you pass to it. The default value is , but you can also use , , and . include_children tells the query whether to include child taxonomies with hierarchical taxonomies or not. The default value is true .

Grouping and nesting taxonomy arrays

Like we mentioned earlier, tax_query is an array of taxonomy arrays. More often than not, the tax_query array won’t contain just one taxonomy array. It’ll contain several of them. And how you structure these taxonomy arrays inside the larger tax_query array matters.

There are two ways to structure taxonomy arrays inside the tax_query array. These ways of structuring taxonomy arrays are what lets you create complex taxonomy queries. Let’s take a look at them.

Grouping

Grouping is the most common structure that you’ll see with tax_query . It comes down to grouping (thus the name!) taxonomy arrays together inside an array. WP_Tax_Query will combine all these taxonomy arrays based on how they related to one another.

array( 'relation' => 'AND', array( 'taxonomy' => 'category', 'terms' => '1', ), array( 'taxonomy' => 'category', 'field' => 'slug', 'terms' => 'some-category-slug', 'operator' => 'NOT IN', ), ); 1 2 3 4 5 6 7 8 9 10 11 12 13 array ( 'relation' = > 'AND' , array ( 'taxonomy' = > 'category' , 'terms' = > '1' , ) , array ( 'taxonomy' = > 'category' , 'field' = > 'slug' , 'terms' = > 'some-category-slug' , 'operator' = > 'NOT IN' , ) , ) ;

Here’s what taxonomy array grouping looks like in practice. The first thing you might have noticed is the relation array key. This tells WP_Tax_Query how to join all the taxonomy arrays to build the taxonomy query.

relation has two possible values: AND and OR . AND means that the results return by WP_Query must match all taxonomy arrays. Meanwhile, OR means that the query results only need to match one of the taxonomy arrays. If you don’t specify a relation , the default is AND .

Analyzing our grouping example

Now, let’s go back to our earlier taxonomy query example. What does it translate to? Let’s break it down and look at it piece by piece.

First, there’s the relationship between taxonomy arrays. We assigned the value AND to the relation array key. This means that we need to match all given taxonomy arrays.

As for for the taxonomy arrays. The first one says that we want posts that have the category with the term ID of 1 . Meanwhile, the second one is a bit more complicated because of the NOT IN operator. It says that we want posts that don’t have the category with the some-category-slug slug.

Let’s finish this up by combining all the taxonomy query pieces back together. What does it say about the posts what we want WP_Query to retrieve? We want posts assigned to the category with the term ID of 1 . But that aren’t assigned to the category with the some-category-slug slug.

Nesting

The second taxonomy array structure is nesting. Nesting is when you take a group of taxonomy arrays (like the one we just saw) and you nest it inside another group of taxonomy arrays. (This is the taxonomy query inception that we were talking about!) This lets you create even more complex taxonomy queries.

$query = new WP_Query(array( 'tax_query' => array( 'relation' => 'OR', array( 'taxonomy' => 'post_format', 'field' => 'slug', 'terms' => 'some-post-format-slug', ), array( 'relation' => 'AND', array( 'taxonomy' => 'category', 'terms' => '1', ), array( 'taxonomy' => 'category', 'field' => 'slug', 'terms' => 'some-category-slug', 'operator' => 'NOT IN', ), ), ) )); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $query = new WP_Query ( array ( 'tax_query' = > array ( 'relation' = > 'OR' , array ( 'taxonomy' = > 'post_format' , 'field' = > 'slug' , 'terms' = > 'some-post-format-slug' , ) , array ( 'relation' = > 'AND' , array ( 'taxonomy' = > 'category' , 'terms' = > '1' , ) , array ( 'taxonomy' = > 'category' , 'field' = > 'slug' , 'terms' = > 'some-category-slug' , 'operator' = > 'NOT IN' , ) , ) , ) ) ) ;

You break this taxonomy query into two parts represented by two groups of queries. You have the top level one with the 'relation' => 'AND' . And the lower level one with the 'relation' => 'OR' .

The lower level one is the main group of taxonomy arrays. Our query results must meet the requirements one of the two taxonomy arrays in its group. The first one is a regular taxonomy array. The other is our nested group of taxonomy arrays.

Let’s start with the regular taxonomy array. It focuses on the post_format taxonomy. It says that a valid post must have post_format with the some-post-format-slug slug.

Otherwise, we have to go to our nested group of taxonomy arrays. This is the same group that we used earlier for the grouping example. It wanted posts assigned to the category with the term ID of 1 , but not the category with the some-category-slug slug.

So that’s what’s going on with our taxonomy query. It’ll return posts that match the singular post_format taxonomy array. Or it’ll return posts that match our group of category taxonomy arrays.

Back to WP_Query_Builder

So that covers the tax_query parameter! As you can see, there’s a good reason why we call it a complex query parameter! Using what we’ve seen, we’re going to create a fluent interface for the tax_query . We’ll add it to our WP_Query_Builder class.

Taxonomy array methods

If we go back to what we saw earlier, what’s the fundamental component of tax_query ? Well, it’s the taxonomy array. So, let’s start by creating a method that can generate taxonomy arrays for us.

class WP_Query_Builder { // ... /** * Generates a taxonomy array. * * @param string $taxonomy * @param array|string $terms * @param string $field * @param string $operator * @param bool $include_children * * @return array */ public function taxonomy($taxonomy, $terms, $field = 'term_id', $operator = 'IN', $include_children = true) { if (!is_array($terms)) { $terms = array($terms); } return array( 'taxonomy' => $taxonomy, 'field' => $field, 'terms' => $terms, 'operator' => $operator, 'include_children' => $include_children, ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class WP_Query_Builder { // ... /** * Generates a taxonomy array. * * @param string $taxonomy * @param array|string $terms * @param string $field * @param string $operator * @param bool $include_children * * @return array */ public function taxonomy ( $taxonomy , $terms , $field = 'term_id' , $operator = 'IN' , $include_children = true ) { if ( ! is_array ( $terms ) ) { $terms = array ( $terms ) ; } return array ( 'taxonomy' = > $taxonomy , 'field' = > $field , 'terms' = > $terms , 'operator' = > $operator , 'include_children' = > $include_children , ) ; } }

The purpose of the taxonomy method is to generate a taxonomy array. And, like the taxonomy array, it’s the fundamental component of our fluent interface. We want it to be as generic as possible. That way, you can use it to create any taxonomy array that you need.

The method has five parameters to match the five array keys in a taxonomy array. taxonomy and terms are mandatory parameters. Meanwhile, field , operator and include_children are optional.

The only special thing about the method is the is_array check at the start. We use it to validate the value in the terms variable. We want to ensure that it’s always an array. This is an optional step, but it lets us standardize the taxonomy arrays that we generate.

Creating more taxonomy array methods

There’s another reason why we made the taxonomy method so generic. It’s because we’re going to use it as the foundation for other fluent interface methods. These methods are going to force some of the arguments passed to the taxonomy method.

The goal of these methods is to expand our domain-specific language. If we use the taxonomy method all the time, it makes our query less readable. This goes against one of the main purposes of the WP_Query_Builder class.

Instead, we want to use different methods with more specific method names. That way you can know part of what’s in the taxonomy array from the method name. This, in turn, improves the readability of our query. This is the same as what WP_Query is doing with its category and tag query parameters.

Specific taxonomy methods

An easy way to improve readability is to create taxonomy array methods for each taxonomy. This clarifies what taxonomy array WP_Query_Builder is generating. Here’s how you’d do it for the category taxonomy:

class WP_Query_Builder { // ... /** * Generates a taxonomy array for a category. * * @param array|string $categories * @param string $field * @param string $operator * @param bool $include_children * * @return array */ public function category($categories, $field = 'term_id', $operator = 'IN', $include_children = true) { return $this->taxonomy('category', $categories, $field, $operator, $include_children); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class WP_Query_Builder { // ... /** * Generates a taxonomy array for a category. * * @param array|string $categories * @param string $field * @param string $operator * @param bool $include_children * * @return array */ public function category ( $categories , $field = 'term_id' , $operator = 'IN' , $include_children = true ) { return $this -> taxonomy ( 'category' , $categories , $field , $operator , $include_children ) ; } }

Our category method is pretty simple. We took out the taxonomy parameter for earlier. Instead, it always passes category as the taxonomy argument to the taxonomy method.

The four category method parameters are the same as the remaining taxonomy method parameters. We pass them as is to the taxonomy method. The only small difference is that we renamed the terms parameter to categories . This ensures that the language of the category method is consistent.

Now, we only did this with the category taxonomy. But it’s not too hard to extend this to any taxonomy that you need. You just need to change the taxonomy argument that you pass to the taxonomy method.

Specific operator methods

We can also do the same thing that we just did with the operator parameter. We can create methods with names that clarify which operator is in the taxonomy array. We can do this for both the generic taxonomy method and our specific taxonomy methods.

class WP_Query_Builder { // ... /** * Generates a taxonomy array where posts don't have * one of the given taxonomy terms. * * @param string $taxonomy * @param array|string $terms * @param string $field * @param bool $include_children * * @return array */ public function not_in_taxonomy($taxonomy, $terms, $field = 'term_id', $include_children = true) { return $this->taxonomy($taxonomy, $terms, $field, 'NOT IN', $include_children); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class WP_Query_Builder { // ... /** * Generates a taxonomy array where posts don't have * one of the given taxonomy terms. * * @param string $taxonomy * @param array|string $terms * @param string $field * @param bool $include_children * * @return array */ public function not_in_taxonomy ( $taxonomy , $terms , $field = 'term_id' , $include_children = true ) { return $this -> taxonomy ( $taxonomy , $terms , $field , 'NOT IN' , $include_children ) ; } }

Let’s start with the generic taxonomy method. The not_in_taxonomy method also has one less parameter like the earlier category method. But this time, we removed that operator parameter. And instead, the method passes NOT IN as the operator argument.

class WP_Query_Builder { // ... /** * Generates a taxonomy array where posts don't have * one of the given categories. * * @param array|string $categories * @param string $field * @param bool $include_children * * @return array */ public function in_category($categories, $field = 'term_id', $include_children = true) { return $this->not_in_taxonomy('category', $categories, $field, $include_children); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class WP_Query_Builder { // ... /** * Generates a taxonomy array where posts don't have * one of the given categories. * * @param array|string $categories * @param string $field * @param bool $include_children * * @return array */ public function in_category ( $categories , $field = 'term_id' , $include_children = true ) { return $this -> not_in_taxonomy ( 'category' , $categories , $field , $include_children ) ; } }

We can also push this further and create specific operator methods for specific taxonomies. This is what we did with the not_in_category method above. We used our new not_in_taxonomy method and forced category as the taxonomy argument.

You could also use the category method and force NOT IN as the operator argument. This is just a matter of personal preference. Use whatever makes more sense to you!

You might have noticed something about our generic taxonomy methods. They’re also specific operator methods if you keep the default value for operator . In that scenario, using the taxonomy method is the same as using the in_taxonomy method. This is something that we’ll use throughout the article.

Grouping and nesting methods

Now that we’ve handled how to generate a taxonomy array, we’ll look at how we can group them together! To do this, we’ll need to create some two methods to handle that. One for each possible relation value.

class WP_Query_Builder { // ... /** * Generates a group of query arrays using the given query arrays. The query * result must match all the given query arrays. * * @param array ...$query_arrays * * @return array */ public function and_where() { $query_arrays = func_get_args(); $query_arrays['relation'] = 'AND'; return $query_arrays; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class WP_Query_Builder { // ... /** * Generates a group of query arrays using the given query arrays. The query * result must match all the given query arrays. * * @param array ...$query_arrays * * @return array */ public function and_where ( ) { $query_arrays = func_get_args ( ) ; $query_arrays [ 'relation' ] = 'AND' ; return $query_arrays ; } }

The and_where method above is the one that generates groups with AND as the relation . While the method is small, there are quite a bit to talk about. It wasn’t as straightforward to design as it looks.

Choosing the right method name

This might sound silly, but the hardest part of designing this method was finding a good name for it! Naming our fluent interface method well is important. We need them to make sense within the context of our domain-specific language.

The problem with this method name is that it overlaps with our specific operator methods. The taxonomy array has AND as an operator . So we can’t use and_taxonomy since we need that name for the taxonomy array method.

Instead, we need to go back to our SQL domain-specific language that we’ve been using. What happens to the MySQL query generated by WP_Tax_Query when you use grouping? Well, WP_Tax_Query adds another condition to the the WHERE clause of the MySQL query.

Knowing this, we could name our method and_where_taxonomy . But is that necessary? There’s nothing taxonomy related in this method.

In fact, grouping works the same for all complex queries. You always have an array of query arrays. And within that array, there’s always a relation array key that tells the class how to group them.

This means that we won’t need different methods for each type of complex query. We can just remove _taxonomy from the method name. And this is how we ended up with and_where as the method name.

Variadic function

If we look at the PHPDoc of the and_where method, there’s something unusual going on there. There’s a @param array ...$query_arrays in the PHPDoc, but no parameters in the method definition. What’s up with that?

Well, it’s because and_where isn’t a normal method. It’s a variadic function. This is a type function (or method!) that can accept a variable number of arguments.

We need that because we can pass any number of query arrays to and_where . That’s also why there are no parameters in the method definition. Instead, we use the func_get_args to get the arguments passed to and_where .

func_get_args returns an array with all the arguments that and_where received. We then assign that array to the query_arrays variable. That’s why we can view query_arrays as a parameter in this situation.

Grouping taxonomy arrays

So how does the and_where method work in practice? Well, let’s convert the grouping query that we had earlier so that it uses our fluent interface. You can find the result below:

$query_builder->and_where( $query_builder->category('1'), $query_builder->not_in_category('some-category-slug', 'slug') ); 1 2 3 4 $query_builder -> and_where ( $query_builder -> category ( '1' ) , $query_builder -> not_in_category ( 'some-category-slug' , 'slug' ) ) ;

You’ll notice that we use the two taxonomy array methods that we designed earlier. We pass them to the and_where method as arguments. And the method will assign them to the query_arrays variable using the func_get_args function.

This is where the variadic function aspect of the and_where method comes into play. You won’t always pass the same number of taxonomy arrays to it. It needs to be able to handle that scenario.

At this point, query_arrays only contains taxonomy arrays that we passed as arguments. But we still need to add a relation to the array. That’s the only other thing that the and_method method does.

array( array( 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => array('1'), 'operator' => 'IN', 'include_children' => true, ), array( 'taxonomy' => 'category', 'field' => 'slug', 'terms' => array('some-category-slug'), 'operator' => 'NOT IN', 'include_children' => true, ), 'relation' => 'AND', ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 array ( array ( 'taxonomy' = > 'category' , 'field' = > 'term_id' , 'terms' = > array ( '1' ) , 'operator' = > 'IN' , 'include_children' = > true , ) , array ( 'taxonomy' = > 'category' , 'field' = > 'slug' , 'terms' = > array ( 'some-category-slug' ) , 'operator' = > 'NOT IN' , 'include_children' = > true , ) , 'relation' = > 'AND' , ) ;

You can see the array that our and_where method generates above. It’s a bit more verbose than the one from our earlier example. That’s just because our taxonomy array methods generate complete taxonomy arrays. They contain all the default values which we didn’t put in the array earlier.

Nesting taxonomy arrays

Now, that we’ve looked at grouping, let’s move on to nesting taxonomy arrays. The good news is that we don’t need to do anything else to support nesting. (That was easy!) So let’s look at what our nesting example from example looks like converted to a fluent interface.

$query_builder->or_where( $query_builder->taxonomy('post_format', 'some-post-format-slug', 'slug'), $query_builder->and_where( $query_builder->category('1'), $query_builder->not_in_category('some-category-slug', 'slug') ) ); 1 2 3 4 5 6 7 $query_builder -> or_where ( $query_builder -> taxonomy ( 'post_format' , 'some-post-format-slug' , 'slug' ) , $query_builder -> and_where ( $query_builder -> category ( '1' ) , $query_builder -> not_in_category ( 'some-category-slug' , 'slug' ) ) ) ;

The first thing that jumps out is the or_where method. We haven’t shown any code for it so far. Let’s fix that!

class WP_Query_Builder { // ... /** * Generates a group of query arrays using the given query arrays. The query * result must match one of the given query arrays. * * @param array ...$query_arrays * * @return array */ public function or_where() { $query_arrays = func_get_args(); $query_arrays['relation'] = 'OR'; return $query_arrays; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class WP_Query_Builder { // ... /** * Generates a group of query arrays using the given query arrays. The query * result must match one of the given query arrays. * * @param array ...$query_arrays * * @return array */ public function or_where ( ) { $query_arrays = func_get_args ( ) ; $query_arrays [ 'relation' ] = 'OR' ; return $query_arrays ; } }

There’s only a small difference between the and_where and or_where methods. It’s the value that gets added to the relation key of the query_arrays array. and_where assigns AND to relation while or_where assigns OR to it.

Going back to the nesting example above, we pass two arguments to the or_where method. The first one is the array generated by our generic taxonomy method. The second one is the array generated by the and_where method that we went over earlier.

And that’s all that you need to do to nest taxonomy arrays! This is all possible because we designed our two methods as variadic methods. The result is that it’s easy and intuitive to group and nest taxonomy arrays.

Passing our taxonomy arrays to tax_query

So far, we haven’t been interacting with WP_Query at all. We’ve only designed methods that generated taxonomy arrays for the tax_query query parameter. We still need a method to assign these taxonomy arrays to it.

class WP_Query_Builder { // ... /** * Specify the taxonomy of the posts that the query will retrieve. * * @param array $taxonomy_query * * @return self */ public function where_taxonomy(array $taxonomy_query) { $this->query_arguments['tax_query'] = $taxonomy_query; return $this; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class WP_Query_Builder { // ... /** * Specify the taxonomy of the posts that the query will retrieve. * * @param array $taxonomy_query * * @return self */ public function where_taxonomy ( array $taxonomy_query ) { $this -> query_arguments [ 'tax_query' ] = $taxonomy_query ; return $this ; } }

And we’re back to designing the cascading methods that you know and love! The where_taxonomy cascading method is quite straightforward. It follows the same format that we used throughout the previous article.

We have the taxonomy_query parameter which the method maps to the tax_query query parameter. Unlike our other cascading methods, there’s no guard clause in our where_taxonomy method. That’s because we’re using type hinting to ensure that taxonomy_query is always an array.

The choice of where_taxonomy for our method name goes back to our earlier discussion. We mentioned that tax_query behaved like a WHERE clause in SQL. We didn’t name it where because the other complex query parameters also behave that way.

Improving things

As is, our where_taxonomy method isn’t that smart. You can’t use it without using either the and_where or or_where methods as well. This means that your query will always look something like this:

$query_builder = new WP_Query_Builder(); $posts = $query_builder->select('*') ->from('post') ->where_taxonomy( $query_builder->and_where( $query_builder->in_taxonomy('category', '5')), $query_builder->not_in_category('some-category-slug', 'slug') ) ) ->limit(3) ->get_results(); 1 2 3 4 5 6 7 8 9 10 11 $query_builder = new WP_Query_Builder ( ) ; $posts = $query_builder -> select ( '*' ) -> from ( 'post' ) -> where_taxonomy ( $query_builder -> and_where ( $query_builder -> in_taxonomy ( 'category' , '5' ) ) , $query_builder -> not_in_category ( 'some-category-slug' , 'slug' ) ) ) -> limit ( 3 ) -> get_results ( ) ;

This is pretty lame! We shouldn’t have to use either those methods if we’re not using nesting. Instead, we should be able to do something like this as well:

$posts = $query_builder->select('*') ->from('post') ->where_taxonomy( $query_builder->category('5'), $query_builder->not_in_category('some-category-slug', 'slug') ) ->limit(3) ->get_results(); 1 2 3 4 5 6 7 8 $posts = $query_builder -> select ( '*' ) -> from ( 'post' ) -> where_taxonomy ( $query_builder -> category ( '5' ) , $query_builder -> not_in_category ( 'some-category-slug' , 'slug' ) ) -> limit ( 3 ) -> get_results ( ) ;

This is much more intuitive and easy to read. And it’s much more common scenario too. Most taxonomy queries don’t use nesting.

Back to the drawing board

So let’s go back to our where_taxonomy method and fix this problem! We need to redesign our where_taxonomy method so that it can handle both scenarios. To achieve that, we’ll have to go back to our friend func_get_args .

Our initial version of where_taxonomy assumed that taxonomy_query was always a valid taxonomy array. But this won’t be the case if we redesign it as a variadic method. You might get a valid taxonomy query array or just a bunch of taxonomy arrays.

So let’s make this the new goal of our where_taxonomy method. It should be able to convert any argument that it receives into a valid taxonomy array. This is also a great opportunity to use some of PHP’s array functions!

Why is this a good opportunity?

This is a good question and it’s worth elaborating on it. First, let’s think about what defines a variadic method. It’s the array of arguments that we get from func_get_args .

Next, what are you trying to do with it? You’re trying to convert that array of taxonomy arrays into a single taxonomy array. And then, you want to assign that array to the tax_query query parameter.

Well, there’s a PHP array function who’s job is to do exactly that! It’s the array_reduce function. It lets us transform an array into a single value using a callable.

class WP_Query_Builder { // ... /** * Specify the taxonomy of the posts that the query will retrieve. * * @param array ...$taxonomies * * @return self */ public function where_taxonomy() { $this->query_arguments['tax_query'] = array_reduce(func_get_args(), array($this, 'merge_query_argument')); return $this; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class WP_Query_Builder { // ... /** * Specify the taxonomy of the posts that the query will retrieve. * * @param array ...$taxonomies * * @return self */ public function where_taxonomy ( ) { $this -> query_arguments [ 'tax_query' ] = array_reduce ( func_get_args ( ) , array ( $this , 'merge_query_argument' ) ) ; return $this ; } }

Above is the new version of our where_taxonomy method that uses array_reduce . We pass it the array of arguments from func_get_args as our array. The callable is the merge_query_argument method that we’ll cover next.

array_reduce will pass each argument to the merge_query_argument method. Its job will be to merge that argument into the query array that we’re building. And this query array is what we’ll assign to the tax_query query parameter.

A generic callable

You might have noticed that we haven’t used the term “taxonomy” for anything related to our callable. Instead, we’ve been using the term “query” to define the job of merge_query_argument . This is an intentional choice of language.

The work that merge_query_argument has to do isn’t specific to taxonomy queries. In fact, you can use it with any of the complex WP_Query query parameters. This will be easier to understand once you see how it works.

class WP_Query_Builder { // ... /** * Merges the given query array argument into the given query. * * @param mixed $query * @param array $query_argument * * @return array */ private function merge_query_argument($query, array $query_argument) { if (!is_array($query)) { $query = array(); } if (!isset($query_argument['relation'])) { $query_argument = array($query_argument); $query_argument['relation'] = 'AND'; } $query['relation'] = $query_argument['relation']; unset($query_argument['relation']); foreach ($query_argument as $query_array) { $query[] = $query_array; } return $query; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class WP_Query_Builder { // ... /** * Merges the given query array argument into the given query. * * @param mixed $query * @param array $query_argument * * @return array */ private function merge_query_argument ( $query , array $query_argument ) { if ( ! is_array ( $query ) ) { $query = array ( ) ; } if ( ! isset ( $query_argument [ 'relation' ] ) ) { $query_argument = array ( $query_argument ) ; $query_argument [ 'relation' ] = 'AND' ; } $query [ 'relation' ] = $query_argument [ 'relation' ] ; unset ( $query_argument [ 'relation' ] ) ; foreach ( $query_argument as $query_array ) { $query [ ] = $query_array ; } return $query ; } }

So here’s what the merge_query_argument method looks like. It’s not the easiest method to understand. That’s why we’ll go through it in more detail than usual. (Is that even possible!?)

Method parameters

Let’s start by looking at the parameters of our merge_query_argument method. The first one is query . It’s the carry that the array_reduce function passes to our method each time it calls it. query always contains the current query array that we’ve generated up to this point.

When it’ll first call convert_argument , array_reduce will pass null as the argument for query . That’s why there’s a guard clause checking if it’s an array at the beginning of the method. We need to always ensure that query is an array.

The second parameter is query_argument . This is one of the query arguments that func_get_args returned in the where_taxonomy method. This is what we’re trying to merge into our query .

Managing the relation between arguments

The trickiest part of the merge_query_argument method is how it manages the relation array key. That’s because query_argument can be a group of query arguments or a single one. We need to analyze it to figure it out.

This is what the second merge_query_argument guard clause does. It checks if query_argument has a relation array key. If it does, we don’t need to anything. We’re dealing with the output of either out and_where method or our or_where method.

But if we don’t, we’re dealing with a taxonomy array. (This is just for this example. It could also be a date or meta array if you were building a query for those.) We need to reformat it so that it’s like a group of arrays.

To do that, we need to add query_argument inside an another array. We then assign it a value for the relation array key. By default, that value is AND so that what we’ll use as well.

Merging the relation

Now that we’ve standardized our query_argument with a relation array key, what’s next? Well, we need to assign the relation value to query . So that’s what we do after our second guard clause.

It’s worth noting that we’re doing this in a way that we overwrite the relation value in query each time. This is a debatable design choice. And, in practice, it shouldn’t matter whether you do it this way or not. That’s because there isn’t a scenario where overwriting that value will cause a problem.

If you’re dealing with the output of and_where or or_where , there’s only one argument. Which means that you won’t enter the merge_query_argument method a second time. So there’s no way to overwrite the relation value.

But, with the other scenario, query_argument will always contain a taxonomy array (in this example). And, as we discussed earlier, these taxonomy arrays will never come with a relation value. So they will always have the value AND assigned to them. Which means that it doesn’t matter if you overwrite it each time.

Once we’re done with the relation array key, we want to remove it from the query_argument array. We do this using the unset function. The goal of this step is to simplify the logic in the last part of the method.

Merging the query argument

At this point, the only thing left to do is merge query_argument into query . (After all, that’s what we say the method does!) We do this by looping through all the query arrays inside query_argument .

The loop itself just appends each query_array to our query . We’re able to do this because we removed the relation array key earlier. Otherwise, we’d need to check to see if query_array was the relation each time.

Everything that you need

So this wraps up this second look at the WP_Query_Builder class! You now have a lot of the tools available to you. With them, you can customize your own version of WP_Query_Builder to your needs. After all, we’ve only covered a fraction of what WP_Query can do.

That said, we can do a lot with what we have right now. Let’s expand our earlier nesting example with some extra WP_Query query parameters. We’ll add some simple ones from our first article.

$query = new WP_Query(array( 'fields' => 'ids', 'post_type' => 'post', 'tax_query' => array( 'relation' => 'OR', array( 'taxonomy' => 'post_format', 'field' => 'slug', 'terms' => 'some-post-format-slug', ), array( 'relation' => 'AND', array( 'taxonomy' => 'category', 'terms' => '1', ), array( 'taxonomy' => 'category', 'field' => 'slug', 'terms' => 'some-category-slug', 'operator' => 'NOT IN', ), ), ), 'posts_per_page' => 3, )); $posts = $query->posts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 $query = new WP_Query ( array ( 'fields' = > 'ids' , 'post_type' = > 'post' , 'tax_query' = > array ( 'relation' = > 'OR' , array ( 'taxonomy' = > 'post_format' , 'field' = > 'slug' , 'terms' = > 'some-post-format-slug' , ) , array ( 'relation' = > 'AND' , array ( 'taxonomy' = > 'category' , 'terms' = > '1' , ) , array ( 'taxonomy' = > 'category' , 'field' = > 'slug' , 'terms' = > 'some-category-slug' , 'operator' = > 'NOT IN' , ) , ) , ) , 'posts_per_page' = > 3 , ) ) ; $posts = $query -> posts

Here’s the same query with our WP_Query_Builder class:

$posts = $query_builder->select('ids') ->from('post') ->where_taxonomy( $query_builder->or_where( $query_builder->taxonomy('post_format', 'some-post-format-slug', 'slug'), $query_builder->and_where( $query_builder->category('1'), $query_builder->not_in_category('some-category-slug', 'slug') ) ) ) ->limit(3) ->get_results(); 1 2 3 4 5 6 7 8 9 10 11 12 13 $posts = $query_builder -> select ( 'ids' ) -> from ( 'post' ) -> where_taxonomy ( $query_builder -> or_where ( $query_builder -> taxonomy ( 'post_format' , 'some-post-format-slug' , 'slug' ) , $query_builder -> and_where ( $query_builder -> category ( '1' ) , $query_builder -> not_in_category ( 'some-category-slug' , 'slug' ) ) ) ) -> limit ( 3 ) -> get_results ( ) ;

For some of you, the initial example using WP_Query might still be easier to read. That’s ok! You can keep on doing what you were doing before.

But for some of us (myself included), this second example is the one that’s easiest the two. A lot of it has to do with the SQL domain-specific language that we’ve been using. It transforms something that we were less familiar with ( WP_Query parameters) into something that we are more (SQL).

And that’s all there is to it. It’s not better or worse. It’s just different.

You can find the complete code for the WP_Query_Builder class here.