What is a Custom Post Type?
It is basically a modified object under the parent Post object. Within each Custom Post Type, you get access to all of the features you get in the traditional Post object but it allows you to rename the post and create a myriad of ways of organizing your WordPress plugin (such as adding a Products post type for a payment plugin or an Affiliate post type).
For the purposes of this example, we’re using the WordPress Plugin Boilerplate (WPPB) framework when building out WordPress plugins. Check out our post introducing working with the WordPress Plugin Boilerplate if you would like to learn more.
The WPMerchant WordPress plugin, for example, can include three custom post types: Products, Plans, and Affiliates (as seen below).
If you click into the Affiliates custom post type, you’ll see a list of the existing Affiliates that have been created. In the case of the picture
If you click on an individual Affiliate in that list, you’ll see the Edit Affiliate page below:
What is a meta box?
An example of a meta box can be seen in the image below under the Affiliate Data heading. You can see that this meta box contains custom fields that include Company Name, Full Name, Email Address, Phone Number, and Notes (which is an instance of the WordPress Wysiwyg Editor).
Now, let’s show you how to create a meta box for a custom post type, render the custom fields to be shown in that meta box, and save the fields after a user publishes the custom post type. We’re going to be using WordPress Plugin Boilerplate for adding a meta box to a custom post type, as it provides a great structure for WordPress plugins.
STEP 1: Hook into add_meta_boxes in the class-plugin-name-admin.php file’s construct function
This is the hook that tells WordPress that you would like to add a meta box to the custom post type of your choosing. If you want to add a meta box to a page, you would simply replace CUSTOM_POST_TYPE_NAME below with page.
If you want to add a meta box to a media upload, also known as an attachment, the normal rules don’t apply and you need to check out this post.
add_action('add_meta_boxes_CUSTOM_POST_TYPE_NAME', array( $this, 'setupCustomPostTypeMetaboxes' ));
STEP 2: Add setupCustomPostTypeMetaboxes to the class-plugin-name-admin.php file
The add_meta_box function actually adds the meta box to the custom post type. The add_meta_box function allows you to define the name of this meta box, the function that will add all of the content inside of it, and where it is positioned on the custom post type edit page.
The setupCustomPostTypeMetaboxes is being referenced in the add_meta_boxes action for the custom post type that you’re working with, so you need to add it to the class-plugin-name-admin.php file. This function
public function setupCustomPostTypeMetaboxes(){
add_meta_box('custom_post_type_data_meta_box', 'Meta Box Data', array($this,'custom_post_type_data_meta_box'), 'custom_post_type', 'normal','high' );
}
Make sure to replace ‘custom_post_type’ above with the string of the custom post type you want to add the custom meta box to.
The different parameters for the add_meta_box function are shown below:
add_meta_box( string $id, string $title, callable $callback, string|array|WP_Screen $screen = null, string $context = 'advanced', string $priority = 'default', array $callback_args = null )
STEP 3: Add custom_post_type_data_meta_box to the class-plugin-name-admin.php file
This function is where you add all of the fields to your meta box. You can add as many custom fields to any WordPress or custom post type.
public function custom_post_type_data_meta_box($post){
// Add a nonce field so we can check for it later.
wp_nonce_field( $this->plugin_name.'_affiliate_meta_box', $this->plugin_name.'_affiliates_meta_box_nonce' );
echo '<div class="post_type_field_containers">';
echo '<ul class="plugin_name_product_data_metabox">';
echo '<li><label for="'.$this->plugin_name.'_company_name">';
_e( 'Company Name', $this->plugin_name.'_company_name' );
echo '</label>';
$args = array (
'type' => 'input',
'subtype' => 'text',
'id' => $this->plugin_name.'_company_name',
'name' => $this->plugin_name.'_company_name',
'required' => '',
'get_options_list' => '',
'value_type'=>'normal',
'wp_data' => 'post_meta',
'post_id'=> $post->ID
);
// this gets the post_meta value and echos back the input
$this->plugin_name_render_settings_field($args);
echo '</li><li><label for="'.$this->plugin_name.'_fullname">';
_e( 'Full Name', $this->plugin_name.'_fullname' );
echo '</label>';
$args = array (
'type' => 'input',
'subtype' => 'text',
'id' => $this->plugin_name.'_fullname',
'name' => $this->plugin_name.'_fullname',
'required' => '',
'get_options_list' => '',
'value_type'=>'normal',
'wp_data' => 'post_meta',
'post_id'=> $post->ID
);
// this gets the post_meta value and echos back the input
$this->plugin_name_render_settings_field($args);
echo '</li><li><label for="'.$this->plugin_name.'_email_address">';
_e( 'Email Address', $this->plugin_name.'_email_address' );
echo '</label>';
unset($args);
$args = array (
'type' => 'input',
'subtype' => 'text',
'id' => $this->plugin_name.'_email_address',
'name' => $this->plugin_name.'_email_address',
'required' => '',
'get_options_list' => '',
'value_type'=>'normal',
'wp_data' => 'post_meta',
'post_id'=> $post->ID
);
// this gets the post_meta value and echos back the input
$this->plugin_name_render_settings_field($args);
echo '</li><li><label for="'.$this->plugin_name.'_phone_number">';
_e( 'Phone Number', $this->plugin_name.'_phone_number' );
echo '</label>';
unset($args);
$args = array (
'type' => 'input',
'subtype' => 'text',
'id' => $this->plugin_name.'_phone_number',
'name' => $this->plugin_name.'_phone_number',
'required' => '',
'get_options_list' => '',
'value_type'=>'normal',
'wp_data' => 'post_meta',
'post_id'=> $post->ID
);
// this gets the post_meta value and echos back the input
$this->plugin_name_render_settings_field($args);
echo '</li>';
// provide textarea name for $_POST variable
$notes = get_post_meta( $post->ID, $this->plugin_name.'_notes', true );
$args = array(
'textarea_name' => $this->plugin_name.'_notes',
);
echo '<li><label for="'.$this->plugin_name.'_notes">';
_e( 'Notes', $this->plugin_name.'_notes' );
echo '</label>';
wp_editor( $notes, $this->plugin_name.'_notes_editor',$args);
echo '</li></ul></div>';
}
STEP 4: Add Render Settings Function
This is an enormous time saver. We built this custom function for our WordPress plugins and it populates and renders an input element. This saves you from adding the input HTML, getting the value of the field from the database, and injecting that value into the field, for each and every field in both your general settings pages and your custom meta box fields pages. Copy and paste the following into your class-plugin-name-admin.php file.
public function plugin_name_render_settings_field($args) {
if($args['wp_data'] == 'option'){
$wp_data_value = get_option($args['name']);
} elseif($args['wp_data'] == 'post_meta'){
$wp_data_value = get_post_meta($args['post_id'], $args['name'], true );
}
switch ($args['type']) {
case 'input':
$value = ($args['value_type'] == 'serialized') ? serialize($wp_data_value) : $wp_data_value;
if($args['subtype'] != 'checkbox'){
$prependStart = (isset($args['prepend_value'])) ? '<div class="input-prepend"> <span class="add-on">'.$args['prepend_value'].'</span>' : '';
$prependEnd = (isset($args['prepend_value'])) ? '</div>' : '';
$step = (isset($args['step'])) ? 'step="'.$args['step'].'"' : '';
$min = (isset($args['min'])) ? 'min="'.$args['min'].'"' : '';
$max = (isset($args['max'])) ? 'max="'.$args['max'].'"' : '';
if(isset($args['disabled'])){
// hide the actual input bc if it was just a disabled input the informaiton saved in the database would be wrong - bc it would pass empty values and wipe the actual information
echo $prependStart.'<input type="'.$args['subtype'].'" id="'.$args['id'].'_disabled" '.$step.' '.$max.' '.$min.' name="'.$args['name'].'_disabled" size="40" disabled value="' . esc_attr($value) . '" /><input type="hidden" id="'.$args['id'].'" '.$step.' '.$max.' '.$min.' name="'.$args['name'].'" size="40" value="' . esc_attr($value) . '" />'.$prependEnd;
} else {
echo $prependStart.'<input type="'.$args['subtype'].'" id="'.$args['id'].'" "'.$args['required'].'" '.$step.' '.$max.' '.$min.' name="'.$args['name'].'" size="40" value="' . esc_attr($value) . '" />'.$prependEnd;
}
/*<input required="required" '.$disabled.' type="number" step="any" id="'.$this->plugin_name.'_cost2" name="'.$this->plugin_name.'_cost2" value="' . esc_attr( $cost ) . '" size="25" /><input type="hidden" id="'.$this->plugin_name.'_cost" step="any" name="'.$this->plugin_name.'_cost" value="' . esc_attr( $cost ) . '" />*/
} else {
$checked = ($value) ? 'checked' : '';
echo '<input type="'.$args['subtype'].'" id="'.$args['id'].'" "'.$args['required'].'" name="'.$args['name'].'" size="40" value="1" '.$checked.' />';
}
break;
default:
# code...
break;
}
}
STEP 5: Add Action to Save Those Custom Fields that you have associated with the custom post type in the construct method
You need to hook into save_post so that you can actually save the information that users add to the custom post type’s meta box.
add_action( 'save_post_CUSTOM_POST_TYPE_NAME', array( $this, 'saveCustomPostTypeMetaBoxData') );
STEP 6: Add the Custom Post Type Save Function Referenced in the Action Above
The following function actually saves the information that customers add to the fields that you add in the meta box that you added to the custom post type.
public function saveCustomPostTypeMetaBoxData( $post_id ) {
/*
* We need to verify this came from our screen and with proper authorization,
* because the save_post action can be triggered at other times.
*/
// Check if our nonce is set.
if ( ! isset( $_POST['custom_post_type_meta_box_nonce'] ) ) {
return;
}
// Verify that the nonce is valid.
if ( ! wp_verify_nonce( $_POST['custom_post_type_meta_box_nonce'], 'custom_post_type_meta_box' ) ) {
return;
}
// If this is an autosave, our form has not been submitted, so we don't want to do anything.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// Check the user's permissions.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Make sure that it is set.
if ( !isset( $_POST[$this->plugin_name.'_company_name'] ) && !isset( $_POST[$this->plugin_name.'_fullname'] ) && !isset( $_POST[$this->plugin_name.'_email_address'] ) && !isset( $_POST[$this->plugin_name.'_phone_number'] ) && !isset( $_POST[$this->plugin_name.'_notes'] )) {
return;
}
/* OK, it's safe for us to save the data now. */
// Sanitize user input.
$company_name = sanitize_text_field( $_POST[$this->plugin_name."_company_name"]);
$fullname = sanitize_text_field( $_POST[$this->plugin_name."_fullname"]);
$email_address = sanitize_text_field( $_POST[$this->plugin_name."_email_address"]);
$phone_number = sanitize_text_field( $_POST[$this->plugin_name."_phone_number"]);
$notes = wp_kses_post( $_POST[$this->plugin_name."_notes"]);
update_post_meta($post_id, $this->plugin_name.'_company_name',$company_name);
update_post_meta($post_id, $this->plugin_name.'_fullname',$fullname);
update_post_meta($post_id, $this->plugin_name.'_email_address',$email_address);
update_post_meta($post_id, $this->plugin_name.'_phone_number',$phone_number);
update_post_meta($post_id, $this->plugin_name.'_notes',$notes);
}