Complex Meta Boxes in WordPress

Posted by & filed under Advanced, Developing for WordPress, How To's, Meta Boxes, Recommended Practices.

Introduction

The present article will discuss the finer details of adding meta boxes to WordPress write screens. It will cover the WordPress functions involved in the process, as well as give in depth information about the necessary hooks involved in the process. You will learn how to display and save meta boxes. After working through this article, you will have the capability of adding your own meta boxes to your projects!

Displaying Meta Boxes with the add_meta_box Function

The add_meta_box function is the primary function that allows for information to be added to the add/edit screens for WordPress post types. This powerful function is easy to leverage and can lead to customized content being added to WordPress write screens.

The add_meta_box function takes seven arguments. The arguments with each argument’s data type, required/optional status, and default value are listed below.

Argument Data Type Required Optional Default
$id string X No default
$title string X No default
$callback string X No default
$page string X No default
$context string X advanced
$priority string X required
$callback_args array X null

To understand the purpose and influence of each argument, each will be discussed separately.

$id

The $id argument defines the internal name for the meta box. WordPress core determines order to display meta boxes and screens to display meta boxes on based on this argument. Internally, this reference to the meta box is utilized in numerous arrays to reference the meta box. Obviously, this is a crucial piece of information to send to the add_meta_box function. Externally, the value of the argument is used for the id attribute in the div that encompasses the meta box. Changing this value has no effect on the display of the meta box unless CSS rules have been applied to the meta box using the id attribute. While the add_meta_box will execute with any string, it is advisable that you only use characters that you would normally use for HTML id and class attributes: 0-9, a-z, ‘-’, and ‘_’. Spaces should also be avoided. It is also recommended that the $id argument is prefaced with a unique identifier. For example, in building a plugin called “Weather Report” that adds a meta box for attaching the current temperature to a post, it would be sensible to add “wr” to the beginning of the $id value. As such, an appropriate $id value would be “wr_weather_box”. By prefixing the $id value, clashes with other plugin utilizing similar values will be reduced and many headaches will be avoided.

