When building a plugin, it’s not uncommon to need to fetch posts from the database. After all, you might be using your own custom post type. Or maybe, you built a special functionality on top of posts and you want to find them that way. Those are just a few reasons why your plugin might need to query WordPress for posts.
Regardless of the reason, the standard way of doing that is to use the WP_Query
class. This lets us create WordPress queries in a safe way outside the loop. The problem is that they’re not easy to reuse.
For example, let’s say that you want a query to fetch only one result. You’re always going to need to add the 'posts_per_page' => 1
and 'no_found_rows' => true
query arguments. That last one removes the SQL_CALC_FOUND_ROWS
query used by WordPress pagination. (You don’t need to paginate one result!) This lets us improve performance a bit by removing the unnecessary query.
Now, you’d need to copy these two query arguments whenever you create a query to fetch a single result. This is far from ideal. So let’s look at designing a solution to this problem. Using object-oriented programming, of course!
The job
But first, let’s take a step back for a moment and think about the problem. The problem is that it’s hard to reuse code around WP_Query
. We’re stuck having to copy query arguments whenever we want to make a new one.
You could say the same thing about inserting, updating and deleting posts. These are all somewhat generic operations that we use a bit everywhere. What we need is a class in charge of all this.
That’ll be the job of the class that we design. It’ll manage everything around posts and the WordPress database. Lucky for us, there’s already an object type that does this. We call it the repository.
Enter the repository
A repository is a type of object that acts as an intermediary. It controls the communication between your code and data storage. For WordPress, that means between your plugin or theme and the WordPress database.
The graph above illustrates that relationship. The plugin or theme talks to our repository. The repository then talks to the WordPress database and relays the message back.
This might seem excessive when you could just create queries. The goal of the repository is to simplify all that. It does that by behaving like a collection.
What does that mean? It means that the repository works a lot like an array of WP_Post
objects. This replaces the need for you to interact with the WordPress database using queries. Instead, you use array-like methods supplied by the repository.
The result is that you have an easier time managing posts. You can just use the repository to add, find, update and remove them. It then handles all the necessary interactions with the WordPress database for you.
Our first repository
class Repository { }
Let’s start building our first repository. Right now, the Repository
class is empty. We want to fill it with some basic array-like operations around posts. We’ll need to create the add, find, update and remove post methods like we mentioned earlier.
Adding a post
We’ll begin by looking at how we can add a post to our repository. Let’s create an add
method that does that.
class Repository { /** * Add a post to the repository. Returns the post ID or a WP_Error. * * @param array $post * * @return int|WP_Error */ public function add(array $post) { return wp_insert_post($post, true); } }
In its current state, the add
method is just a wrapper around wp_insert_post
. It takes an array as its only argument. post
is the post data array that we’ll pass to wp_insert_post
.
We also pass the true
as the second argument of wp_insert_post
. This tells wp_insert_post
to return an instance of WP_Error
when there’s an error. By default, it would return 0
. This small change allows our repository to send better feedback when there’s an error.
Finding a post by ID
Searching a repository for a post is the most open-ended task that a repository can do. There are countless ways you might want to look for posts. You might want to find all the posts for a given author. Or maybe it’s all the posts on a given date.
For now, we’ll look at a simple case. We’ll search the repository for a post using a given ID. We’ll name it find_by_id
. It’ll return the WP_Post
object for that given ID or null
.
class Repository { // ... /** * Find a post using the given post ID. * * @param int $id * * @return WP_Post|null */ public function find_by_id($id) { $query = new WP_Query(array( 'p' => $id, 'posts_per_page' => 1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, )); $posts = $query->get_posts(); return !empty($posts[0]) ? $posts[0] : null; } }
As you can see, find_by_id
has only one argument. It’s the ID of the post that we want. It passes that argument to a new instance of WP_Query
as the p
query argument. This is what tells WP_Query
to fetch the post with that ID.
When you create an instance of WP_Query
like we just did, you don’t get the results of your query back. You need to perform another step and call get_posts
. This will return the array of WP_Post
objects.
Once you have the array, you want to check that the query returned a post. You can do that by checking that there’s a value at the zero index of the array. If it’s not empty, we return the WP_Post
object at index zero. Otherwise, we return null
.
We have to do this to prevent a warning from PHP. It doesn’t like it when you try to access a value at an index when the array is empty.
Removing a post
class Repository { // ... /** * Remove the given post from the repository. * * @param WP_Post $post * @param bool $force */ public function remove(WP_Post $post, $force = false) { wp_delete_post($post->ID, $force); } }
Like our add
method, our remove
method is just a wrapper around wp_delete_post
. It takes two arguments: post
and force
. post
is the WP_Post
object to remove. force
decides whether we’re hard deleting or soft deleting (trashing) the post.
One design decision worth discussing is why we’re using WP_Post
as an argument. Why not just pass it the ID of the post we want to delete? This goes back to the job description of the Repository
class.
When you use the Repository
class, you’re not dealing with the database anymore. You’re working with a collection of WP_Post
objects. And if you want to remove one from the collection, you have to give it the object to remove.
It’s not your job to know that the post ID is what Repository
needs to delete the post from the database. That’s an implementation detail that we leave to Repository
. All that you should care about is that you gave it a WP_Post
object and now it’s gone.
But should it delete it for good? That varies from project to project. That’s why we’ll keep the force
argument in this example. If you never want to trash your posts, you could take it out and just replace it with true
. But it’s a design decision that you’ll have to make for yourself.
Updating a post
Our last task is to create a method to update an existing post. We’ll call it update
.
class Repository { // ... /** * Update a post in the repository. Returns the post ID or a WP_Error. * * @param array $post * * @return int|WP_Error */ public function update(array $post) { return wp_update_post($post, true); } }
update
is also a wrapper method. It’s pretty much identical to our save
method. It takes the same post data array. The only difference is the function that it wraps. Instead of wrapping the wp_insert_post
function, it wraps wp_update_post
.
What we have right now
Here’s our current Repository
class with our four methods:
class Repository { /** * Add a post to the repository. Returns the post ID or a WP_Error. * * @param array $post * * @return int|WP_Error */ public function add(array $post) { return wp_insert_post($post, true); } /** * Find a post using the given post ID. * * @param int $id * * @return WP_Post|null */ public function find_by_id($id) { $query = new WP_Query(array( 'p' => $id, 'posts_per_page' => 1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, )); $posts = $query->get_posts(); return !empty($posts[0]) ? $posts[0] : null; } /** * Remove the given post from the repository. * * @param WP_Post $post * @param bool $force */ public function remove(WP_Post $post, $force = false) { wp_delete_post($post->ID, $force); } /** * Update a post in the repository. Returns the post ID or a WP_Error. * * @param array $post * * @return int|WP_Error */ public function update(array $post) { return wp_update_post($post, true); } }
Refining our repository class
So looking at what we have so far, does anything feel off to you? To me, it looks like a good first pass. That said, it’s still a first pass. There are a few things that don’t feel quite right yet.
Let’s look at what we can improve on our second pass at it.
Redundant methods
The first thing that sticks out to me is the add
and update
methods. They feel pretty redundant. Does our class need both these methods? Could we get away with combining them into one method?
The only way to find out is by digging through the code of wp_update_post
! What does it do compared to wp_insert_post
? If you look at it, you’ll see that it doesn’t do that much.
wp_update_post
fetches the current post from the database using the ID in the array. If it couldn’t find one, it returns 0
or a WP_Error
. Otherwise, it merges the new fields into it. It then reinserts the post using wp_insert_post
.
So what do we know after doing this? Well, we know that it also uses wp_insert_post
to update the existing post. We also know that wp_update_post
won’t work if there’s no ID
key in the post
array. So why not handle this ourselves?
class Repository { /** * Save a post into the repository. Returns the post ID or a WP_Error. * * @param array $post * * @return int|WP_Error */ public function save(array $post) { if (!empty($post['ID'])) { return wp_update_post($post, true); } return wp_insert_post($post, true); } }
That’s what this new save
method does. It replaces our existing add
and update
methods. It still takes an array of post data as its argument.
The difference is that the method checks if the ID
key in the given post
array is empty. If it isn’t, it calls wp_update_post
and returns the result. Otherwise, it calls wp_insert_post
and returns that result instead. This lets us add and update posts with the same method!
Reusing WP_Query
The next thing that felt wrong was WP_Query
. We’re creating a WP_Query
object right in the find_by_id
method. This is a big no-no. We don’t want to create a tight coupling like that inside our Repository
class. Let’s add this dependency to the constructor instead.
class Repository { /** * WordPress query object. * * @var WP_Query */ private $query; /** * Constructor. * * @param WP_Query $query */ public function __construct(WP_Query $query) { $this->query = $query; } }
So here’s our constructor. It takes a WP_Query
object as an argument. It then assigns it to the internal query
variable. An optional next step that you can take is to create a custom constructor for the repository.
class Repository { // ... /** * Initialize the repository. * * @return Repository */ public static function init() { return new self(new WP_Query()); } }
This is what the init
static method does. new self
creates a new instance of our Repository
class. We then pass it a new WP_Query
object. The only thing left is to rework our find_by_id
to use our query
variable.
class Repository { // ... /** * Find a post using the given post ID. * * @param int $id * * @return WP_Post|null */ public function find_by_id($id) { $posts = $this->query->query(array( 'p' => $id, 'posts_per_page' => 1, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, )); return !empty($posts[0]) ? $posts[0] : null; } }
As you can see, we removed the query
variable inside our method. We replaced it with a call to the query
method of our internal WP_Query
variable. This method is what lets us reuse the same WP_Query
object.
Whenever you call it, it resets all internal query arguments inside the WP_Query
object. That means that you can call it as many times as you want without any side effects. Once reset, it processes your query as it would if you created a new WP_Query
object.
You even get a small bonus when you use it! You don’t need it to call get_posts
anymore because it does it for you. That’s why we assign the result to the posts
variable. We finish up by doing the same check to see if we have a post or not.
Breaking down the find_by_id method
And speaking of reusing WP_Query
, what about reusing part of our find_by_id
methods? You might want to find posts for a given author or maybe on a given date. What can we do to maximize the code reuse for those scenarios?
A common thing that you see is creating a method for finding a single object and another to find more than one object. You then use one of these methods as a base for your more specific find_by_*
method. Let’s start with the method that finds more than one object.
Creating a find method
class Repository { // ... /** * Find all post objects for the given query. * * @param array $query * * @return WP_Post[] */ private function find(array $query) { $query = array_merge(array( 'no_found_rows' => true, 'update_post_meta_cache' => true, 'update_post_term_cache' => false, ), $query); return $this->query->query($query); } }
This is the find
method shown above. We set the visibility of the method to private
so that only our Repository
class can use it. Its only argument is an array of query arguments. It takes the given array and merges default query arguments into it using array_merge
. These defaults will be the same performance improving ones that we’ve used so far.
If you want to overwrite them, you just need to add them to the array that you’ll pass to the find
method. array_merge
will only merge these default query arguments if they’re not present.
This array then gets passed to the query
method of WP_Query
. The result of query
method is always going to be an array of WP_Post
objects. That means that we can return the result right away without doing anything else.
Finding a single WP_Post object
Now that we have our find
method, we can use it to create a method to find a single WP_Post
object. Why? That’s because finding a single WP_Post
object is almost the same as finding more than one. The only difference it that you’re just limiting your results to one WP_Post
object.
class Repository { // ... /** * Find a single post object for the given query. Returns null * if it doesn't find one. * * @param array $query * * @return WP_Post|null */ private function find_one(array $query) { $query = array_merge($query, array( 'posts_per_page' => 1, )); $posts = $this->find($query); return !empty($posts[0]) ? $posts[0] : null; } }
And that’s exactly what find_one
does. It sets the posts_per_page
query argument to 1
so that it only gets a single result. As opposed to the query arguments in the find
method, this isn’t a default. It’s a mandatory query argument.
That’s why we inverted the order of the arguments for array_merge
. This ensures that posts_per_page
is always 1
. You can’t overwrite it without overwriting the method itself.
Next, it passes the modified query arguments array to the find
method. Now, even if we asked for one result, we’re still going to get an array of WP_Post
back. That’s why we still need to use our ternary operator to check if we got a result back. This lets our method return a WP_Post
or null if it found nothing.
Reworking our find_by_id
Using our new find_one
method, we can streamline our find_by_id
method.
class Repository { // ... /** * Find a post using the given post ID. * * @param int $id * * @return WP_Post|null */ public function find_by_id($id) { return $this->find_one(array('p' => $id)); } }
As you can see, almost all the query arguments are gone. The only one left is p
. That’s the one that lets us find a post by ID.
Leveraging our new methods
This is a small bonus to show how simple it is to create different find methods for our repository. We’ll create one to find posts written by a given user. Let’s name it find_by_author
.
class Repository { // ... /** * Find posts written by the given author. * * @param WP_User $author * @param int $limit * * @return WP_Post[] */ public function find_by_author(WP_User $author, $limit = 10) { return $this->find(array( 'author' => $author->ID, 'posts_per_page' => $limit, )); } }
The method takes two arguments. author
is a mandatory WP_User
object. limit
controls the number posts our method returns. The default is 10
to match the WordPress default.
Since we’re expecting more than one result, we need to use our find
method. We pass it a small query arguments array containing two keys. The author’s user ID is under the author
key. While the limit
argument is under the posts_per_page
.
Our repository in practice
$repository = Repository::init(); // find_by_id example $post = $repository->find_by_id(1); // find_by_author example $current_user = wp_get_current_user(); $posts = array(); if ($current_user instanceof WP_User) { $posts = $repository->find_by_author($current_user); }
Above is a small code sample that shows the find_by_id
and find_by_author
methods. We start by creating an instance of our Repository
class using the init
method. This is the custom constructor that we created earlier.
The find_by_id
example is pretty simple. We ask the repository to find a post which has a post ID of 1
. For the curious, that’s the ID of “Hello world!” post.
The last example shows how we use our find_by_author
method. To begin, we get the current user with wp_get_current_user
. We also create an empty post array. If current_user
is an instance of WP_User
, we pass it to our find_by_author
method. The repository will then find the 10 most recent posts from the current user.
Our intermediary with the WordPress database
If we look back at our job description, you can see that our Repository
class satisfies it pretty well. It’s taken over all interactions with the WordPress database. You can use it to find, save and delete posts objects.
We’ve removed the reliance on always building new WP_Query
objects. With functions like find
and find_one
, we’ve increased our ability to reuse code. Your other find methods can focus on the query themselves and not the logic of returning the results.
You can find the complete Repository
class below.
class Repository { /** * WordPress query object. * * @var WP_Query */ private $query; /** * Constructor. * * @param WP_Query $query */ public function __construct(WP_Query $query) { $this->query = $query; } /** * Initialize the repository. * * @uses PHP 5.3 * * @return self */ public static function init() { return new self(new WP_Query()); } /** * Find posts written by the given author. * * @param WP_User $author * @param int $limit * * @return WP_Post[] */ public function find_by_author(WP_User $author, $limit = 10) { return $this->find(array( 'author' => $author->ID, 'posts_per_page' => $limit, )); } /** * Find a post using the given post ID. * * @param int $id * * @return WP_Post|null */ public function find_by_id($id) { return $this->find_one(array('p' => $id)); } /** * Save a post into the repository. Returns the post ID or a WP_Error. * * @param array $post * * @return int|WP_Error */ public function save(array $post) { if (!empty($post['ID'])) { return wp_update_post($post, true); } return wp_insert_post($post, true); } /** * Find all post objects for the given query. * * @param array $query * * @return WP_Post[] */ private function find(array $query) { $query = array_merge(array( 'no_found_rows' => true, 'update_post_meta_cache' => true, 'update_post_term_cache' => false, ), $query); return $this->query->query($query); } /** * Find a single post object for the given query. Returns null * if it doesn't find one. * * @param array $query * * @return WP_Post|null */ private function find_one(array $query) { $query = array_merge($query, array( 'posts_per_page' => 1, )); $posts = $this->find($query); return !empty($posts[0]) ? $posts[0] : null; } }