Want to learn object-oriented programmming? Get my free course

Designing a class to build simple WordPress queries

In a previous article, we looked at how you could create a class to manage WordPress posts. We ended creating a class who’s job it was to interact with the WordPress database. And it did that quite well!

As part of its job, our class also had to be able to query the WordPress database. To do that, we made it easier to reuse code around the WP_Query class. We achieved that by creating methods that used predefined query arguments.

But have you ever looked at the codex page for WP_Query? Holy Moley, there are a lot of query parameters in there! It’s pretty intimidating and not always easy to use in practice.

That’s the problem that we’re going to look at in this article. We’ll design a class to simplify how we build WP_Query objects. It’ll handle all the complexity around WP_Query query parameters for you. The result should be an easier way for you for you to create WordPress queries.

How do you make WordPress queries easier?

Before we proceed any further, we should take a moment to answer this question. Now, we know that the job of the WP_Query class is to do just that. It lets you query the database without the need for you to know any SQL. You just need to fill an array with query arguments and pass it to the WP_Query object. It acts as a database abstraction layer.

This is good because not every WordPress developer knows SQL. And it’s a lot easier to remember a bunch of query arguments than to learn it. Instead, WordPress just uses those query arguments and crafts the MySQL query for you.

This also eliminates potential security holes. If you don’t know SQL that well, you can introduce security holes by mistake. But, if you let WordPress take care of it, you don’t have to worry about it. (Nice!)

That said, the problem with query arguments is that they don’t scale well. Your array of query arguments can grow quite large with a dozen or more arguments. It becomes hard to read and know what you’re asking your query to do.

Improving WP_Query readability

That’s where our new class comes in. Its primary job will be to improve readability around WP_Query. And the way that we’ll do it is by creating a fluent interface for building WP_Query objects.

What is a fluent interface?

A fluent interface is a type object-oriented API. It’s not an interface in the sense of an object-oriented interface. (Yes, it’s a bit confusing, but it’s because API stands for application programming interface.) Martin Fowler and Eric Evans coined the expression for it in 2005.

The job of a fluent interface is to make your code more readable. It achieves that by using specific concepts and techniques that we’ll look at a bit later. First, we’ll go through a small example so that you have a better idea of what a fluent interface looks like.

WP_Query vs WP_Query_Builder

// WP_Query example
$query = new WP_Query(array(
    'fields' => 'ids',
    'post_type' => 'page',
    'orderby' => 'ID',
    'posts_per_page' => 5,
));
$posts = $query->get_posts();

// WP_Query_Builder example
$query_builder = new WP_Query_Builder();
$posts = $query_builder->select('ids')
                       ->from('page')
                       ->order_by('ID')
                       ->limit(5)
                       ->get_results();

Both queries shown above are trying to get the IDs of the 5 most recent pages ordered by ID. The first one is our standard WP_Query object with an array of query arguments. The other is the same query generated by our WP_Query_Builder class which uses a fluent interface.

As you can see, both queries don’t read the same at all even if they generate the same result. It’s also possible that you find the first one easier to read. And that’s ok! We’ll look at why that is next.

The elements of a fluent interface

As an object-oriented API, the fluent interface doesn’t quite fit the mold of a software design pattern. It’s not quite a solution to a programming problem. That said, it does have some defining characteristics.

Method Cascading

There’s a good chance that you noticed the excessive use of method calls in the previous example. This is the most identifiable characteristic of a fluent interface. We call it method cascading.

Method cascading is a more specific type of method chaining. Method chaining is a type of syntax where you … chain method calls together. (Of course! Duh!) A good example of the use of method chaining is jQuery which depends on it a lot.

The goal of method chaining is to remove the need to store intermediate values. Each method returns an object which you can then call another method from. You can keep doing this as long as you want.

Now, by default, method chaining has a serious problem. It’s that each method can return a different object. If we look back at our previous example, you might assume that you’re always dealing with the same WP_Query_Builder object. But method chaining doesn’t guarantee that at all. (This violates the law of Demeter, but we won’t go into it today.)

