Main Content

How to create custom field with validation on saving

Adding a Meta Box

We will called add_meta_box inside a callback function that should be executed when the current page’s meta boxes are loaded. This task can be performed hooking the callback to the add_meta_box_{custom-post-type} action hook.

/**
 * Add meta box
 *
 * @param post $post The post object
 * @link https://codex.wordpress.org/Plugin_API/Action_Reference/add_meta_boxes
 * add_meta_box(
 * 	string $id, 
 * 	string $title, 
 * 	callable $callback, 
 * 	$screen = null, 
 * 	string $context = 'advanced',
 * 	string $priority = 'default', 
 * 	array $callback_args = null )
 * );
 * $screen - (string|array|WP_Screen) (Optional) The screen or screens on which to show the box (such as a post type, 'link', or 'comment'). Accepts a single screen ID, WP_Screen object, or array of screen IDs. Default is the current screen.
 *
 * add_meta_boxes_{post_type}
 */
function advance_add_meta_boxes( $post ) {
	add_meta_box( 
		'advance_meta_box', 
		'Advanced Options',
		array( $this, 'advance_build_meta_box' ),
		array( 'post', 'page' ), 
		'side', 
		'high'
	);
}
add_action( 'add_meta_boxes_post', 'advance_add_meta_boxes' );
add_action( 'add_meta_boxes_page', 'advance_add_meta_boxes' );

In the code above we add meta box on post type “Post” and “Page”, we’re passing six arguments to add_meta_box. Arguments: ID for meta box, title, callback function, slug of post type(string|array|WP_Screen), context(default:advanced|normal|side), priority(default|high|low)

 

The callback function will output the HMTL markup into the meta box.


function advance_build_meta_box( $post ){
	// Your code goes here
}

We should consider adding wp_nonce_field to add extra layer of security to protect against unexpected requests.

wp_nonce_field( basename( __FILE__ ), 'advance_meta_box_nonce' );

Here, we’ve passed the function just two of the four admitted arguments. The first one is the action name, here set to the basename of the current file, while the second argument is the name attribute of the hidden field.

 

After adding wp_nonce_field we are going to add custom field using get_post_meta to retrieve from the database.

// retrieve the _advance_display_title current value
$current_display_title = get_post_meta( $post->ID, '_advance_display_title', true );

// retrieve the _advance_custom_text current value
$current_custom_text = get_post_meta( $post->ID, '_advance_custom_text', true );

// stores _advance_share_posts array 
$current_share_posts = ( get_post_meta( $post->ID, '_advance_share_posts', true ) ) ? get_post_meta( $post->ID, '_advance_share_posts', true ) : array();

 

Printing form fields

$meta_box_output = '';
		
$meta_box_output .= '<div class="inside">

	<h3>' . __( "Display Title", "advance_example_plugin" ) . '</h3>
	<p>
		<input type="radio" id="display_title_0" name="display_title" value="0" ' . checked( $current_display_title, 0, false ) . '> <label for="display_title_0">Yes</label><br />
		<input type="radio" id="display_title_1" name="display_title" value="1" ' . checked( $current_display_title, 1, false ) . '> <label for="display_title_1">No</label>
	</p>

	<h3>' . __( "Custom Text", "advance_example_plugin" ) . '</h3>
	<p>
		<input type="text" name="custom_text" value="' . $current_custom_text . '" /> 
	</p>

	<h3>' . __( "Share this on", "advance_example_plugin" ) . '
	<p>';
		$share_post_count = 0;
		foreach ( $share_posts as $share_post ) {
			$meta_box_output .= '<input type="checkbox" id="share_post_' . $share_post_count . '" name="share_posts[]" value="' . $share_post . '" ' . checked( ( in_array( $share_post, $current_share_posts ) ) ? $share_post : '', $share_post, false ) . '> <label for="share_post_' . $share_post_count . '">' . $share_post . '</label><br />';
			$share_post_count++;
		}

	$meta_box_output .= '</p>
</div>';

echo $meta_box_output;

 

And now we can put it all together:

