Designing a class to manage WordPress posts

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

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.

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.

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

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.

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:

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?

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.

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.

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.

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

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.

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.

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.

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

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.

  • Note: I’d make find_by_id a simple wrapper for get_post() to improve readability and performance.

    • There’s no major impact on performance. There’s an extra query between the two, but that’s because we want to update the post meta cache. If you disable that, they’re almost identical in speed.

      Readability, I think, comes down to a personal preference. I find mine easier to read because I’m always thinking in the context of WP_Query. get_post doesn’t work the same way.

      get_post uses WP_Post::get_instance which queries the database using wpdb. You also lose the automatic post meta cache update as well. That said, it might not be useful to you in the first place. It’s a design tradeoff 🙂

  • Great article that enlightened me immensely. Kudos.

    Everything was perfect in terms of DRY, but one thing bugged me a bit – the name of the Class (Repository).

    Maybe it is me, but Repository does not really describe what this class does. Maybe something like WPQuery_xxxxx or something? Repository is ambiguous and reminds of GIT.

    Outside of that, geez, you have sent me on a whole new way of thinking.

    • I think “PostRepository” could have been a good name too. I left it a bit ambiguous because I plan on looking at more advanced use cases of this in the future.

      I’ve done a lot of Domain-driven Design in the last few years. Repository is a term that’s used a lot there and with ORMs. That’s why I used it and tried to explain it at the beginning.

      If you prefer, “Collection” is a good name too. WPQuery_XXX could work if we did more abstraction. We could have WPDB_XXX.

      I’d say pick a name that works for you. 🙂

      • Agreed somewhat. Repository still does not fit as a name.

        Since this is a query type of class that extends WordPress’s query object, it should be named accordingly.

        The word ‘repository’ has nothing to do with queries. Neither does ‘collection’. This is an extension of the WP_Query class. Should be named, or branded, accordingly.

        Not my place to say. But I am going to use your code somewhat as is. My class name is going to be {prefix}_WPQuery. Actually going to work this into a current project. It is that good.

        Just wanted to let you know that your article should at least use a different class designation.

        • Martin Sotirov

          Actually, Repository is the correct name for Carl’s class because it does data retrieval AND data persistence. This is a well known design pattern. You can read about it in Eric Evans’ book “Domain-Driven Design”.

  • Hi Carl, I have a question about your init() method, why you write it static and not in the same manner of the others methods? What is the benefit to use static method?
    Thank you 🙂

    • The goal of the init method is to act as a “custom constructor”. That’s why it uses “new self”. This is only possible inside a static method.

      If the method wasn’t static, we’d have to create a new Repository object and then call init. Except this wouldn’t work. That’s because you need an instance of WP_Query to construct Repository which is the role of the “init” method.

      I discuss this idea of custom constructor more here:

      Feel free to let me know if you need more clarifications!

      • I’ve read and I understand that with custom constructor it’s easy to do unit test without load all WP when I use hooks and I can write only one line of code like this:

        add_action( ‘plugins_loaded’, array( ‘MyPlugin’, ‘init’) );

        but in this case I don’t understand what is the difference to write the code lake this:

        $repository = Repository::init();
        $post = $repository->find_by_id( 1 );

        Instead of this:

        $repository = new Repository( WP_Query );
        $post = $repository->find_by_id( 1 );

        (I would like to better understand the static purpose of method)

        thank you

        • Ah! Well there isn’t lol. You don’t have to do it that way at all if you prefer to do. That’s why I say:

          “An optional next step that you can take is to create a custom constructor for the repository.”

          In this case, it’s optional. Like you said, you don’t have hooks that make this an obvious choice. You do it if you like it better. That’s it. 🙂

          Sorry for the confusion!