Custom post types are a powerful WordPress functionality. Everyone that works with WordPress long enough ends up using them. A custom post type can be anything. That flexibility is the source of much of its power. When using custom post type, you’re only limited by your imagination (trademark pending).

This flexibility also makes it a great use case for designing an interface. This article will put you in the interface creator seat. Exciting, I know!

As the interface creator, you’re in charge of designing the interface contract. This means that you get to dictate how someone use your interface (insert evil laughter here). It’s not all (evil) roses though.

The job gives you quite a lot of responsibility. This can make it hard for you to know where to start or what to do. Lucky for you, that’s what this article will help you with.

How WordPress handles post data

Before we go any further, let’s look at WordPress post data management. In particular, we want to look at post data in two contexts. What WordPress does when it retrieves and saves post data. The two (retrieving and saving) are quite different.

Retrieving a post

WordPress has a few ways to retrieve a post. You can use functions like get_post or a query. Whatever method you choose to use, you’ll always get WP_Post objects back.

The WP_Post class handles both the regular post data and all post meta information. That said, WP_Post is final. This prevents us from extending it and replacing it with another class. Because of this, it’s hard to use an interface when WordPress retrieves a post from the database. That’s why the article won’t cover it.

Saving a post

wp_insert_post is the primary function that WordPress uses to save a post. Unlike when you retrieve a post, it works using a post data array.

It’s important to note that the wp_insert_post function doesn’t handle post meta. You need to handle those yourself once it returns to you with a post ID. You can view an example of this two-step process below. It’s the cornerstone of the contract that we’ll create with the interface.

$post_id = wp_insert_post($post_data); foreach ($post_meta as $key => $value) { update_post_meta($post_id, $key, $value); } 1 2 3 4 5 $post_id = wp_insert_post ( $post_data ) ; foreach ( $post_meta as $key = > $value ) { update_post_meta ( $post_id , $key , $value ) ; }

The setup

Ok, now that you’re familiar with how WordPress manages post data. Let’s start working on our interface and its contract. The easiest way to do that is to start with an empty interface. This will act as the blank canvas that we’ll fill up during the rest of the article.

namespace MyPlugin; /** * Interface for WordPress custom post types. */ interface PostTypeInterface { } 1 2 3 4 5 6 7 8 namespace MyPlugin ; /** * Interface for WordPress custom post types. */ interface PostTypeInterface { }

That poor interface is all sad and lonely (on top of being empty). So let’s add a bit more to the mix. Let’s build a small Product class as well (yay friends!).

namespace MyPlugin; /** * A Product. */ class Product implements PostTypeInterface { /** * The key used by the product post type. * * @var string */ const POST_TYPE = 'product'; /** * The description of the product. * * @var string */ private $description; /** * The name of the product. * * @var string */ private $name; /** * The price of the product. * * @var float */ private $price; /** * Constructor. * * @param string $name * @param float $price * @param string $description */ public function __construct($name, $price, $description = '') { $this->description = $description; $this->name = $name; $this->price = $price; } } 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 namespace MyPlugin ; /** * A Product. */ class Product implements PostTypeInterface { /** * The key used by the product post type. * * @var string */ const POST_TYPE = 'product' ; /** * The description of the product. * * @var string */ private $description ; /** * The name of the product. * * @var string */ private $name ; /** * The price of the product. * * @var float */ private $price ; /** * Constructor. * * @param string $name * @param float $price * @param string $description */ public function __construct ( $name , $price , $description = '' ) { $this -> description = $description ; $this -> name = $name ; $this -> price = $price ; } }

It’s nothing too fancy so far. The class has a constructor, a constant and three internal variables: description , name and price . It also implements our PostTypeInterface that we just created. We’ll use this class for our custom post type.

register_post_type(Product:POST_TYPE); 1 register_post_type ( Product : POST_TYPE ) ;

This code uses the register_post_type function to register a “product” post type. It does so by using the constant from the Product class.

We’re keeping this simple for this article. We just need WordPress to know that our custom post type exists. This is what this does.

Let’s put that interface to work

