# Designing a class to create complex WordPress queries

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](https://carlalexander.ca/designing-class-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`](https://carlalexander.ca/wordpress-adventurous-wp-query-class/) 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`](https://codex.wordpress.org/Class_Reference/WP_Query#Date_Parameters), [`meta_query`](https://codex.wordpress.org/Class_Reference/WP_Query#Custom_Field_Parameters) and [`tax_query`](https://codex.wordpress.org/Class_Reference/WP_Query#Taxonomy_Parameters). 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](https://carlalexander.ca/designing-class-simple-wordpress-queries/) (linking a second time for effect), we saw two concepts. These were the [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) and the [domain-specific language](https://en.wikipedia.org/wiki/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;
    }
}
```

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](https://codex.wordpress.org/Class_Reference/WP_Query#Category_Parameters) and [tag](https://codex.wordpress.org/Class_Reference/WP_Query#Tag_Parameters) 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,
);
```

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.
- `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.
- `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`.
- `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`.
- `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',
    ),
);
```

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',
            ),
        ),
    )
));
```

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,
        );
    }
}
```

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`](https://secure.php.net/manual/en/function.is-array.php) 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);
    }
}
```

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);
    }
}
```

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);
    }
}
```

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;
    }
}
```

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](https://carlalexander.ca/importance-naming-programming/). 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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/Variadic_function). This is a type function (or method!) that can accept a [variable number of arguments](https://secure.php.net/manual/en/functions.arguments.php#functions.variable-arg-list). 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`](https://secure.php.net/manual/en/function.func-get-args.php) 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')
);
```

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',
);
```

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')
   )
);
```

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;
    }
}
```

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;
    }
}
```

And we're back to designing the [cascading methods](https://en.wikipedia.org/wiki/Method_cascading) 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](https://secure.php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration) 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();
```

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();
```

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`](https://secure.php.net/manual/en/function.array-reduce.php) function. It lets us transform an array into a single value using a [callable](https://secure.php.net/manual/en/language.types.callable.php). ```
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;
    }
}
```

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`](https://secure.php.net/manual/en/function.unset.php) 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
```

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();
```

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](https://gist.github.com/carlalexander/66bfb197dd55be218083a351e6e4f080).