Usage Tips:

  • Use only the following characters: 0-9, a-z, ‘-’, and ‘_’
  • Prefix with a unique string that represents your plugin or theme (e.g., “wb” for “Weather Box”

$title

The $title argument is only slightly more interesting than the $id argument. The $title argument controls the value that will be printed for the meta box label. This argument will find itself nestled nicely inside a span and h3 tag within the main meta box div. For instance, if the $title argument is set as “Current Weather”, the following HTML would be generated:

<h3 class='hndle'><span>Current Weather</span></h3>

Adding the text for the h3 element is all that the $title argument does. It is utilized only for labeling the meta box. As such, the argument value must be a string. It is recommended that the value of the $title argument is descriptive enough that the user has a good understanding of the purpose of the meta box. For instance, something like “Current Weather” is much easy to understand than “Awesome Weather Meta Box”. Additionally, parsimonious use of words is recommended to avoid the uglification of a user’s write screen. The following image shows the difference between using a simple two word title versus a nine word title.

The final recommendation for this argument is to make it localizable. A discussion of localization in WordPress is well beyond the scope of this article. Please refer to the WordPress Codex for an excellent discussion of localization in WordPress. For the $title argument, the __ function should be utilized with the desired string and the text domain for the plugin or theme.

__('Current Weather', 'wr_weather_box')

Usage Tips:

  • Use a descriptive title
  • Avoid a lengthy title as it will cause display problems
  • Use the l18n WordPress functions __ or _x with an appropriate text domain

$callback

In setting up a meta box, the $callback argument is one of the most essential arguments. It controls the function that is called to generate the HTML for the meta box. The function that is specified is responsible for generating the content that the meta box displays. Without it, the meta box would display a label with no content. The HTML generated by the callback function will be displayed in a div with class “inside”, which is inside the overall meta box div wrapper. The HTML will look like:

<div class="inside">
	The HMTL
</div>

It is important that the $callback argument only contains the following characters: 0-9, a-z, and ‘_’. Because this argument is referencing a function defined in PHP, it must follow the rules of naming PHP functions. Additionally, it is advisable to prefix the function with the a string representative of the plugin or function in which this code is added. Not doing so will open the project up to possible errors if another plugin or theme contains a function of the same name. In the case of the weather report meta box example, one might use the string “wr_display_meta_box” that corresponds with a function of the same name as the value for the $callback argument.

Usage Tips:

  • Use only the following characters: 0-9, a-z, and ‘_’
  • Prefix with a unique string that represents your plugin or theme (e.g., “wb” for “Weather Box”

$page

The last of the required arguments is $page. The $page argument specifies the write screens in which the meta box will be displayed. By default, WordPress has two post types (“post”, “page”) write screens, as well as the links write screen. The meta box can be displayed on any of these three content types’ write screens. Use “post” for the post write screen, “page” for the page write screen and “link” for the link write screen. Unfortunately, the $page argument cannot accept an array of values. To display the meta box on all three write pages, the add_meta_box function would need to be called three times, once for each of the three default $page argument values (Note: there is another way to add the meta box on every write screen by using the parameter passed to the callback function that is associated with the “add_meta_boxes” hook. As this is a more advanced topic, it is discussed in the “Using add_meta_box” of this article.

In addition to utilizing the three default write screen values for the $page argument, a custom post type’s slug can be used as the value for the $page argument. For instance, imagine that a custom post type was defined using the following:

$args = array(
	// Your args here
);
register_post_type('review', $args);

The meta box can be added to the “review” custom post type’s write screen by using the “review” slug as the $page argument value.

Usage Tips:

  • Can only be “post”, “page”, “link”, or any custom post type slug
  • Cannot be an array of values

$context and $priority

Using only the four previously discussed arguments, a meta box can be displayed; however, to gain greater control of the positioning, it is essential to understand the $context and $priority arguments. Both of these arguments are optional, but are necessary for precise positioning of meta boxes. I will discuss these two arguments together as the arguments are so closely related. Loosely speaking, the $context argument defines where the meta box will be displayed. It takes three possible values: “side”, “normal”, or “advanced” with “advanced” being the default value. The $context argument has a dramatic effect on where the meta box is positioned on the write screen. The “side” value will place the meta box in the right hand column of the screen where the “Publish”, “Format”, and “Featured Image” boxes (amongst others) are displayed (this assumes that you are using a two column layout in your write screens). The “normal” and “advanced” values will place the meta box in the middle column somewhere below the main content textarea. While the $context argument will place the meta box in a general area, the $priority argument will allow for greater control of where the meta box displays within the area of the write screen that it is being displayed. The $priority argument takes four values: “high”, “core”, “low”, and “default”, with “default” cleverly being the default value.

Placement of the meta boxes can be a tricky especially as the position of meta boxes may be altered by meta boxes defined by other themes or plugins. It is not the case that other meta boxes will change the definition of the meta box; rather, the other meta boxes may be competing for a similar position and, depending on a number of factors, it is uncertain which meta box will get the desired position. To help understand where the meta boxes will be displayed, please refer to the following table. The table shows all 12 combinations of the $context and $priority arguments. The “Display Position” column refers to where the meta box will be shown assuming that only the default meta boxes are being displayed and that all of the default meta boxes are selected to “Show on screen” in the “Screen Options” panel.

$context $priority Display Position
advanced default Below “Revisions”
high Below “Revisions”
low Below “Revisions”
core Below “Revisions”
normal default Below “Revisions”
high Above “Excerpt”
low Below “Revisions”
core Below “Revisions”
side default Below “Featured Image”
high Above “Publish”
low Below “Featured Image”
core Below “Featured Image”

One can see that much of this seems redundant. For instance, if $context is set to “advanced”, it will always display below the “Revisions” meta box. An astute reader may wonder what the purpose of having twelve argument combinations if it only results in four different positions. These arguments become important once you introduce other meta boxes into the write screen. Within a context, one meta box may be displayed higher or lower than another meta box depending on the $priority argument. For instance, a meta box with $context set to “side” and $priority set to “high” will display higher than a meta box with $context set to “side” and $priority set to “low”.

While the “advanced” and “normal” $context values appear to display meta boxes in the same area, the “normal” $context value will always place meta boxes above meta boxes with “advanced” as the $context value, regardless of the $priority argument. In the event that two meta boxes are in the same context, the location of meta boxes will be determined by the $priority argument as previously discussed. The order of the $priority argument, for highest to lowest is: “high”, “core”, “default”, “low”.

Finally, if two meta boxes have identical $context and $priority, the meta boxes will be arranged alphanumerically according to the $id argument, with “a” and “0″ being highest and “z” and “∞” being the lowest.

Note that you can put as much time and effort as you want into placing a meta box, yet, a user can take advantage of the drag and drop interface to move it wherever the user desires. Additionally, if you would like to have the meta box in the side area, a user can elect to switch to the single column mode and your meta box will be moved.

Usage Tips:

  • $priority values (highest to lowest): “high”, “core”, “default”, “low”
  • $context values: “advanced”, “normal”, “side”
  • “normal” is higher than “advanced”
  • Alphanumeric sort of $id will disambiguate two meta boxes with identical $context and $priority values
  • Users can always move the meta box wherever they want so do not assume your meta box will be where you want it to be

$callback_args

The final argument to discuss is the $callback_args argument. The value of this argument can take either a string or an array. It represents data that can be passed to the callback function that is defined by the $callback argument. In order to access this data, two arguments must be specified for the callback function. The first argument contains the standard WordPress post object and the second argument will contain the an array of data which includes the data explicitly passed to the callback function via the $callback_args parameter.

By default, whether data is passed to the callback function using $callback_args or not, the second argument of the callback function will be an array containing the following keys:

  • id: $id argument specified for the add_meta_box function
  • title: $title argument specified for the add_meta_box function
  • callback: $callback argument specified for the add_meta_box function
  • args: $callback_args argument specified for the add_meta_box function

The data passed to the $callback_args argument can be accessed using the “args” key of the second argument specified in the callback function. This key and value is merged with the existing array that contains the “id”, “title”, and “callback” values for the current meta box. Even if no $callback_args value is specified, the default data can be accessed via the second argument of the callback function. To understand how this data is accessed, consider the following code snippet.

add_action('add_meta_boxes', 'wr_call_meta_box', 10, 1);
function wr_call_meta_box($post_type, $post)
{
	$args = array('test', array('some data', 'in here'), 3);
	add_meta_box(
		'weather_box',
		__('Current Weather', 'wr_plugin'),
		'wr_display_meta_box',
		'post',
		'advanced',
		'default',
		$args
	);
}

function wr_display_meta_box($post, $args)
{
	echo '<pre>';
	print_r($post);
	print_r($args);
	echo '</pre>';
}

Within the meta box, the $post object would be printed and should look something like:

stdClass Object
(
    [ID] =&gt; 1
    [post_author] =&gt; 1
    [post_date] =&gt; 2011-07-22 01:37:29
    [post_date_gmt] =&gt; 2011-07-22 01:37:29
    [post_content] =&gt; Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!
    [post_title] =&gt; Hello world!
    [post_excerpt] =&gt;
    [post_status] =&gt; publish
    [comment_status] =&gt; open
    [ping_status] =&gt; open
    [post_password] =&gt;
    [post_name] =&gt; hello-world
    [to_ping] =&gt;
    [pinged] =&gt;
    [post_modified] =&gt; 2011-08-06 07:30:39
    [post_modified_gmt] =&gt; 2011-08-06 07:30:39
    [post_content_filtered] =&gt;
    [post_parent] =&gt; 0
    [guid] =&gt; http://localhost:8888/wordpress-3.2/?p=1
    [menu_order] =&gt; 0
    [post_type] =&gt; post
    [post_mime_type] =&gt;
    [comment_count] =&gt; 1
    [ancestors] =&gt; Array
        (
        )

    [filter] =&gt; edit
)

Regardless if this information is needed or not, the $post object is always passed to the callback function. The output above shows everything that is contained in the object that can be utilized in the meta box. Usually, it is necessary to have the current post’s ID value to access meta data to manipulate. That datum, among other data, can be accessed through the $post object. Just remember, the $post object will always be the first argument sent to the callback function. If you want to access your $callback_args values only, remember to access the second argument, not the first.

In addition to the $post object output displayed above, the output for the $args value is also displayed. It should look like the following:

Array
(
    [id] =&gt; weather_box
    [title] =&gt; Current Weather
    [callback] =&gt; wr_display_meta_box
    [args] =&gt; Array
        (
            [0] =&gt; test
            [1] =&gt; Array
                (
                    [0] =&gt; some data
                    [1] =&gt; in here
                )

            [2] =&gt; 3
        )

)

As one can see, the $callback_args value is appended to the end of the $args array and is accessible via the “args” keys. The data can then be used as desired in the meta box.

Usage Tips:

  • The $callback_args is only accessible via the second argument of the callback function
  • The $callback_args is stored in the “args” key
  • $callback_args can be a string or array
  • Do not forget that the first argument is the $post object

    • It is not necessary to send any information contained in this object via the $callback_args argument
    • Do not try to access the $callback_args argument via the first argument of the callback function

Using add_meta_box

With a proper introduction to the add_meta_box function’s argument, it is time to bring it all together to create a meta box. While I will be using the code that I previously used to demonstrate the $callback_args argument, I will go through it piece by piece to explain what is happening. In this example, I will be creating a very simple weather meta box that allows users to attach weather data to posts.

Initiating the Call to add_meta_box

To begin with, the call to add_meta_box is made inside a function that will be used to hook into the “add_meta_boxes” function.

function wr_call_meta_box($post_type, $post)
{
	add_meta_box(
		'weather_box',
		__('Current Weather', 'wr_plugin'),
		'wr_display_meta_box',
		'post',
		'advanced',
		'default'
	);
}
add_action('add_meta_boxes', 'wr_call_meta_box', 10, 2);

Inside the wr_call_meta_box, the call to add_meta_box is made. Note the naming of the function that calls the add_meta_box function. First of all, it is being added to the global namespace, so it is prudent that it is prefixed. Since I am creating a weather report meta box, “wr” seems fitting as a prefix. Next, I add “call” to the name. Since you need a total of three functions (the save function will be discussed later in the article) to make meta box function, it is recommended that you set a naming convention that helps keep all of the functions organized. After adding the prefix, I tend to use a word that explains the purpose that the function serves in the overall meta box creation process. As such, I use “call” for this function because it is the function that “calls” the add_meta_box function. This convention is obviously my preference. Whether you like it or not, it would be wise to have some convention that works for you, especially when you find yourself adding multiple meta boxes within a plugin. Finally, I use “meta_box” to finish the function name. I do this to show that it is involved in the meta box creation process.

Within the wr_call_meta_box function, the call to add_meta_box is made. As one can see, this creates a meta box shown on the post screen with a title of “Current Weather” that is displayed in the bottom most area under the “Revisions” meta box. The wr_display_meta_box function will be responsible for building the HTML that will comprise the meta box.

The last function performed in this snippet is hooking the function to the appropriate WordPress action. For the purpose of adding meta boxes, the “add_meta_boxes” hook should be utilized. As such, it is the first parameter of the add_action function used to start the process. The next parameter for the the “add_action” function defines the callback function that adds the meta box. “10″ is the value of the next parameter that defines the priority of the call to the function. “10″ is the default value for this parameter and since there is no reason to have this function run earlier or later that the default, it is left as “10″. The final parameter of the add_action function specifies how many arguments are sent to the callback function. WordPress sends two arguments to the callback function for the “add_meta_boxes” hook and this final parameter allows those two parameters to be sent.

Regarding the parameters sent to the callback function, the first parameter is the name of the post type. For instance, when the code displayed above is executed on the post write screen, the value of the $post_type would be “post”, on the page write screen it would be “page”, and on a “Review” custom post type write screen, it would be “review”. The attentive reader will notice that since the $post_type is sent to the callback function and a “post-type” variable is used for the $page argument of the add_meta_boxes function, the $post_type parameter can be used for the $page argument. Yes, this can be done; however, this will add the meta box to every write screen, including the built in write screens (pages, posts, and links) and all custom post types. This method is a simple way to add a meta box to all write screens without having to make multiple calls to the add_meta_box function. The following code would add the weather meta box to all write screens.

function wr_call_meta_box($post_type, $post)
{
	add_meta_box(
		'weather_box',
		__('Current Weather', 'wr_plugin'),
		'wr_display_meta_box',
		$post_type,
		'advanced',
		'default'
	);
}
add_action('add_meta_boxes', 'wr_call_meta_box', 10, 2);

The second parameter sent to the callback function is $post. It contains the current post object. Printing this value to the screen reveals that this object contains the following information:

stdClass Object
(
    [ID] =&gt; 1
    [post_author] =&gt; 1
    [post_date] =&gt; 2011-07-22 01:37:29
    [post_date_gmt] =&gt; 2011-07-22 01:37:29
    [post_content] =&gt; Welcome to WordPress. This is your first post. Edit or delete it, then start blogging!
    [post_title] =&gt; Hello world!
    [post_excerpt] =&gt;
    [post_status] =&gt; publish
    [comment_status] =&gt; open
    [ping_status] =&gt; open
    [post_password] =&gt;
    [post_name] =&gt; hello-world
    [to_ping] =&gt;
    [pinged] =&gt;
    [post_modified] =&gt; 2011-08-06 07:30:39
    [post_modified_gmt] =&gt; 2011-08-06 07:30:39
    [post_content_filtered] =&gt;
    [post_parent] =&gt; 0
    [guid] =&gt; http://localhost:8888/wordpress-3.2/?p=1
    [menu_order] =&gt; 0
    [post_type] =&gt; post
    [post_mime_type] =&gt;
    [comment_count] =&gt; 1
    [ancestors] =&gt; Array
        (
        )

    [filter] =&gt; edit
)

The only change made in this snippet was to utilize the $post_type parameter as the $page argument for the add_meta_box function. While this may be tempting to utilize as a quick and easy way to add meta boxes, I would caution you to be more specific about which pages you add the meta boxes to. Not all meta boxes make sense in all write screens. Furthermore, if a user defines a custom post type and you use this method to add meta boxes to all write screens, that newly defined custom post type will also get the meta box whether or not it is meant to be there. Use this method with caution!

Before moving on to the function that produces the HTML for the meta box, it is important to discuss the “add_meta_boxes” hook. While other hooks can be used to add the meta boxes, it important that the “add_meta_boxes” hook be used for this purpose, not other hooks, which are, unfortunately, widely used for this purpose. Most prominently, “admin_init” is commonly used to execute the function that calls the add_meta_box function. It will work; however, the purpose of the “add_meta_boxes” hook is specifically for these functions that add the meta boxes. There are two reasons to use the “add_meta_boxes” hook, as opposed to other hooks. The first reason is that if you use another hook, the callback function attached to the hook may be executed when it is unnecessary. This causes unnecessary code to be executed and can lead to unexpected (and very difficult to debug) errors. The second reason is that the “add_meta_boxes” hook can be extended to be used for a specific custom post type. There is a variable hook version of the “add_meta_boxes” function that takes the name of the custom post type. For instance, if you refer back to the example post type “review” defined above, you can specifically target the “add_meta_boxes” hook for the “review” write screen only, by modifying the hook to be “add_meta_boxes_review”. The hook is generated in WordPress core using: 'add_meta_boxes_' . $post_type. Note that the post-type-name would be identical to that used above as the $page argument. Ideally, with a custom post type, the function that calls add_meta_box would be attached to the variable version of the “add_meta_boxes” hook. As an example, in order to add a meta box to only the “review” post type, a very solid way of executing this code only when absolutely necessary would to use the following snippet:

function wr_call_meta_box($post)
{
	add_meta_box(
		'weather_box',
		__('Current Weather', 'wr_plugin'),
		'wr_display_meta_box',
		'review',
		'advanced',
		'default'
	);
}
add_action('add_meta_boxes_review', 'wr_call_meta_box', 10, 1);

Take note that there is only one parameter being passed to the callback function when the variable hook is used. Instead of sending both the post type and the post object, only the post object is sent to the callback function when the variable “add_meta_boxes” hook is used. Now, to further reduce redundancy in the code, instead of hard coding “review” as the $page parameter, it can be added using the “post_type” parameter within the post object that is passed to the callback function. The resulting code would look like:

function wr_call_meta_box($post)
{
	add_meta_box(
		'weather_box',
		__('Current Weather', 'wr_plugin'),
		'wr_display_meta_box',
		$post-&gt;post_type,
		'advanced',
		'default'
	);
}
add_action('add_meta_boxes_review', 'wr_call_meta_box', 10, 1);

Instead of using the variable “add_meta_boxes” hook to call the function that produces the HTML for the meta box, this process can be started when a new post type is registered. Within the arguments array that the register_post_type accepts is an argument called “register_meta_box_cb”. The “register_meta_box_cb” argument works identically to the “add_meta_boxes” variable hook. In fact, it ultimately attaches the value set for “register_meta_box_cb” to the “add_meta_boxes” variable hook. As such, the callback function defined by the “register_meta_box_cb” argument will only receive the single post object argument that the variable “add_meta_boxes” hook sends. To initiate the adding of a meta box via register_post_type, you would do something like the following:


function wr_register_post_type()
{
	$labels = array(
		'name' =&gt; _x('Reviews', 'post type general name', 'wr_plugin'),
		'singular_name' =&gt; _x('Review', 'post type singular name', 'wr_plugin'),
		'add_new' =&gt; _x('Add New', 'review', 'wr_plugin'),
		'add_new_item' =&gt; __('Add New Review', 'wr_plugin'),
		'edit_item' =&gt; __('Edit Review', 'wr_plugin'),
		'new_item' =&gt; __('New Review', 'wr_plugin'),
		'all_items' =&gt; __('All Reviews', 'wr_plugin'),
		'view_item' =&gt; __('View Review', 'wr_plugin'),
		'search_items' =&gt; __('Search Reviews', 'wr_plugin'),
		'not_found' =&gt;  __('No reviews found', 'wr_plugin'),
		'not_found_in_trash' =&gt; __('No reviews found in Trash', 'wr_plugin'),
		'parent_item_colon' =&gt; '',
		'menu_name' =&gt; __('Reviews', 'wr_plugin')
	);
	$args = array(
		'labels' =&gt; $labels,
		'public' =&gt; true,
		'publicly_queryable' =&gt; true,
		'show_ui' =&gt; true,
		'show_in_menu' =&gt; true,
		'query_var' =&gt; true,
		'rewrite' =&gt; true,
		'capability_type' =&gt; 'post',
		'has_archive' =&gt; true,
		'hierarchical' =&gt; false,
		'menu_position' =&gt; null,
		'supports' =&gt; array('title','editor','author','thumbnail','excerpt','comments'),
		'register_meta_box_cb' =&gt; 'wr_call_meta_box'
	);
	register_post_type('review', $args);
}

add_action('init', 'wr_register_post_type');

I will not cover registering custom post types here as it has been covered extensively elsewhere. The important part of the above code is to notice how the “register_meta_box_cb” argument points to the previously defined wr_call_meta_box function. When this post type is registered, it also sets into motion events that will create the meta box in the appropriate write screen. It saves the trouble of having to use a separate call to add_action to set up the meta box. It also keeps all of the code for the individual post type in one place. Functionally, it does the exact same thing as the “add_meta_boxes” variable hook. It is just a different way to accomplish the same task.

Usage Tips:

  • Know the arguments sent to the callback function
    • “add_meta_boxes” sends:
      • $post_type: the name of the post type
      • $post: the post object
    • “add_meta_boxes_{$post_type}” sends:
      • $post: the post object
  • Use the arguments sent to the callback function
  • If possible, register the callback function for custom post types when registering the custom post type

Defining the Callback Function

Now that you know a few different ways to call the function that initiates the creation of the meta box, it is time to write the code that builds the actual meta box. Note that in all of the above code examples, the value for the $callback parameter has been “wr_display_meta_box”. This value refers to the function that generates the HTML for the meta box. Keeping with the previously described naming convention, I prefix the function with “wr” as it is being added to the global namespace. I then add “display” as its the function that “displays”" the HTML for the meta box. Finally, I stick with the “meta_box” term to finish the function name so I know that it belongs to the family of functions that create the meta box.

Without further ado, the following code snippet generates the HTML for the meta box.

function wr_display_meta_box($post, $args)
{
   wp_nonce_field(plugins_url(__FILE__), 'wr_plugin_noncename');
?>
   <p>
       <label for="wr-temperature"><?php _e('Temperature (&deg;F)', 'wr_plugin'); ?>: </label>
       <input type="text" name="wr-temperature" value="<?php echo get_post_meta($post->ID, 'wr-temperature', true); ?>" />
       <em><?php _e('Must be a numeric value', 'wr_plugin'); ?></em>
   </p>
<?php
}
?>

In this example, a simple text input field with a label and two hidden fields are generated. The resulting HTML for the meta box should look something like:

<div class="postbox ">
	<div class="handlediv"><br></div>
	<h3 class="hndle"><span>Current Weather</span></h3>
	<div class="inside">

		<p>
			<label for="wr-temperature">Temperature (&deg;F): </label>

			<em>Must be a numeric value</em>
		</p>
	</div>
</div>

Notice that the HTML defined in the callback function shows up inside the div with the aptly name class of “inside”. The rest of the HTML, including the div structure is automatically generated. An h3 element is created based on the desired title for the meta box and the meta box wrapper div is given an id equivalent to the $id argument of the add_meta_box function.

The first line of the wr_display_meta_box calls the wp_nonce_field function, which generates two hidden fields. These hidden fields help protect the form against cross-site scripting. It is recommended that when using forms within WordPress that you use the nonce functions for security purposes. The first argument of the wp_nonce_field function defines the “action” for the nonce field. When the function creates the “hidden” input, it will use this value to create the “value” attribute for the input. Later, when validating the form, this value will be checked against to make sure that the form coming from the correct place. Note that it is typical to use the something like the plugins_url function for this argument as it is an easy way to stay consistent with the action name when it is created and when it is validated. The next argument specifies the “name” and “id” attributes for the “hidden” input. In this case, we will call the “hidden” input “wr_plugin_noncename”. These two arguments create the following HTML:

<input type="hidden" id="wr_plugin_noncename" name="wr_plugin_noncename" value="24062433f6" />

The next argument in the wp_nonce_field function determines whether a “hidden” input field with the referrer information will be added or not. The value “true” is sent for this argument (which is the default), which will create the referrer “hidden” input. Later, when saving the meta box information, this value will be checked. It generates the following HTML.

<input type="hidden" name="_wp_http_referer" value="/wordpress-3.2/wp-admin/post-new.php" />

The final argument in the wp_nonce_field function determines whether the HTML generated will be returned by the function or echoed. The “true” value echoes the HTML. If you are unfamiliar with the built-in WordPress nonce functions, I would recommend reading about them in order to harden and secure your WordPress work.

You will recall that the callback function defined in the add_meta_box function will take two arguments: the post object and the custom arguments sent via add_meta_box. The ID variable in the $post object is utilized within the get_post_meta function to find and display the previous value of “wr-temperature” if it exists as post meta. The get_post_meta function finds a value in the database given a post ID and a meta key name. At this point, no values have been saved to the database, so this value will not be found and nothing will be printed. In the next section of this article, this value will be saved and the current function is being written in such a way that it will be prepared to handle the existing value when it is in the database.

To bring everything together at this point, the following code summarizes adding a meta box to the post write screen in the “advanced” area.

function wr_call_meta_box($post_type, $post)
{
   add_meta_box(
       'weather_box',
       __('Current Weather', 'wr_plugin'),
       'wr_display_meta_box',
       'post',
       'advanced',
       'default'
   );
}
add_action('add_meta_boxes', 'wr_call_meta_box', 10, 2);

function wr_display_meta_box($post, $args)
{
   wp_nonce_field(plugins_url(__FILE__), 'wr_plugin_noncename');
?>
   <p>
       <label for="wr-temperature"><?php _e('Temperature (&deg;F)', 'wr_plugin'); ?>: </label>
       <input type="text" name="wr-temperature" value="<?php echo get_post_meta($post->ID, 'wr-temperature', true); ?>" />
       <em>Must be a numeric value</em>
   </p>
<?php
}
?>

Saving Meta Boxes

The data gathered by the meta box needs to be added to the database. Typically, meta boxes will collect data that are associated with the post. As such, it is appropriate to add this data to the post as post meta data. To do so, a new function will be defined that will save the meta data using the update_post_meta function when the post is saved.

Hooking the Save Function

To begin, an appropriate hook needs to be identified that runs whenever the a post is saved. Fortunately, there is a hook called “save_post” that can be used for this purpose. The following code snippet sets up a function to be executed when the “save_post” hook is called.

add_action('save_post', 'wr_save_meta_box', 10, 2);

Examining the call to the add_action function reveals that the function wr_save_meta_box will be executed on the “save_post” action. Note that I have again followed the naming structure that uses “wr” as the prefix, followed by a word (“save”) that explains what the function is doing, and ends with “meta_box”, which associates it with the family of functions that handle the meta box. The remaining two arguments for the add_action function specify that it will run in the default order (“10″) and that two (“2″) arguments are passed to the wr_save_meta_box function. The callback function for the “save_post” action is sent the ID for the post and the post object itself. This data can be used to help process data collected by the meta box.

The Save Function

While “save_post” is the hook to use in this scenario, it is important to note a few details about this hook before you use it. There are some counterintuitive aspects regarding when this hook is run. First of all, you must understand that when you visit the “post-new.php” page (i.e., the “Add New” link under the “Posts”, “Pages”, or any custom post type menu item), functions associated with the “save_post” hook are called. Indeed, even though you are merely only visiting the write screen and have not yet saved any data, the “save_post” hook is called. When you visit the “Add New” write screen, a post with status of “auto-draft” is created. It is a completely blank post that is a placeholder for the post you are about to create. To illustrate this point, add the following code to your “functions.php” of your current theme or within a new plugin.

add_action('save_post', 'wr_save_meta_box', 10, 2);

function wr_save_meta_box($post_id, $post)
{
	var_dump($post_id);
}

Simply put, this function will output the ID of the current post to the screen each time that the “save_post” took is executed. In certain situations, this will cause errors, but the point is that the functions associated with the “save_post” hook are executed at times that you might not intuitively expect. As such, it is prudent to add some code to check for specific context when utilizing the “save_post” hook. For instance, there is no need to try to save the post meta for the weather meta box when a user first accesses the “Add New” write screen. As an aside, to help catch these types of errors in the code, I recommend developing with the WP_DEBUG constant set to “true”. You can find this variable in the “wp-config.php” file.

It is necessary then that the routine within the wr_save_meta_box function is not executed if not in the right context. How is this accomplished? Three things need to be checked when this function is executed:

  • Is the current context an “auto save”
  • Does the user have the capabilities to edit posts
  • Has the appropriate nonce field been set and is its value correct

Generally speaking, it is not necessary to save meta data during auto save routines. Meta data is not maintained in the revision history and thus, it is not important that it is saved during the auto save for the post. To accomplish the goal of exiting the function on auto save, we begin the wp_save_meta_box function by adding a check for the auto save condition

function wr_save_meta_box($post_id, $post)
{
	if(defined('DOING_AUTOSAVE') &amp;&amp; DOING_AUTOSAVE)
      return;
}

When an auto save is initiated, WordPress core sets the constant DOING_AUTOSAVE to true. This variable is set during a function that is executed when the auto save routine is initiated by an AJAX action. The purpose of this variable is to make the rest of the scripts aware that an auto save is being performed and the routines should be thusly altered. In the previous function, AUTO_SAVE is detected and if it is set, the function returns nothing. Therefore, none of the actual save routines within the function will be executed.

The next step is to check the capabilities of the current user to determine if the current user is able to perform the action. Adding this section to the function looks like:

function wr_save_meta_box($post_id, $post)
{
	if(defined('DOING_AUTOSAVE') &amp;&amp; DOING_AUTOSAVE)
    	return;

    if(!current_user_can('edit_post', $post_id))
    	return;
}

Simply put, this part of the function utilizes the current_user_can function to determine if the current user has the capability to perform the intended action (i.e., “edit_post”) for the current post (i.e., $post_id. This snippet shows the general form of this check, as well as demonstrates the basic concept; however, a few more situations need to be considered before this is completed. Note that this snippet checks for the “edit_post” capability. What if the current context is saving a page or a custom post type? Interestingly, if the user can “edit_post”, s/he will pass this check even if it is called in the context of a page or a custom post type. If your intent is to have every user who has the “edit_post” capability update this meta data in any context, the code above is sufficient; however, should you wish to have the ability to save post meta data more closely tied to post type, you can check for the context prior to making the call to current_user_can. To check specifically for posts and pages, the following snippet would be what you need.

function wr_save_meta_box($post_id, $post)
{
	if(defined('DOING_AUTOSAVE') &amp;&amp; DOING_AUTOSAVE)
    	return;

    if('page' == $_POST['post_type'])
    {
    	if(!current_user_can('edit_page', $post_id))
    		return;
    }
    else
    	if(!current_user_can('edit_post', $post_id))
    		return;

}

This function checks if the current context is a page. If it is, then it tests if the current user can “edit_page”. If it is not a page, it instead checks if the current user can “edit_post”. Note that there is no condition to check for a custom post type. In fact, if the current context of this function is a custom post type, the function would test if the current user can “edit_post”. In the sample code for defining a custom post type that I provided earlier in the article, I set the “capability_type” to “post”. What this means is that all of the capabilities that are associated with posts are associated with the “review” custom post type. As such, the wr_save_meta_box function would, assuming that the current user has the “edit_post” capability, determine that the user has the proper permissions to edit the “review” custom post type. Should you want to define a set of capabilities specifically for a custom post type, you would need to make sure that you test specifically for those capabilities in this function to ensure that it runs properly for the right users.

Finally, we need to check that we are in the right context with regard to the meta box and its form fields being present. We can guarantee the right context if the nonce field that was generated previously is checked for before executing the code. When discussing the nonce field earlier in the article, I discussed its security benefit; however, it has the added benefit of controlling the context in which the routines within the wr_save_meta_box function are executed. With that said, the following code represents an updated wr_save_meta_box function that checks for its context and adds updates the post meta value.

function wr_save_meta_box($post_id, $post)
{
	if(defined('DOING_AUTOSAVE') &amp;&amp; DOING_AUTOSAVE)
    	return;

    if('page' == $_POST['post_type'])
    {
    	if(!current_user_can('edit_page', $post_id))
    		return;
    }
    else
    	if(!current_user_can('edit_post', $post_id))
    		return;

 	if(isset($_POST['wr_plugin_noncename']) &amp;&amp; wp_verify_nonce($_POST['wr_plugin_noncename'], plugins_url(__FILE__)) &amp;&amp; check_admin_referer(plugins_url(__FILE__), 'wr_plugin_noncename'))
 	{
 		if(is_numeric($_POST['wr-temperature']))
 		{
	 		update_post_meta($post_id, 'wr-temperature', $_POST['wr-temperature']);
	 	}
	}
	return;
}

The function first checks to see if the “wr_plugin_noncename” key within the $_POST array is set. If it is not set, this means that the “Add New” or the “Edit” write screen has not been submitted. This check is not enough, however, as it could be set as part of an cross-site scripting attack. As such, the value of the nonce field needs to be checked. The wp_verify_nonce function checks to make sure that the value of the nonce field is as expected. It takes the value of the nonce field as its first argument and the “action” value that was sent to the wp_nonce_field as the second argument. If the actual value (i.e., $_POST['wr_plugin_noncename']) of the nonce field is equivalent to the expect value for the action (i.e., plugins_url(__FILE__)), the function returns true.

It is also important to check that the referrer is the expected value. In other words, we need to check that the form was submitted from where we expected it to be submitted. The check_admin_referer (sic) function is used for this purpose (Note: while the proper spelling of the term is “referrer”, the HTTP standard that defines the term spells the word “referer”. As such, WordPress has used the latter spelling convention. Do not forget to misspell the word!). This function takes the “action” as the first argument and the nonce field name as the second argument. It tests to ensure that the referring page is the expected page. If any of these checks fail, the update_post_meta function is not executed. If both checks pass, the update_post_meta function is executed.

(Note: The check_admin_referer function will run the wp_verify_nonce function within it. As such, it is redundant to use both wp_verify_nonce and check_admin_referer together as demonstrated above; however, I am including both in this article to thoroughly cover what each will do to help inform readers about the purpose of each of these functions. Ideally, I would only use check_admin_referer in this case.

Once the origin of the request has been verified, the data is validated and submitted to the database. Because the input field states that it will only accept numeric values, we will simply test to make sure that the values are numeric using the PHP function is_numeric before updating the value. If the value is numeric, the value will be added using the update_post_meta function. The update_post_meta function accepts four arguments: $post_id, $meta_key, $meta_value, and $prev_value. The $post_id argument takes the ID of the post that the meta data will be associated with. The $meta_key argument specifies the key that will be used to identify the piece of data. The piece of data itself is specified by the $meta_value argument. Finally, the $prev_value (not used in the function above), is an optional argument that contains the previous data value. If it is set, it will change the table row where the “meta_key” column is the same as the $meta_key argument and the “meta_value” column is equivalent to the $meta_value argument.

Having completed the wr_save_meta_box function, the meta value can now be saved! To summarize, the following code will define a meta box and save it when a post is updated.

function wr_call_meta_box($post_type, $post)
{
   add_meta_bo
       'weather_box',
       __('Current Weather', 'wr_plugin'),
       'wr_display_meta_box',
       'post',
       'advanced',
       'default'
   );
}
add_action('add_meta_boxes', 'wr_call_meta_box', 10, 2);

function wr_display_meta_box($post, $args)
{
   wp_nonce_field(plugins_url(__FILE__), 'wr_plugin_noncename');
?>
   <p>
       <label for="wr-temperature"><?php _e('Temperature (&deg;F)', 'wr_plugin'); ?>: </label>
       <input type="text" name="wr-temperature" value="<?php echo get_post_meta($post->ID, 'wr-temperature', true); ?>" />
       <em>Must be a numeric value</em>
   </p>
<?php
}

add_action('save_post', 'wr_save_meta_box', 10, 2);
function wr_save_meta_box($post_id, $post)
{
   if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
       return;

   if('page' == $_POST['post_type'])
   {
       if(!current_user_can('edit_page', $post_id))
           return;
   }
   else
       if(!current_user_can('edit_post', $post_id))
           return;

   if(isset($_POST['wr_plugin_noncename']) && wp_verify_nonce($_POST['wr_plugin_noncename'], plugins_url(__FILE__)) && check_admin_referer(plugins_url(__FILE__), 'wr_plugin_noncename'))
   {
       if(is_numeric($_POST['wr-temperature']))
       {
           update_post_meta($post_id, 'wr-temperature', $_POST['wr-temperature']);
       }
   }
   return;
}
?>

With that we have added a meta box and saved its data to the post meta table.

Usage Tips:

  • Within a function called by the “save_post” hook:
    • Check for DOING_AUTOSAVE
    • Check user capabilities
    • Use the nonce field to check for context and for security
  • Remember that “save_post” is executed at times you might not expect; check for the context!

Conclusion

The present tutorial has thoroughly outlined how to add a meta box to any write screen within WordPress. All of the necessary functions have been discussed and usage recommendations and strategies have been outlined. Using these basic concepts, you can now create any meta box that you desire, from simple weather meta boxes to more complex meta boxes that handle significant amounts of data. The only thing limiting the possibilities is your creativity.

[author id="zacktollman"]

Tags: , , ,

zack

Zack Tollman is a freelance web developer specializing in PHP development for the WordPress platform. Learn more about him at zackdev.com or follow him on twitter.

30 Responses

    • zack August 26, 2011 at 8:22 pm

      Glad you enjoyed it Andrew! WP Roots has been particularly mind blowing and I’m glad I can contribute to that tradition.

    • zack August 27, 2011 at 6:40 pm

      Thanks for the compliment AJ!

      Thanks for mentioning Rilwis’ “Meta Box Script”. I had not seen it before. It seems like a lot of people are beginning to look around for easier solutions for adding meta boxes. The WordPress meta box API is not at all difficult, but if you have a write screen that needs 5 meta boxes, which is not uncommon for a custom post type, the API is redundant and time consuming.

      There are at least three other API’s/classes that I am aware of that try to tackle the meta box issue:

      Konstanin Kovshenin’s Post Options API: https://github.com/kovshenin/post-options-api
      Nikolay Yordanov’s Smart Meta Box: http://www.wproots.com/ultimate-guide-to-meta-boxes-in-wordpress/
      Dimas Begunoff’s WPAlchemy MetaBox PHP Class: http://www.farinspace.com/wpalchemy-metabox/

      This is what’s so great about open source! There are at least 4 great options for enhancing the meta box feature already available. And, if you don’t like any of them, you can build your own!

  1. AJ August 27, 2011 at 10:00 pm

    Thanks for sharing those other links! I will definitely check them out. Although I have customized the one I showed you to a point that I am really happy with all the custom functions and color picker settings that I’ve built in.

    Great job on the tutorial again…and amazing to see you respond so quickly to comments!

    Reply
    • zack September 4, 2011 at 9:38 pm

      Unfortunately, I’m not aware of any way to do this. I just looked at the code that generates that part of the write screen and there is no hook that will allow you to insert code into that area. A hacky way to handle this would be to generate the meta box using the method(s) outlined above, then use some javascript to maneuver the meta box above the title.

  2. Alessio September 10, 2011 at 7:38 am

    I have a problem saving custom fields, anyone can help?

    add_action( ‘add_meta_boxes’, ‘custom_field_tetti’ );

    function custom_field_tetti(){
    add_meta_box(“specifiche_tetti_meta”, “Specifiche Tetti”, “specifiche_tetti”, “tetti”, “normal”, “high” );
    }

    function specifiche_tetti(){
    global $post;
    $custom_seriale = get_post_custom($post->ID);
    $tetti_seriale = $custom_seriale["tetti_seriale"][0]; ?>
    Seriale:
    <input name="tetti_seriale" value="”/>

    ID, “tetti_seriale”, $_POST["tetti_seriale"]);
    }

    Reply
    • zack September 13, 2011 at 2:21 am

      Alessio,

      You need to define a function and call it on the “save_post” hook to save data from the meta boxes. Please read the section of the article titled “Saving Meta Boxes” to get started!

      If you are still having issues, please post back and hopefully I can point you in the right direction.

  3. Dave October 19, 2011 at 3:20 pm

    Hi Zack,

    A fantastic post, with almost everything I need :)

    Have you any ideas on passing error messages back to the form? I want to guide users to the field they’ve made an error in rather than returning and hoping they can work it out.

    Thanks in advance

    Reply
    • Palvi July 23, 2014 at 6:30 pm

      Ρe si 12:10 Sta sinedria painosmgos se ola ta meri tu kosmu o politikos politizmos epivali na proskalis ola ta komata apo olo to politiko fasma.Tora an esis iste toso ma toso ikani pu mberdhevete ta sovrako me ti gravata den sas fteei kanis.Pandos i elipsi psihremias ine poli kakos simvulos pistepste me.Den sas fteei kanis eki sto Mega moni sas ta kanate thalasa.Apla ke katanoita mbleksate sta dihtia tu Berisa gia na vgalete kana frango ke sas ekane kuluvahato.Psihremia pedia opos strosate tha kimithite toso apla.

  4. vi November 26, 2011 at 9:26 pm

    Thanks for the tutorial! I’m using multiple metaboxes in my edit post screens. I can;t figure out how to retrieve the field names and values of a specific metabox in my posts. Do you have any idea?

    Reply
  5. seron August 20, 2012 at 1:37 pm

    I was a bit daunted by the mass of text in this post, but reading it through I found it of unusual quality, clarity and conscientiousness. It was very rewarding and enlightening to read. Many question marks were set straight for me while reading. For example, the part about when AUTOSAVE is used, which many tutorials check for in code but don’t elaborate on, or the discussion about capabilities check for different post types. Awesome!

    I think your writing style is exceptional and you should write a book, if you haven’t already done or considered doing so.

    Reply
  6. kenny September 6, 2013 at 8:17 pm

    This tutorial was awesome. Exactly what I’ve been searching for all day to clear up the process of creating metaboxes. THANKS!

    Reply
  7. binary option trading September 10, 2013 at 6:41 pm

    Pretty section of content. I just stumbled upon your web site and in accession
    capital to assert that I get actually enjoyed account your blog posts.
    Anyway I’ll be subscribing to your augment and even I achievement you access consistently rapidly.

    Reply
  8. JFrankParnell December 12, 2013 at 7:59 pm

    Great post, killer explanations. I’m looking for a way to stick a couple options into existing wp metaboxes. I could do my own metabox, as explained here, but for one little option, I’d rather hook into the existing page attributes box. I found this hook: page_attributes_dropdown_pages_args but that obv doesnt do it, adn I dont see any other hooks in the function.

    Reply
  9. london dj March 19, 2014 at 8:35 am

    Asking questions are actually pleasant thing if you are
    not understanding anything completely, however this piece of writing offers pleasant understanding yet.

    Reply
  10. Fidelia June 10, 2014 at 4:15 am

    You’re so awesome! I do not believe I’ve read anything like
    this before. So great to discover someone with a few unique thoughts
    on this subject. Seriously.. thank you for starting this up.

    This web site is something that is needed on the internet, someone with a little originality!

    Reply
    • Reginald July 22, 2014 at 11:12 pm

      I wanted to use your sun.png while lenraing how to do it as callback function. However, your sample project does not include sun.png file. please advice.

  11. wild oyster mushrooms for sale June 23, 2014 at 12:01 am

    Different types of oyster mushrooms include yellow oyster, pink oyster, elm
    oyster and blue oyster mushrooms. Probably you have your own tricks or techniques
    in cultivating oyster mushrooms on logs, feel free to comment and to share them with us,
    because only this way we can help each other in order to succeed.
    Oyster Mushrooms are great because they can pretty much grow anywhere.

    Reply
  12. Nick Soper July 6, 2014 at 10:14 am

    Thanks for the awesome tutorial. Thought I’d let you know there is a small error in your last code example (final function code).

    Line 03 is “add_meta_bo” and it should be “add_meta_box(”

    the x( is clipped off.

    Reply
  13. internet download manager September 23, 2014 at 9:18 am

    that you are super LUCKY and (2) total and compldte BELIEF.
    This saves the user the effort too click on individual link
    to download them. With the Qik Video Camera for Nokiia
    N73 softwaree you can choose to save your location and as you share the video it is automatically saved on the Qik website therefore iit can be viewed later on your Nokia N73 phone
    or ykur PC or Mac.

    Reply
  14. business opportunity October 21, 2014 at 8:16 pm

    You will have to give extreme importance to your products and
    promotion tactics. Learning the tricks of the
    trade is a crucial aspect of the game. Traditional strategies have always been associated with an immense budget and market research requirements.

    Reply

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>