This is the problem that method cascading addresses. When you use method cascading, your methods always return the current object. This means that you never have to worry about the type of object that a method is returning. You’re always dealing with the same object that you started the method chain with. In this case, a WP_Query_Builder object.

Domain-specific language

The other defining characteristic of a fluent interface is that it always uses a domain-specific language. A domain-specific language (or DSL) is type of computer language. The goal of a domain-specific language is to give you the tools to solve a problem in a specific domain. Meanwhile, the underlying programming language figures out how to do it.

Domain-specific languages are often the best at what they do that. You use them every day without knowing that you’re using one. For example, HTML, CSS and SQL are all domain-specific languages.

Domain-specific languages are also better at articulating and describing the problem that they solve. This tends to make them easier to read than regular programming languages. It’s also why Martin Fowler and Eric Evans chose the term fluent to describe the interface. It acknowledges the importance of fluency when designing a fluent interface.

But this is also the hardest part of designing of a fluent interface. That’s because naming things is hard. But it’s even harder here where the language is an integral part of the solution. If you create or use a language that’s confusing, you end up making things harder and not easier.

Reusing the SQL domain-specific language

This is why we’re not going to create a domain-specific language for our WP_Query_Builder class. Instead, we’re going to reuse a well-known domain-specific language: SQL. (You might have even noticed it in the example earlier.)

Why are we choosing to use SQL as our domain-specific language? Wasn’t the goal of WP_Query to remove the need for us to know SQL? Why go back to it now? These are all good valid questions!

So like we saw at the beginning, the primary goal of the fluent interface is to provide more readable code. And, when you’re starting off with WordPress or programming, SQL might be hard to read. But it won’t stay that way forever.

That’s because SQL is everywhere in the programming world. There’ll come a point when you’ll have to start using it. And over time, it’ll become something that’s easy to read.

And this is who we’re going to design our fluent API for. We’re not going to design it for someone who doesn’t know SQL that well. The WP_Query query parameters do a good job of helping them already.

Instead, we’ll focus on those who find that limit makes more sense than posts_per_page. They’re the developers that the WP_Query query arguments don’t serve as well. They’ll get the most out of our new fluent API.

Building our query builder

class WP_Query_Builder
{
}

Now that we’ve covered the theory around the WP_Query_Builder class, let’s start building it. We’ll start with an empty class and fill it with our domain-specific language methods from our initial example. These were select, from, order_by, limit and get_results.

Constructor

But before we do all that, we can’t forget about our friend the __construct method! What do we know about our WP_Query_Builder class? What does it need to work?

Well, we know that it’s an abstraction on top of WP_Query and that it works using query arguments. That means that we need an internal variable to store these query arguments. So let’s start with that!

class WP_Query_Builder
{
    /**
     * The query arguments collected by the query builder.
     *
     * @var array
     */
    private $query_arguments;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->query_arguments = array()
    }
}

Here’s our first pass at the __constructor method. We started by creating a query_arguments internal variable to store all our query arguments. We then initialize it inside the __construct method with an empty array.

But we can still do a bit more here. We could set some good default query arguments. These will help us improve the default performance of the WP_Query queries generated by our WP_Query_Builder class.

class WP_Query_Builder
{
    /**
     * The query arguments collected by the query builder.
     *
     * @var array
     */
    private $query_arguments;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->query_arguments = array(
            'no_found_rows' => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
        );
    }
}

We added three default query arguments: no_found_rows, update_post_meta_cache and update_post_term_cache. By default, a WP_Query object query will ask WordPress to perform five database queries. Each of these three query arguments removes one of those five database queries.

But it’s a bit shortsighted to assume that we’ll never want to change these default query arguments. For example, you might need to create a query where you want to update one or both of those caches. So let’s update our __construct method so that you’re able to do that.

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

We started by adding the query_arguments parameter to the __construct method. It uses an empty array as its default value. We then changed how we initialized the query_arguments internal variable. Instead of just assigning it an array, we now call array_merge and assign the resulting array to it.