Now that we have a nice foundation. Let’s find work for that empty interface. It’s time it earned its keep (no one likes slackers). Let’s look at two common scenarios that it needs to handle.

Inserting a custom post type

WordPress inserts posts into the database using the wp_insert_post function. We’ll use it to insert our own custom post types. So let’s create our own wp_insert_custom_pos to do that.

namespace MyPlugin; /** * Insert or update a custom post type. * * @param PostTypeInterface $post * @param bool $wp_error * * @return int|WP_Error */ function wp_insert_custom_post(PostTypeInterface $post, $wp_error = false) { } 1 2 3 4 5 6 7 8 9 10 11 12 13 namespace MyPlugin ; /** * Insert or update a custom post type. * * @param PostTypeInterface $post * @param bool $wp_error * * @return int|WP_Error */ function wp_insert_custom_post ( PostTypeInterface $post , $wp_error = false ) { }

The function takes two parameters like wp_insert_post . The difference is that our first parameter is now a type hint of our interface. We still need a way to convert our custom post type to a wp_insert_post compatible array. That’ll be the first job of our interface.

namespace MyPlugin; interface PostTypeInterface { /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data(); } 1 2 3 4 5 6 7 8 9 10 11 namespace MyPlugin ; interface PostTypeInterface { /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data ( ) ; }

Our interface now has the get_post_data method. This method allows us to convert a class using our interface to a wp_insert_post compatible array. Let’s update our wp_insert_custom_post function.

function wp_insert_custom_post(PostTypeInterface $post, $wp_error = false) { return wp_insert_post($post->get_post_data(), $wp_error); } 1 2 3 4 function wp_insert_custom_post ( PostTypeInterface $post , $wp_error = false ) { return wp_insert_post ( $post -> get_post_data ( ) , $wp_error ) ; }

Once that’s done, we need to take a look at our Product class. It’s missing the get_post_data method that we defined in PostTypeInterface . The method will return the relevant data we want to pass to wp_insert_post .

namespace MyPlugin; class Product implements PostTypeInterface { // ... /** * Get the product data as a wp_insert_post compatible array. * * @return array */ public function get_post_data() { return array( 'post_content' => $this->description, 'post_title' => $this->name, 'post_status' => 'publish', 'post_type' => self::POST_TYPE ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace MyPlugin ; class Product implements PostTypeInterface { // ... /** * Get the product data as a wp_insert_post compatible array. * * @return array */ public function get_post_data ( ) { return array ( 'post_content' = > $this -> description , 'post_title' = > $this -> name , 'post_status' = > 'publish' , 'post_type' = > self :: POST_TYPE ) ; } }

Looking at the array, you can see that we assigned our object variables to post data keys. We assigned our product name to the post title and the product description to the post content. We also needed to add the Product post type (using the constant) and the post status.

By default, WordPress publishes a post as a “draft”. This isn’t ideal because the post type doesn’t have an edit page. Instead, we want WordPress to publish our product right away. That’s why post status is set to “publish”.

Dealing with post metas

But what happens to the price of our product? That’s the type of information that goes in a post meta. We’ll need to add another method to our interface to handle that.

namespace MyPlugin; interface 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 namespace MyPlugin ; interface 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 ( ) ; }

This is the goal of the get_post_meta method that we added to the interface. We can use it in our wp_insert_custom_post function. It’ll allow us to add the post meta of a custom post type.

