Want to learn object-oriented programmming? Get my free course

Designing a class to represent a WordPress meta box

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 the id of the meta box.
  • Meta Box Title is the title of the meta box.
  • array($this, 'render_meta_box') is the callback 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 ourMyPlugin_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

Creative Commons License