Custom post types are one the most powerful features of WordPress. You can use them to save any type of data that you want in the wp_posts table. Most plugins that build complex features on top of WordPress (e.g. WooCommerce) rely on them.

But custom post types aren’t just useful for developing new features on top of WordPress. They also allow us to rethink how we use object-oriented programming with WordPress. And, as we’ll see, this is an important step in your journey learning object-oriented programming with WordPress.

That’s because having a different view of custom post types will expand your object-oriented design horizons. They’ll help you build new types of classes that you might not have considered before. And this can be a game-changer when creating larger object-oriented plugins or themes.

Reviewing how to use an interface with custom post types

Before we can begin, we’re going to need to review how we can use interfaces with custom post types. This is a subject that we covered in a previous article. That said, a lot of what we’re going to do in this article uses design elements from that article.

That’s why we’re going to take a moment to review it. We’ll look at some of the design decisions that this article suggested. We’ll use them as the foundation for what we’ll see in this article.

The PostType interface

Let’s begin by looking at the interface that we created for custom post types. We had called it MyPlugin_PostTypeInterface . It looked like this:

/** * Interface for WordPress custom post types. */ interface MyPlugin_PostTypeInterface { /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data(); /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * Interface for WordPress custom post types. */ interface MyPlugin_PostTypeInterface { /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data ( ) ; /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta ( ) ; }

The MyPlugin_PostTypeInterface interface defined two methods: get_post_data and get_post_meta . We used these two methods to extract the data stored in the objects that implemented the interface. This, in turn, allowed us to save that data inside the WordPress database.

But why did we need two methods? Well, it’s because these two methods target two different ways to save data in the WordPress database. And, with few exceptions, we’ll need them both to save that data inside our object.

The get_post_data method extracts the data that we to save in the wp_posts table. It’s an array of post data that we want to pass to the wp_insert_post function. Meanwhile, the get_post_meta method extracts the data that we want to save in the wp_postmeta table. It returns an array of keys and values that we want to save using the update_post_meta function.

Using the PostType interface

The article also went over how we used the MyPlugin_PostTypeInterface interface. The main interaction point was the wp_insert_custom_post function. You can see it below:

/** * Insert or update a custom post type. * * @param MyPlugin_PostTypeInterface $post * @param bool $wp_error * * @return int|WP_Error */ function wp_insert_custom_post(MyPlugin_PostTypeInterface $post, $wp_error = false) { $post_id = wp_insert_post($post->get_post_data(), $wp_error); if (0 === $post_id || $post_id instanceof WP_Error) { return $post_id; } foreach ($post->get_post_meta() as $key => $value) { update_post_meta($post_id, $key, $value); } return $post_id; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * Insert or update a custom post type. * * @param MyPlugin_PostTypeInterface $post * @param bool $wp_error * * @return int|WP_Error */ function wp_insert_custom_post ( MyPlugin_PostTypeInterface $post , $wp_error = false ) { $post_id = wp_insert_post ( $post -> get_post_data ( ) , $wp_error ) ; if ( 0 === $post_id || $post_id instanceof WP_Error ) { return $post_id ; } foreach ( $post -> get_post_meta ( ) as $key = > $value ) { update_post_meta ( $post_id , $key , $value ) ; } return $post_id ; }

The wp_insert_custom_post function has two parameters: post and wp_error . The post parameter is type hinted. This means that it has to be an object implementing our MyPlugin_PostTypeInterface .

The wp_error parameter is just a boolean value that mirrors the wp_error parameter in the wp_insert_post function. We use it to determine whether the wp_insert_post function should return a WP_Error object when there’s an error. By default it’s false which means that the wp_insert_post function doesn’t return a WP_Error object.

The wp_insert_custom_post function itself shows how to use both methods from the PostTypeInterface interface. We use the get_post_data method first. It’s to get the array that we pass as the first argument to the wp_insert_post function.

We store the post ID returned by the wp_insert_post function in the post_id variable. We then use a guard clause to check if post_id is valid. If it’s is either 0 or an instance of WP_Error , we return it.

However, if post_id doesn’t meet either of those conditions, we know it’s valid. We can then use the array from get_post_meta method. We loop through it using a foreach loop using both a key and a value for each iteration. We then use those three values ( post_id , key and value ) as arguments for the update_post_meta function. This allows us also to update all the post meta values used by our object implementing the MyPlugin_PostTypeInterface interface.

Thinking about domains

Now that we’ve reviewed how we handled custom post types using the MyPlugin_PostTypeInterface interface, let’s dial things back a bit. We need to think about the larger picture around custom post types. And to do that, we need to talk about the idea of domains.

So what are domains? Well, they’re collections of knowledge surrounding a specific aspect of an application. These collections of knowledge often contain (but aren’t limited to) business requirements, specific terminology (also known as ubiqutous language) and application functionality.

For example, if you’re an e-commerce platform like WooCommerce, you could think of shipping as a domain. To ship an order to someone, WooCommerce must meet specific business requirements. There’s application functionality that developers designed to meet those requirements. And to design that functionality, it’s important for those developers to know specific terminology surrounding shipping.

But it’s really about domain logic

What we’ve just described about domains is, in essence, domain-driven design. However, the goal here isn’t to go over domain-driven design but to talk about domain logic. Domain logic is the application functionality that solves business requirements. You might also know it as business logic.

The big problem with domain logic is that it’s hard to know where to put it. And it’s hard regardless of whether you use object-oriented programming or not. But the difference is that you can do something concrete about it if you do use object-oriented programming.

We can create classes that represent various parts of our domain. And then those classes can manage the specific domain logic that applies to them. In domain-driven design, we would call these types of classes entities.

Entities are an essential building block in domain-driven design. But they’re a powerful concept on their own. Outside domain-driven design, you might know entities as models.

Entity vs Model

The term model comes from object-relational mapping frameworks. It’s what they call these types of classes. In practice, there’s a conceptual difference between the two terms. That said, most developers use the terms interchangeably.

But, for us, it’s important to know that difference. With an object-relational mapping framework, a model is a class that also maps to the database in some way. This creates a tight coupling between your models and the database.

The concept of entities isn’t related to the database or any form of persistence at all. In fact, you can even use entities without ever persisting them. (Persisting is a fancy way of saying saving.) What defines an entity is what it represents. An entity is always something that needs a unique representation within our domain.

If we continue with our WooCommerce example, an order would be an entity. But a shipment would one as well. They’re both things that need a unique representation according to our WooCommerce domain.

This idea of unique representation is often why there’s a confusion between entities and models. Since an entity needs to have a unique representation, it’s often persisted in a database like you would a model. But how you persist an entity doesn’t matter. In fact, even if you don’t persist it anywhere, something can still be an entity.

Persisting entities using custom post types

And now, we’ve gone full circle! This is how we can rethink how we use WordPress custom post types. We can use them as a way to persist our entities.

Which is why we reviewed the article on the MyPlugin_PostTypeInterface interface. This is the method that we’ll use to persist entities using a custom post type. But if you have another way that you want to use, that’s ok too! (Like we said, how we persist an entity shouldn’t matter. This is just one way to do it.)

Building a Product entity

But first, we need to build an entity. Let’s keep going with the e-commerce domain that we were talking about earlier. We won’t create an entity for an order or a shipment. Let’s pick something a bit simpler: a Product.

class MyPlugin_Product { } 1 2 3 class MyPlugin_Product { }

Above is our empty MyPlugin_Product class that we’ll use as our product entity. Now, let’s add a few properties to it. What would we say are properties that define a product?

class MyPlugin_Product { /** * Flag to see if the product is available or not. * * @var bool */ private $available; /** * The name of the product. * * @var string */ private $name; /** * The price of the product. * * @var string */ private $price; /** * The product's universal code. * * @var string */ private $product_code; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class MyPlugin_Product { /** * Flag to see if the product is available or not. * * @var bool */ private $available ; /** * The name of the product. * * @var string */ private $name ; /** * The price of the product. * * @var string */ private $price ; /** * The product's universal code. * * @var string */ private $product_code ; }

So we’ve upgraded our MyPlugin_Product entity class and added four properties to it. First, we have the available which is a boolean flag that lets us know if the product is available or not. Next, we have name and price which are for the name and price of the product. (Pretty self-explanatory!)

The last property is product_code . It’s our product’s universal product code. Those are the barcodes that you see everywhere.

Changing the price of a product

Now, we need to add some business logic to our product entity. For example, you might want to change its price. So let’s create a method to do that:

class MyPlugin_Product { /** * Change the price of a product. * * @return string */ public function change_price($new_price) { $this->price = $new_price; } } 1 2 3 4 5 6 7 8 9 10 11 12 class MyPlugin_Product { /** * Change the price of a product. * * @return string */ public function change_price ( $new_price ) { $this -> price = $new_price ; } }

Thinking about the concept of price

So far, there isn’t any business logic in our change_price method. We’re just assigning new_price to the price property like you would with a setter method. But this is incorrect because we all know that a price isn’t a normal data type like a string or an integer. We can’t just assign it blindly.

We first need to validate the new_price argument before assigning it to the price variable. This means that we need to figure out what defines the price of a product. While some elements of a price might vary from business to business, here are some common ones that apply to most prices:

Prices are numeric.

Prices aren’t negative.

Prices only have two decimal places of precision.

Adding our price business logic

Our change_price method doesn’t account for any of these business requirements right now. So let’s make some changes to it. That way we can at least cover the three requirements that we just talked about.

class MyPlugin_Product { // ... /** * Change the price of a product. * * @param mixed */ public function change_price($new_price) { if (!is_numeric($new_price)) { throw new \InvalidArgumentException('A product must have a "price" that is a non-negative number.'); } elseif ($new_price < 0) { throw new \InvalidArgumentException('A product must have a "price" that isn\'t negative.'); } elseif (!is_float($new_price)) { $new_price = (float) $new_price; } $this->price = number_format($new_price, 2, '.', ''); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class MyPlugin_Product { // ... /** * Change the price of a product. * * @param mixed */ public function change_price ( $new_price ) { if ( ! is_numeric ( $new_price ) ) { throw new \ InvalidArgumentException ( 'A product must have a "price" that is a non-negative number.' ) ; } elseif ( $new_price < 0 ) { throw new \ InvalidArgumentException ( 'A product must have a "price" that isn\'t negative.' ) ; } elseif ( ! is_float ( $new_price ) ) { $new_price = ( float ) $new_price ; } $this -> price = number_format ( $new_price , 2 , '.' , '' ) ; } }

Alright, so there’s a lot more going on in the modified change_price method above now! First, our change_price method now starts with three guard clauses. The two first guard clauses are there to cover the two first business requirements that we had for prices.

The first one checks whether new_price is numeric or not. The is_numeric PHP function is great for detecting that. If it determines that new_price isn’t numeric, we throw an InvalidArgumentException .

The second guard clause builds on the previous one. We know that new_price is numeric at that point. So all that we have to check now is whether new_price is negative or not. If it’s negative, we throw another InvalidArgumentException .

The last guard clause checks whether our numeric value is a float or not. If it’s not, we cast it as a float. We need this to meet the last business requirement surrounding our price.

That business requirement was that a price should only have two decimal places of precision. A float is a numeric value with decimal precision, so we have that part covered. The issue is that a float has more than two decimal points of precision.

So we need a way to ensure that our new_price only has two decimal places of precision. That’s why we pass it through the number_format PHP function. Only after doing that, do we assign it to the price property.

Persisting our entity

It’s not too far-fetched to imagine our MyPlugin_Product class filled with methods like our change_price method. But, as you can see, methods like it don’t retrieve or save data about a product. It’s all about business logic and nothing more.

Using the PostTypeInterface

This is where the idea to save a custom post type using an interface comes into play. This is the technique that we’ll use to persist our product entity. This means that we need to have our MyPlugin_Product class implement the MyPlugin_PostTypeInterface interface.

class MyPlugin_Product implements MyPlugin_PostTypeInterface { // ... } 1 2 3 4 class MyPlugin_Product implements MyPlugin_PostTypeInterface { // ... }

Next, we have to decide where we want to save the different properties of our entity. There’s no clear rule to follow for this. You can do it whichever way makes sense for you.

class MyPlugin_Product implements MyPlugin_PostTypeInterface { /** * The post type used by products. * * @var string */ const POST_TYPE = 'myplugin_product'; /** * Flag to see if the product is available or not. * * @var bool */ private $available; /** * The name of the product. * * @var string */ private $name; /** * The price of the product. * * @var string */ private $price; /** * The product's universal code. * * @var string */ private $product_code; // ... /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data() { return array( 'comment_status' => 'closed', 'post_name' => $this->name, 'post_title' => $this->name, 'post_type' => self::POST_TYPE, 'post_status' => $this->available ? 'publish' : 'draft', ); } /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta(); { return array( 'myplugin_product_price' => $this->price, 'myplugin_product_code' => $this->product_code, ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 class MyPlugin_Product implements MyPlugin_PostTypeInterface { /** * The post type used by products. * * @var string */ const POST_TYPE = 'myplugin_product' ; /** * Flag to see if the product is available or not. * * @var bool */ private $available ; /** * The name of the product. * * @var string */ private $name ; /** * The price of the product. * * @var string */ private $price ; /** * The product's universal code. * * @var string */ private $product_code ; // ... /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data ( ) { return array ( 'comment_status' = > 'closed' , 'post_name' = > $this -> name , 'post_title' = > $this -> name , 'post_type' = > self :: POST_TYPE , 'post_status' = > $this -> available ? 'publish' : 'draft' , ) ; } /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta ( ) ; { return array ( 'myplugin_product_price' = > $this -> price , 'myplugin_product_code' = > $this -> product_code , ) ; } }

Here’s our updated MyPlugin_Product class. We added the two methods from the MyPlugin_PostTypeInterface interface. We also added the POST_TYPE class constant which we’ll discuss in a moment.

Implementing the PostTypeInterface methods

The easiest method to talk about is the get_post_meta method. We use it to save data that doesn’t quite fit in the wp_posts table. And the two properties that don’t quite do are price and product_code .

The method with more things going is the get_post_data method. This is the method that we use to map properties to the wp_posts table. But we also use it to give some more information about our product to WordPress.

That’s why our array contains comment_status and post_status . We want to set comment_status to closed so that people can’t comment on our product. We also want to use the availability of our product to determine whether we set post_status to publish or draft .

For the rest, we’re going to use the name of our product for the post_name and post_title . The only thing left is the post_type or custom post type. This is where we use our POST_TYPE class constant.

About using a class constant

But why are we using a class constant for post_type ? Well, to begin with, we want to allow others to access this information. It’s not just MyPlugin_Product class that might want to know what post_type it uses.

That said, allowing others to know what post_type the MyPlugin_Product class uses is one thing. It doesn’t mean we should allow anyone (even the class itself) to modify it. That’s why it makes sense to use a class constant for post_type .

Why not extend the WP_Post class?

So the last thing that we’ll talk about is a bit of the elephant in the room. If you weren’t aware of it, WordPress already has a class that it uses to represent posts. It’s the WP_Post class.

If WordPress has a class that it uses to posts from the wp_posts database table, why aren’t we using it? Why don’t we have our MyPlugin_Product class extend it? Why don’t we combine it with our MyPlugin_PostTypeInterface interface? These are all questions that we shouldn’t leave unanswered.

The major reason why we can’t use the WP_Post class is due to the fact that it’s final. So even we wanted to use it, we wouldn’t be able to because of the final keyword. But even if WP_Post wasn’t final, we’d still have issues trying to use it.

That’s because of the design of the WP_Post class. If you dig into the WP_Post class, you’ll see that it has almost no business logic to speak of. All of it is in specific WordPress functions.

Instead, WordPress uses WP_Post objects more like data transfer objects. If you’re not familiar with the concept, data transfer objects are objects whose sole purpose is to transfer data. (The name is pretty self-explanatory!) Here, WordPress uses it to transfer post data from the MySQL database.

But this job transferring data from the MySQL database isn’t something that we care about with entities. In fact, it’s something that we’re trying to avoid since entities shouldn’t care about their persistence. They should only care about business logic.

That’s why we don’t want to use the WP_Post class with our entities even if it’s tempting to do so. Our MyPlugin_PostTypeInterface is more than enough to deal with the persistence of our entities. And there’s no need to tie it to the WP_Post class either. (Not that we could right now anyway.)

Where business logic lives

And this wraps up our look at entities and how we can use them with custom post types. This isn’t as easy of a concept to understand as some of the other articles that you might have read. That said, this doesn’t make it any less important to understand.

As developers, we often need to solve business problems for our clients or the company that we work for. This becomes an issue when you’re trying to design classes for WordPress. The logic used to solve these business problems needs to be somewhere.

In a lot of situations, entities are the solution to that problem. That’s why they’re an important tool in your object-oriented design toolbox. And now you have a way to persist them using WordPress custom post types.

Photo Credit: Christopher Gower