The order that we pass the arguments to array_merge is important. array_merge processes arrays from left to right. That means that we want our array with our defaults to be first. That way, the query_arguments array that we passed as an argument can overwrite those defaults.

Cascading methods

With the constructor out of the way, we can focus on the rest of the methods in our WP_Query_Builder class. Those are our cascading methods. These cascading methods will serve as implementations of our domain-specific language.

The cascading methods in this article are going to stay quite simple. By that, we mean that they won’t have a lot of inherent complexity. All that they’ll do is map arguments to query parameters. There won’t be that much logic besides that.

Because all our simple methods are so similar, we’ll start by creating a template method for them. Each simple method will make small changes to this template method. Understanding them will come in handy when you’ll want to create a fluent interface of your own.

Template method for method cascading

So, like we just mentioned earlier, all our simple methods follow a similar pattern. Well, almost all of them. The only exception is get_results which we’ll cover at the end. Using this pattern, we can create a template method that we’ll use for our cascading methods.

class WP_Query_Builder
{
    // ...

    /**
     * A method that implements method cascading.
     *
     * @param string $argument
     *
     * @return self
     */
    public function cascading_method($argument)
    {
        if (empty($argument)) {
            return $this;
        }

        // Do something with $argument.

       $this->query_arguments['query_parameter'] = $argument;

        return $this;
    }
}

Above is our template of a cascading method. We named it cascading_method. It has a single string parameter called argument. (This is just for this example. Your method doesn’t have to have a parameter and it can also have more than one.)

The most important feature of a cascading method is return $this;. This is what tells our method to return the current object. And you must ensure that our method always returns the current object. Otherwise, method cascading won’t work.

With that in mind, our cascading method should also always start with a guard clause. That’s the if statement at the beginning of the method. For cascading_method, the guard clause only checks if argument is empty or not.

In practice, you might want your guard clause to do more than just check if argument is empty or not. We want the guard clause is to check for all possible invalid argument values. That way, if argument is invalid, we return the current object right away. This ensures that our method always returns the current object and never breaks the cascade.

Once passed our guard clause, cascading_method might do something with argument. It will then assign it to a query parameter inside our query_arguments internal array. It then finishes by returning the current object.

select method

class WP_Query_Builder
{
    // ...