/**
 * Build custom field meta box
 *
 * @param post $post The post object
 * You can add wp-editor
 * $custom_content = get_post_meta( $post->ID, 'custom-field-name',  true );
	* $custom_id = 'custom-id';
	* $custom_setting = array(
		* 'media_buttons' => false,
		* 'tinymce' => false,
		* 'textarea_rows' => 10,
		* 'tabindex' => 2,
		* 'textarea_name' => $custom_id, // Editor #ID
	* );
	* wp_editor( 
		* $custom_content, 
		* $custom_id, 
		* $custom_setting
	* );
 */
function advance_build_meta_box( $post ){
	// make sure the form request comes from WordPress
	wp_nonce_field( basename( __FILE__ ), 'advance_meta_box_nonce' );
	
	// retrieve the _advance_display_title current value
	$current_display_title = get_post_meta( $post->ID, '_advance_display_title', true );
	
	// retrieve the _advance_custom_text current value
	$current_custom_text = get_post_meta( $post->ID, '_advance_custom_text', true );
	
	$share_posts = array(
		'Facebook',
		'Twitter',
		'Google Plus',
		'Instagram'
	);
	
	// stores _advance_share_posts array 
	$current_share_posts = ( get_post_meta( $post->ID, '_advance_share_posts', true ) ) ? get_post_meta( $post->ID, '_advance_share_posts', true ) : array();

	// Output
	$meta_box_output = '';
	
	$meta_box_output .= '<div class="inside">

		<h3>' . __( "Display Title", "advance_example_plugin" ) . '</h3>
		<p>
			<input type="radio" id="display_title_0" name="display_title" value="0" ' . checked( $current_display_title, 0, false ) . '> <label for="display_title_0">Yes</label><br />
			<input type="radio" id="display_title_1" name="display_title" value="1" ' . checked( $current_display_title, 1, false ) . '> <label for="display_title_1">No</label>
		</p>

		<h3>' . __( "Custom Text", "advance_example_plugin" ) . '</h3>
		<p>
			<input type="text" name="custom_text" value="' . $current_custom_text . '" /> 
		</p>

		<h3>' . __( "Share this on", "advance_example_plugin" ) . '
		<p>';
			$share_post_count = 0;
			foreach ( $share_posts as $share_post ) {
				$meta_box_output .= '<input type="checkbox" id="share_post_' . $share_post_count . '" name="share_posts[]" value="' . $share_post . '" ' . checked( ( in_array( $share_post, $current_share_posts ) ) ? $share_post : '', $share_post, false ) . '> <label for="share_post_' . $share_post_count . '">' . $share_post . '</label><br />';
				$share_post_count++;
			}

		$meta_box_output .= '</p>
	</div>';

	echo $meta_box_output;
}

 

Storing Data

We’re gonna use save_post_{$post_type} this will run on specific post type only.

add_action( 'save_post_{$post_type}', 'advance_save_meta_box_data' );
function advance_save_meta_box_data( $post_id ) {
	// Your code goes here
}

 

Check if $_POST is empty this happens on bulk edit.

if ( empty( $_POST ) ) return $post_id;

 

We have to check the nonce value field we have set earlier.

if ( !isset( $_POST['advance_meta_box_nonce'] ) || !wp_verify_nonce( $_POST['advance_meta_box_nonce'], basename( __FILE__ ) ) )
			return;

 

Verify if edit comes from “Quick Edit”.

if ( isset( $_POST[ '_inline_edit' ] ) && ! wp_verify_nonce( $_POST[ '_inline_edit' ], 'inlineeditnonce' ) ) return $post_id;

 

Don’t save on autosave.

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return $post_id;

 

Dont save on revisions.

if ( isset( $post->post_type ) && $post->post_type == 'revision' ) return $post_id;

 

Check the user’s permissions.

if ( !current_user_can( 'edit_page', $post_id ) ) return;

 

Check post status.

if ( get_post_status( $post_id ) == 'trash'  ) return;

 

Unhook this function to prevent infinite looping but we will turn this back after updating post meta.

remove_action( 'save_post_food', 'advance_save_meta_box_data' );

 

Storing value

// display_title string
if ( isset( $_REQUEST['display_title'] ) ) {
	update_post_meta( $post_id, '_advance_display_title', sanitize_text_field( $_POST['display_title'] ) );
}

