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