    /**
     * 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;
    }
}

The first method that we’ll look at is the select method. As you can see, it’s almost the same as our template method. The only thing that we added was a is_string check to our guard clause for the select variable.

The purpose of the select method is to let you specify which columns we want our generated WP_Query object to return. That said, it’s not even close to being as powerful as its SQL equivalent.

This due to limitations with the fields parameter that it maps to. By default, it’ll tell WP_Query to return an array of WP_Post objects containing everything. It’s like if you used * in SQL.

Otherwise, it has two other options: ids and id=>parent. ids returns an array with all the post IDs. id=>parent returns stdClass objects with the ID and post_parent database columns.

But that’s it. We can’t specify which database columns we want WP_Query to return to us. This makes this method a bit underwhelming in practice.

from method

The next method that we’ll look at is the from method. It lets us control the post types that the query will return. It’s a bit more complicated than the select method that we saw earlier. But that’s because of the fancier guard clause.

class WP_Query_Builder
{
    // ...

   /**
     * 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;
    }
}

If you read the PHPDoc of the from method (you read PHPDoc right!?), it mentions that you can pass it a comma delimited string. The problem is that the post_type parameter that it maps to doesn’t. It only accepts a string with a single post type or an array of post types.

So why did we decide to do that then? Well, there’s no specific reason. (Carl!!!) It’s just a nice-to-have feature and it lets us see a more complex guard clauses.

And, speaking of guard clause, how does it deal with a comma delimited string? First, it needs to check if from is a string using is_string. If it’s a string, we convert it into an array using the explode function.

We then pass that array through the array_map function. This is an advanced PHP array function that lets us apply changes to every element of an array using a callback. In the code above, the callback is the trim function. The goal is to remove any whitespace around our array element so that we’re only left with the post type.

The second guard clause checks if from is an array using is_array. If it’s not an array, we return the current object. Once passed the guard clauses, we’re sure that from is an array of post types. We can assign it to the post_type query parameter without worrying about it.

order_by method

After the from method, let’s look at the order_by method. It maps to orderby and order which are the two ordering query parameters. These let you control the order of the query results.

class WP_Query_Builder
{
    // ...

    /**
     * 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;
    }
}

This method a bit different from the other ones because it has two parameters: sort and order. Why did we decide to group these two query parameters? It’s because of their close relationship.

order is only taken into consideration when orderby is a string. If you’re using an array, WP_Query won’t use it. Instead, it expects it to be part of the array passed to orderby.

Because of the dual nature of orderby, the guard clause is a bit more complex. You want to start by checking if sort is empty or not. Once you know that it isn’t empty, you want to check that if it’s an array or a string. To check that, you need both is_array and is_string to be false.

The second guard clause is for the order parameter. It checks if order is a string and if that string is either ASC or DESC. But, if those two checks fail, it doesn’t return the current object. Instead, it changes the value of order back to its default value of DESC.

This is different from the guard clauses that we’ve seen so far. This goes back to the link between the orderby and order query parameters. We always need a valid sort value for orderby. But order can be invalid if sort is valid. We just reset it back to its default value when that happens.

limit method

The limit method is the last and simplest method that we’ll see. It maps to the posts_per_page query parameter. It controls how many results our query will return.

class WP_Query_Builder
{
    // ...

    /**
     * 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;
    }
}

The interesting thing that is worth mentioning is the use of is_numeric in the guard clause. It’s the only PHP function that will validate a numeric string and not just a number type. That’s why we use it to check if limit is either a number or a numeric string.

But this creates an another problem. The issue is that posts_per_page wants an integer and is_numeric cannot guarantee that. It’ll return true for other number types. That’s why we cast limit as an integer before assigning it to the posts_per_page query parameter.

get_results method

So far, we’ve built a few cascading methods for WP_Query_Builder. They highlighted the use of our template cascading method and how to make changes to it. And, using these methods, it’s possible to create a good variety of WordPress queries.

But there’s still one thing that we haven’t looked at. It’s how to convert query arguments inside WP_Query_Builder into WP_Post objects. That’ll be the job of the get_results method below.

class WP_Query_Builder
{
    // ...

    /**
     * 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;
    }
}

As you might have noticed, get_result doesn’t look at all like the method that we’ve seen up until now. It doesn’t use a guard clause. In fact, it doesn’t even return an object, but an array of WP_Post objects.

This is good because you can’t use method chaining with an array. This means that we’re still following method cascading. WP_Query_Builder still only returns the current object when it returns an object.

The goal with a method like get_results is to act as a termination point for the method chaining. You can’t chain anymore because you asked for the results back. (Which makes sense!)

In the case of get_results, the method itself isn’t too complicated. We take the internal query_arguments array that we’ve been building using method cascading. We pass it as the argument for the WP_Query constructor.

WP_Query will parse the given query arguments and turn them into a MySQL query. It’ll then execute that query and convert the results into WP_Post objects. It finishes by storing these objects in the posts variable. That’s why we return the posts variable.

Keeping things simple for now

At this point, we’ve only looked at what we’ll call simple cascading methods. They had one or more guard clauses, a bit of logic and returned the current object. They also mapped to a query parameter in a straightforward manner.

This isn’t a bad thing! There’s no way that we could look at every WP_Query query parameter. (There’s so many!) But, because our methods follow a specific pattern, we were able to create a template method for them. And, using it, you can fill in the gaps in our domain-specific language yourself.

That said, there are query parameters that don’t fit the mold that we’ve seen today. They need a more elaborate domain-specific language. You also need more advanced fluent interface building techniques to implement them. But this is out of scope for this article. (Sorry!)

For now, you can find the complete WP_Query_Builder class 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;
    }
}
Creative Commons License