// store custom fields values
// custom_text string
if ( isset( $_REQUEST['custom_text'] ) ) {
	update_post_meta( $post_id, '_advance_custom_text', sanitize_text_field( $_POST['custom_text'] ) );
}

// store custom fields values
if( isset( $_POST['share_posts'] ) ){
	$share_posts = (array) $_POST['share_posts'];
	// sinitize array
	$share_posts = array_map( 'sanitize_text_field', $share_posts );
	// save data
	update_post_meta( $post_id, '_advance_share_posts', $share_posts );
}else{
	// delete data
	delete_post_meta( $post_id, '_advance_share_posts' );
}

 

Rehook save_post

add_action( 'save_post_food', 'advance_save_meta_box_data' );

 

Full code below:

new custom_field();
class custom_field {
	function __construct() {
		add_action( 'add_meta_boxes_post', array( $this, 'advance_add_meta_boxes' ) );
		add_action( 'save_post_post', array( $this, 'advance_save_meta_box_data' ) );

		add_action( 'add_meta_boxes_page', array( $this, 'advance_add_meta_boxes' ) );
		add_action( 'save_post_page', array( $this, 'advance_save_meta_box_data' ) );
	}

	/**
	 * Add meta box
	 *
	 * @param post $post The post object
	 * @link https://codex.wordpress.org/Plugin_API/Action_Reference/add_meta_boxes
	 * add_meta_box(
	 * 	string $id, 
	 * 	string $title, 
	 * 	callable $callback, 
	 * 	$screen = null, 
	 * 	string $context = 'advanced',
	 * 	string $priority = 'default', 
	 * 	array $callback_args = null )
	 * );
	 * $screen - (string|array|WP_Screen) (Optional) The screen or screens on which to show the box (such as a post type, 'link', or 'comment'). Accepts a single screen ID, WP_Screen object, or array of screen IDs. Default is the current screen.
	 *
	 * add_meta_boxes_{post_type}
	 */
	function advance_add_meta_boxes( $post ) {
		add_meta_box( 
			'advance_meta_box', 
			'Advanced Options',
			array( $this, 'advance_build_meta_box' ),
			array( 'post', 'page' ), 
			'side', 
			'high'
		);
	}

	/**
	 * Build custom field meta box
	 *
	 * @param post $post The post object
	 * You can add wp-editor
	 * $custom_content = get_post_meta( $post->ID, 'custom-field-name',  true );
		* $custom_id = 'custom-id';
		* $custom_setting = array(
			* 'media_buttons' => false,
			* 'tinymce' => false,
			* 'textarea_rows' => 10,
			* 'tabindex' => 2,
			* 'textarea_name' => $custom_id, // Editor #ID
		* );
		* wp_editor( 
			* $custom_content, 
			* $custom_id, 
			* $custom_setting
		* );
	 */
	function advance_build_meta_box( $post ){
		// make sure the form request comes from WordPress
		wp_nonce_field( basename( __FILE__ ), 'advance_meta_box_nonce' );
		
		// retrieve the _advance_display_title current value
		$current_display_title = get_post_meta( $post->ID, '_advance_display_title', true );
		
		// retrieve the _advance_custom_text current value
		$current_custom_text = get_post_meta( $post->ID, '_advance_custom_text', true );
		
		$share_posts = array(
			'Facebook',
			'Twitter',
			'Google Plus',
			'Instagram'
		);
		
		// stores _advance_share_posts array 
		$current_share_posts = ( get_post_meta( $post->ID, '_advance_share_posts', true ) ) ? get_post_meta( $post->ID, '_advance_share_posts', true ) : array();

		// Output
		$meta_box_output = '';
		
		$meta_box_output .= '<div class="inside">

			<h3>' . __( "Display Title", "advance_example_plugin" ) . '</h3>
			<p>
				<input type="radio" id="display_title_0" name="display_title" value="0" ' . checked( $current_display_title, 0, false ) . '> <label for="display_title_0">Yes</label><br />
				<input type="radio" id="display_title_1" name="display_title" value="1" ' . checked( $current_display_title, 1, false ) . '> <label for="display_title_1">No</label>
			</p>

			<h3>' . __( "Custom Text", "advance_example_plugin" ) . '</h3>
			<p>
				<input type="text" name="custom_text" value="' . $current_custom_text . '" /> 
			</p>

			<h3>' . __( "Share this on", "advance_example_plugin" ) . '
			<p>';
				$share_post_count = 0;
				foreach ( $share_posts as $share_post ) {
					$meta_box_output .= '<input type="checkbox" id="share_post_' . $share_post_count . '" name="share_posts[]" value="' . $share_post . '" ' . checked( ( in_array( $share_post, $current_share_posts ) ) ? $share_post : '', $share_post, false ) . '> <label for="share_post_' . $share_post_count . '">' . $share_post . '</label><br />';
					$share_post_count++;
				}

			$meta_box_output .= '</p>
		</div>';

		echo $meta_box_output;
	}

