In a previous article, we discussed how to design a class representing a WordPress admin page. This was important because almost every plugin or theme needs an admin page. But this isn’t the only common thing that plugins or themes add to the WordPress admin.
Another one of those is the meta box. If you’re not familiar with meta boxes, they’re the draggable boxes that you see on the post editing screen. It’s quite common for plugins and themes to add post-specific functionality through them.
This makes them a good topic to discuss with object-oriented programming. (Is there really a bad topic to discuss with object-oriented programming!?) And, as we’ll see, meta boxes have a lot of in common with admin pages. This means that designing a class to represent them will be a lot like what we did for admin pages.
What makes a meta box?
A meta box like an admin page has two elements to it. The first one is the registration process. We need to register a meta box with WordPress if we want it displayed anywhere. We also need a way to render the HTML of the meta box.
Registering a meta box
Like with admin pages, this registration process centres around a single function. That function is the add_meta_box
function. But, unlike its admin page counterpart, this function is a lot simpler to use.
That’s because, while the add_meta_box
function has a lot of parameters, most are optional. The only required parameters are: id
, title
and callback
. screen
, context
, priority
and callback_args
are the other four parameters, and they’re optional.
Required parameters
id
is the ID used by WordPress to store the meta box in the wp_meta_boxes
global variable. WordPress also uses id
for the id
HTML attribute. This id
should be unique to your meta box. Or at the very least unique for the screen
that you’re registering it to. (We’ll talk about that when we discuss the screen
parameter.)
Meanwhile, the other two required parameters are pretty self-explanatory. title
is the title that you’ll see above the meta box on the admin screen where WordPress will display it. And callback
is the callable used by WordPress to render the inside of the meta box.
Optional parameters
For the optional parameters, we have the screen
parameter. This is the screen or screens where you want WordPress to display the meta box. The add_meta_box
function accepts screen
arguments that are either a string ID, an array of string IDs or a WP_Screen
object.
A screen ID is often the slug of the admin page where you want your meta box to appear. By default, if you leave it empty, it will use the screen returned by the get_current_screen
function. In general, the get_current_screen
function will return the WordPress post editing screen.
As we mentioned earlier, the wp_meta_boxes
global array is a multidimensional array. It uses both the screen ID and the id
argument that you pass to the add_meta_box
function as keys. This means that, in theory, you can use the same id
for two different meta box as long as you use two different screen
values.
Meanwhile, context
determines where WordPress should display the meta box on the given screen
. This value changes depending on the screen
value. That said, both normal
and side
are values that work on most screens.
priority
determines the priority of the meta box within the given context
. There are four possible values for priority: high
, core
, default
and low
. But, in practice, you only want to choose between high
and low
if you’re going to specify one. high
will put the meta box at the top of the list of meta boxes for the given context
while low
will place it at the end.
The last optional parameter is callback_args
. You use this to pass custom meta box data. WordPress will then pass that data as the second argument of your callback
callable.
When do you register a meta box?
It’s also worth pointing out that we can’t call the add_meta_box
function whenever we want. We need to use the plugin API to call it at the right time. But, lucky for us, WordPress has hooks specifically to add meta boxes.
The main one is the add_meta_boxes
hook. WordPress triggers it right after it finishes registering all the built-in meta boxes. It passes two arguments to the registered callback: post_type
and post
.
post_type
is the post type of the post that WordPress is registering meta boxes for. Meanwhile, post
is the WP_Post
itself. This can be useful if you need more information than just the post type.
There’s also the add_meta_boxes_{post_type}
hook. It’s useful if you don’t want WordPress to call your callback for every post type. Just replace {post_type}
with the post type that you want to register a meta box for. WordPress will then only call your callback for that post type.
Rendering the HTML of a meta box
The other element of a meta box is the rendering of the HTML inside of it. This is the role of the callback
that we pass to the add_meta_box
function. This problematic because WordPress doesn’t have a clean way to generate HTML.
The good thing is that this is a problem that we’ve explored before. We saw that we could create a small templating system that we could use in our classes. This is also what we’ll use here in a modified form. (But no need to read the article, we’ll go over it again.)
Building our meta box class
Now that we’ve done an overview how meta boxes work with WordPress, we can start building a class for them. As usual, we’ll start with an empty class. We’ll name it MyPlugin_MetaBox
.
class MyPlugin_MetaBox { }
Registering our meta boxes
To begin, we need a way to register our MyPlugin_MetaBox
class as a WordPress meta box. This means that we need a way to convert our MyPlugin_MetaBox
class into something the add_meta_box
function understands. There are a few ways for us to do this, but there are two of them that come up more often.
The first one is to create one method for each argument of the add_meta_box
function. The other is to pass all our arguments to the add_meta_box
function using the call_user_func_array
function. Both have their advantages and disadvantages so let’s take a second to look at them.
Passing arguments using the “call_user_func_array” function
Let’s start by talking about the call_user_func_array
function. It’s possible that you’ve never heard of this function before, but, don’t let that fool you, it’s quite useful. In fact, the plugin API couldn’t work without it.
The function has two parameters: callback
and param_arr
. When you call the call_user_func_array
function, it’ll make a call to another function or method using the two arguments that you pass to it. The function that it’ll call will be the one defined by callback
. (This means that callback
needs to be callable.) And the arguments that it passes to the callback
will be the ones in the param_arr
array.
In this situation, we know what the callback
is. It’s the add_meta_box
function that we want to use to add our meta box. For the call_user_func_array
function to use it, we just need to pass it a string with add_meta_box
. (PHP considers this string to be a callable since it’s the name of a function that it can execute.)
The param_arr
argument is where our MyPlugin_MetaBox
class comes into play. We need to create a method that will return the array that we want to pass to the call_user_func_array
function. We can call it the get_meta_box_arguments
method.
class MyPlugin_MetaBox { // ... /** * Get the arguments for the "add_meta_box" function. * * @return array */ public function get_meta_box_arguments() { return array( 'meta-box-id', 'Meta Box Title', array($this, 'render_meta_box'), ); } }
Now, here’s our updated MyPlugin_MetaBox
class. We added the get_meta_box_arguments
method to it. It returns an array with three elements in it. Those three elements represent the three required parameters of the add_meta_box
function.
meta-box-id
is theid
of the meta box.Meta Box Title
is thetitle
of the meta box.array($this, 'render_meta_box')
is thecallback
that WordPress will use to render the inside of the meta box.
function myplugin_add_meta_box() { $meta_box = new MyPlugin_MetaBox(); call_user_func_array('add_meta_box', $meta_box->get_meta_box_arguments()); } add_action('add_meta_boxes', 'myplugin_add_meta_box');
And here’s how you would register that meta box using the add_meta_boxes
hook that we saw earlier. The myplugin_add_meta_box
function first initializes a new instance of our MyPlugin_MetaBox
class. We then call the call_user_func_array
function using the array returned by the get_meta_box_arguments
method as the second argument.
Passing arguments using one method per argument
The other option is to do the reverse of what we just did and have one method per add_meta_box
argument. This will increase the size of our MyPlugin_MetaBox
class since we’ll have up to seven methods instead of one. That said, the advantage is that it makes our integration a bit more flexible. It’s also easier to understand since it doesn’t rely on the call_user_func_array
function.
function myplugin_add_meta_box() { $meta_box = new MyPlugin_MetaBox(); add_meta_box($meta_box->get_id(), $meta_box->get_title(), $meta_box->get_callback(), $meta_box->get_screens(), $meta_box->get_context(), $meta_box->get_priority()); } add_action('add_meta_boxes', 'myplugin_add_meta_box');
Here’s our myplugin_add_meta_box
function in this context. We replaced our call to the call_user_func_array
function with one to the add_meta_box
function. And instead of a call to the get_meta_box_arguments
method, we now have calls to six different methods. One for each of the add_meta_box
arguments.
A small note for those that might have noticed. The code above doesn’t have a method for the callback_args
parameter. (That’s the 7th and last parameter in the add_meta_box
function.) This is just to make the example a bit simpler. (Six methods is already a lot!) But, if you need it in your project, feel free to add it!
class MyPlugin_MetaBox { /** * Get the callable that will the content of the meta box. * * @return callable */ public function get_callback() { return array($this, 'render_meta_box'); } /** * Get the screen context where the meta box should display. * * @return string */ public function get_context() { return 'advanced'; } /** * Get the ID of the meta box. * * @return string */ public function get_id() { return 'meta-box-id'; } /** * Get the display priority of the meta box. * * @return string */ public function get_priority() { return 'default'; } /** * Get the screen(s) where the meta box will appear. * * @return array|string|WP_Screen */ public function get_screens() { return null; } /** * Get the title of the meta box. * * @return string */ public function get_title() { return 'Meta Box Title'; } }
And this is our MyPlugin_MetaBox
class if we do one method per argument. As you can see, it’s quite a bit bigger than the other MyPlugin_MetaBox
class. (That’s why we cut one method. You can cut more of them if you don’t need them.) That said, all the methods are simple and just return a value.
There’s no correct way
The complicated thing with design is that it’s often about making a decision where neither solution is perfect. Well, this happens to be one of those situations. Both options have their trade-offs, and neither is superior to the other.
So it boils down to a personal choice on your part. If you like the idea of using a single method to pass arguments to the add_meta_box
function, then use that. Otherwise, you can use one method per add_meta_box
function argument.
That said, for this article, we’re going to use the second option. If you’d rather go ahead and use the first, you can! There’s nothing that we’ll see next that you can’t do with the first options as well.
Creating the rest of our meta box class
Alright, now that we’ve decided on how to register our MyPlugin_MetaBox
class, we can continue building it. In the previous section, our MyPlugin_MetaBox
class methods returned some values just to show how they worked. But that’s not what we want to do with our final class. We have to go back to the drawing board a bit.
Making our class generic
We want to go back to the drawing because it’s easier if we make our class generic. What do we mean by that? Well, we mean that it would be better if we could reuse our MyPlugin_MetaBox
class to create a variety of meta boxes.
The best way to do that is by making all the values that we return from our methods properties of the MyPlugin_MetaBox
class. We then use a constructor to assign values to these internal class properties. Let’s look at what that could look like.
class MyPlugin_MetaBox { /** * Screen context where the meta box should display. * * @var string */ private $context; /** * The ID of the meta box. * * @var string */ private $id; /** * The display priority of the meta box. * * @var string */ private $priority; /** * Screens where this meta box will appear. * * @var string[] */ private $screens; /** * The title of the meta box. * * @var string */ private $title; /** * Constructor. * * @param string $id * @param string $title * @param string $context * @param string $priority * @param string[] $screens */ public function __construct($id, $title, $context = 'advanced', $priority = 'default', $screens = array()) { if (is_string($screens)) { $screens = (array) $screens; } $this->context = $context; $this->id = $id; $this->priority = $priority; $this->screens = $screens; $this->title = $title; } /** * Get the screen context where the meta box should display. * * @return string */ public function get_context() { return $this->context; } /** * Get the ID of the meta box. * * @return string */ public function get_id() { return $this->id; } /** * Get the display priority of the meta box. * * @return string */ public function get_priority() { return $this->priority; } /** * Get the screen(s) where the meta box will appear. * * @return array|string|WP_Screen */ public function get_screens() { return $this->screens; } /** * Get the title of the meta box. * * @return string */ public function get_title() { return $this->title; } }
So here’s our updated MyPlugin_MetaBox
class. All the earlier returned values are gone. We’ve replaced them all with individual properties.
The constructor is in charge of assigning values to these properties. It has five parameters: id
, title
, context
, priority
and screens
. Their names match the five properties that we added right above it.
Both id
and title
are mandatory parameters. Meanwhile, context
, priority
and screens
are optional. Their default values are the same as the ones for the add_meta_box
function.
The only unusual thing about the constructor is how we handle the screens
argument. Since screens
can be either a string or an array of strings, we want to standardize it as an array of strings. To do that, we check if screens
is an array using the is_array
PHP function. If screens
isn’t an array, we cast it as one.
Where is the callback method?
It’s possible that you’ve noticed already, but there’s one method missing from our updated MyPlugin_MetaBox
class. It’s the get_callback
method. Why didn’t we create a get_callback
method like the other methods that we created?
The reason is that, unlike the other methods that we created, the get_callback
method is more than just a method that returns a value. It’s also how we tell WordPress how we’ll render the inside of our meta box. This deserves a bit more of a discussion than the methods that we saw so far.
So how will we render the inside of our meta box? Well, this is probably one of the more fascinating parts of designing our MyPlugin_MetaBox
class. That’s because this is the area that you have the most creative freedom as a designer.
Render the HTML inside our class
The most common solution to this problem is to have our class render the HTML that it needs. This means that our MyPlugin_MetaBox
class would have a method with HTML in it. Here’s an example of what this could look like:
class MyPlugin_MetaBox { // ... /** * Get the callable that will the content of the meta box. * * @return callable */ public function get_callback() { return array($this, 'render'); } // ... /** * Render the content of the meta box. * * @param WP_Post $post */ public function render(WP_Post $post) { ?> <p>Post title: <?php echo $post->post_title; ?></p> <?php } }
As you can see, our MyPlugin_MetaBox
class has its get_callback
method back! (Yay!) It returns an array with our object and the string render
. This is a callable
that represents the render
method that we added below the get_callback
method.
The render
method has one parameter: post
. This is the WP_Post
object that WordPress is loading our meta box for. We use it to render the HTML inside of our meta box. Right now, this is just a paragraph with the title of our post.
Using an abstract class
The problem with rendering our HTML this way is that we can’t keep our MyPlugin_MetaBox
class generic. Each different type of meta box would need its own unique render method. It would then make more sense to make our
MyPlugin_MetaBox` class an abstract class like this:
abstract class MyPlugin_MetaBox { // ... /** * Get the callable that will the content of the meta box. * * @return callable */ public function get_callback() { return array($this, 'render'); } // ... /** * Render the content of the meta box. * * @param WP_Post $post */ abstract public function render(WP_Post $post); }
Here’s our modified MyPlugin_MetaBox
class. There are two changes that we made to it. We added the abstract
keyword in front of the class and render
method.
The first made our MyPlugin_MetaBox
class an abstract class. And the other makes our render
method abstract. This lets us defer the implementation of the render
method to a child class.
class MyPlugin_PostTitleMetaBox extends MyPlugin_MetaBox { /** * Render the content of the meta box. * * @param WP_Post $post */ public function render(WP_Post $post) { ?> <p>Post title: <?php echo $post->post_title; ?></p> <?php } }
And above is an example of what such a child class would look like. The MyPlugin_PostTitleMetaBox
extends our MyPlugin_MetaBox
abstract class. It then implements the same render
method as the one we had earlier.
Render the HTML using a template
Now, our MyPlugin_MetaBox
abstract class still doesn’t solve the problem that we mentioned earlier. Our MyPlugin_MetaBox
class isn’t generic anymore. We need to create a child class for every type of meta box that we want to create.
If we want to keep our MyPlugin_MetaBox
class generic, we need a way to pass it the HTML that we want to render in its constructor. One of the easiest ways to do that is by using a PHP template to render the HTML. (This is also something that we saw when we looked at designing classes for WordPress admin pages.) We can then pass the path to that template in the constructor of our MyPlugin_MetaBox
class like this:
class MyPlugin_MetaBox { /** * Screen context where the meta box should display. * * @var string */ private $context; /** * The ID of the meta box. * * @var string */ private $id; /** * The display priority of the meta box. * * @var string */ private $priority; /** * Screens where this meta box will appear. * * @var string[] */ private $screens; /** * Path to the template used to display the content of the meta box. * * @var string */ private $template; /** * The title of the meta box. * * @var string */ private $title; /** * Constructor. * * @param string $id * @param string $emplate * @param string $title * @param string $context * @param string $priority * @param string[] $screens */ public function __construct($id, $template, $title, $context = 'advanced', $priority = 'default', $screens = array()) { if (is_string($screens)) { $screens = (array) $screens; } $this->context = $context; $this->id = $id; $this->priority = $priority; $this->screens = $screens; $this->template = rtrim($template, '/'); $this->title = $title; } // ... }
Our MyPlugin_MetaBox
class constructor now has a sixth parameter: template
. Like the other constructor parameters, we assign it to an internal property with the same name. But that’s where the similarities with what we’ve done before end.
Right away, we have a small difference when we assign the template
argument to its property. We pass it through the rtrim
function. We do this to ensure that template
never ends with a slash.
Rendering our template
Next, we need to rework the render
method that we had previously. Instead of using HTML right in the class, we need to use our template
to render the HTML. Lucky for us, this isn’t too hard to do!
class MyPlugin_MetaBox { // ... /** * Render the content of the meta box using a PHP template. * * @param WP_Post $post */ public function render(WP_Post $post) { if (!is_readable($this->template)) { return; } include $this->template; } }
Above is our new render
method. It first starts by checking if we can read the template file. We do that by passing the template
value to the is_readable
PHP function.
If the file found at the location stored in template
isn’t readable, we return right away. Otherwise, we use the include
statement to include the PHP file at template
location. This will cause PHP to evaluate the file and generate the HTML inside it. (You can read more on how to generate HTML that way here.)
<p>Post title: <?php echo $post->post_title; ?></p>
So what would our template contain in this scenario? Well, it would contain the same HTML code that we had earlier. This is what you can see above.
But wait… we can use the post
variable inside our template? Yes, we can! PHP considers our template to be within the same scope as the render
method.
A great foundation
And that’s it for our MyPlugin_MetaBox
class! Because we made the class generic, you can use it to create all sorts of different meta boxes. And, as you use it more, you can look at extending it to deal with more specific scenarios. But, for now, you can find the complete MyPlugin_MetaBox
class below:
/** * A WordPress meta box that appears in the editor. */ class MyPlugin_MetaBox { /** * Screen context where the meta box should display. * * @var string */ private $context; /** * The ID of the meta box. * * @var string */ private $id; /** * The display priority of the meta box. * * @var string */ private $priority; /** * Screens where this meta box will appear. * * @var string[] */ private $screens; /** * Path to the template used to display the content of the meta box. * * @var string */ private $template; /** * The title of the meta box. * * @var string */ private $title; /** * Constructor. * * @param string $id * @param string $emplate * @param string $title * @param string $context * @param string $priority * @param string[] $screens */ public function __construct($id, $template, $title, $context = 'advanced', $priority = 'default', $screens = array()) { if (is_string($screens)) { $screens = (array) $screens; } $this->context = $context; $this->id = $id; $this->priority = $priority; $this->screens = $screens; $this->template = rtrim($template, '/'); $this->title = $title; } /** * Get the callable that will the content of the meta box. * * @return callable */ public function get_callback() { return array($this, 'render'); } /** * Get the screen context where the meta box should display. * * @return string */ public function get_context() { return $this->context; } /** * Get the ID of the meta box. * * @return string */ public function get_id() { return $this->id; } /** * Get the display priority of the meta box. * * @return string */ public function get_priority() { return $this->priority; } /** * Get the screen(s) where the meta box will appear. * * @return array|string|WP_Screen */ public function get_screens() { return $this->screens; } /** * Get the title of the meta box. * * @return string */ public function get_title() { return $this->title; } /** * Render the content of the meta box using a PHP template. * * @param WP_Post $post */ public function render(WP_Post $post) { if (!is_readable($this->template)) { return; } include $this->template; } }
Photo Credit: Dhruv Deshmukh