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

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.

Repository-Graph

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;
    }
}
Creative Commons License