namespace MyPlugin; function wp_insert_custom_post(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 namespace MyPlugin ; function wp_insert_custom_post ( 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 first thing we need to do is save the post ID returned by wp_insert_post . We add some error validation code to check for the two possible error values. If we have an error, we return it and stop there.

Otherwise, we use the get_post_meta interface method to add all the post meta. We cycle through the array and add each post meta using the update_post_meta function. The array key acts as the post meta key.

namespace MyPlugin; class Product implements PostTypeInterface { // ... /** * Get the product data as a wp_insert_post compatible array. * * @return array */ public function get_post_data() { return array( 'post_content' => $this->description, 'post_title' => $this->name, 'post_status' => 'publish', 'post_type' => self:POST_TYPE ); } /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta() { return array( 'price' => $this->price ); } } 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 namespace MyPlugin ; class Product implements PostTypeInterface { // ... /** * Get the product data as a wp_insert_post compatible array. * * @return array */ public function get_post_data ( ) { return array ( 'post_content' = > $this -> description , 'post_title' = > $this -> name , 'post_status' = > 'publish' , 'post_type' = > self : POST_TYPE ) ; } /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta ( ) { return array ( 'price' = > $this -> price ) ; } }

We also have to update our Product class and add the get_post_meta method. The method returns an array with the price of the product. The wp_insert_custom_post function will then convert that array into a post meta. It’ll create it using “price” as the post meta key and our product price as the value.

Putting our interface to use

So now that we have everything built, what does it look like in practice? Let’s say you want a new product called “My awesome product”. It costs 49.99. How would you create the product and save it?

namespace MyPlugin; $product = new Product('My awesome product', 49.99); wp_insert_custom_post($product); 1 2 3 4 namespace MyPlugin ; $product = new Product ( 'My awesome product' , 49.99 ) ; wp_insert_custom_post ( $product ) ;

The first line creates our Product object with the name and price of the new product. The second line uses our new wp_insert_custom_post function to save it into the WordPress database.

And there you have it!

You managed to take a WordPress problem and build an interface around it (Awww yeah!). You can find all the beautiful (and complete) code from the article just below.

namespace MyPlugin; /** * Interface for WordPress custom post types. */ interface 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(); } /** * A Product. */ class Product implements PostTypeInterface { /** * The key used by the product post type. * * @var string */ const POST_TYPE = 'product'; /** * The description of the product. * * @var string */ private $description; /** * The name of the product. * * @var string */ private $name; /** * The price of the product. * * @var float */ private $price; /** * Constructor. * * @param string $name * @param float $price * @param string $description */ public function __construct($name, $price, $description = '') { $this->description = $description; $this->name = $name; $this->price = $price; } /** * Get the product data as a wp_insert_post compatible array. * * @return array */ public function get_post_data() { return array( 'post_content' => $this->description, 'post_title' => $this->name, 'post_status' => 'publish', 'post_type' => self:POST_TYPE ); } /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta() { return array( 'price' => $this->price ); } } /** * Insert or update a custom post type. * * @param PostTypeInterface $post * @param bool $wp_error * * @return int|WP_Error */ function wp_insert_custom_post(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; } /** * Registers our product custom post type. */ function register_custom_post_product() { register_post_type(Product::POST_TYPE); } add_action('init', 'register_custom_post_product'); 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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 namespace MyPlugin ; /** * Interface for WordPress custom post types. */ interface 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 ( ) ; } /** * A Product. */ class Product implements PostTypeInterface { /** * The key used by the product post type. * * @var string */ const POST_TYPE = 'product' ; /** * The description of the product. * * @var string */ private $description ; /** * The name of the product. * * @var string */ private $name ; /** * The price of the product. * * @var float */ private $price ; /** * Constructor. * * @param string $name * @param float $price * @param string $description */ public function __construct ( $name , $price , $description = '' ) { $this -> description = $description ; $this -> name = $name ; $this -> price = $price ; } /** * Get the product data as a wp_insert_post compatible array. * * @return array */ public function get_post_data ( ) { return array ( 'post_content' = > $this -> description , 'post_title' = > $this -> name , 'post_status' = > 'publish' , 'post_type' = > self : POST_TYPE ) ; } /** * Get all the post meta as a key-value associative array. * * @return array */ public function get_post_meta ( ) { return array ( 'price' = > $this -> price ) ; } } /** * Insert or update a custom post type. * * @param PostTypeInterface $post * @param bool $wp_error * * @return int|WP_Error */ function wp_insert_custom_post ( 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 ; } /** * Registers our product custom post type. */ function register_custom_post_product ( ) { register_post_type ( Product:: POST_TYPE ) ; } add_action ( 'init' , 'register_custom_post_product' ) ;