	/**
	 * Store custom field meta box data
	 *
	 * @param int $post_id The post ID.
	 * @link https://codex.wordpress.org/Plugin_API/Action_Reference/save_post
	 * 
	 * The save_post_{$post_type} hook runs whenever a new $post_type is saved or updated.
	 * 
	 * If saving a HTML don't use sanitize_text_field
	 * sanitize_text_field()
	 * Description #Description
		* Checks for invalid UTF-8,
		* Converts single < characters to entities
		* Strips all tags
		* Removes line breaks, tabs, and extra whitespace
		* Strips octets
	 * sanitize_text_field( $_POST['display_title'] )
	 * Sanitize an array field using array_map and sanitize_text_field
	 * array_map( 'sanitize_text_field', $share_posts )
	 */
	function advance_save_meta_box_data( $post_id ) {
		// Pointless if $_POST is empty (this happens on bulk edit)
		if ( empty( $_POST ) ) 
			return $post_id;
		
		// Verify taxonomies meta box nonce
		if ( !isset( $_POST['advance_meta_box_nonce'] ) || !wp_verify_nonce( $_POST['advance_meta_box_nonce'], basename( __FILE__ ) ) )
			return;
		// Add the 2 lines below on custom field meta box
		// If you have multiple meta box function you can add it on one metabox if same page post type
		// // make sure the form request comes from WordPress
		// wp_nonce_field( basename( __FILE__ ), 'advance_meta_box_nonce' );
		
		// Verify quick edit nonce
		if ( isset( $_POST[ '_inline_edit' ] ) && ! wp_verify_nonce( $_POST[ '_inline_edit' ], 'inlineeditnonce' ) ) 
			return $post_id;
		
		// Don't save on autosave
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) 
			return $post_id;
		
		// Dont save on revisions
		if ( isset( $post->post_type ) && $post->post_type == 'revision' ) 
			return $post_id;
		
		// Check the user's permissions.
		if ( !current_user_can( 'edit_page', $post_id ) ) 
			return;
		
		// Check post status
		if ( get_post_status( $post_id ) == 'trash'  ) 
			return;
			
		// Unhook this function to prevent infinite looping
		remove_action( 'save_post_food', 'advance_save_meta_box_data' );

		// store custom fields values
			// display_title string
			if ( isset( $_REQUEST['display_title'] ) ) {
				update_post_meta( $post_id, '_advance_display_title', sanitize_text_field( $_POST['display_title'] ) );
			}
			
			// store custom fields values
			// custom_text string
			if ( isset( $_REQUEST['custom_text'] ) ) {
				update_post_meta( $post_id, '_advance_custom_text', sanitize_text_field( $_POST['custom_text'] ) );
			}
			
			// store custom fields values
			if( isset( $_POST['share_posts'] ) ){
				$share_posts = (array) $_POST['share_posts'];
				// sinitize array
				$share_posts = array_map( 'sanitize_text_field', $share_posts );
				// save data
				update_post_meta( $post_id, '_advance_share_posts', $share_posts );
			}else{
				// delete data
				delete_post_meta( $post_id, '_advance_share_posts' );
			}
		
		// 	rehook save_post
		add_action( 'save_post_food', 'advance_save_meta_box_data' );
	}

}