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

Designing a class representing a WordPress admin page

A WordPress plugin can have a lot of different components. For example, some might need to use shortcodes to achieve their purpose. While others might need to create custom post types.

But one component that almost every plugin needs is an admin page. (Or they might even need more than one!) That said, designing an admin page using object-oriented programming isn’t that straightforward. There are a lot of different moving pieces that you have to take in consideration in your design.

A well-designed admin page class will combine these different moving pieces into a cohesive class. The key to achieving this is to understand the role of these different moving pieces in the larger picture of an admin page. We’ll do that by analyzing what makes up an admin page in the first place.

Elements of an admin page

With this in mind, let’s go ahead and look at the different elements of an admin page. All of them fall into two broad categories. These are registering the admin page and rendering it.

Registering the admin page

We’re going to start with registering an admin page. With WordPress, you register an admin page using the add_submenu_page function. There are also shortcut functions such as add_options_page that let you add a submenu page to a specific WordPress menu. That said, behind the scenes, all these functions call the add_submenu_page function.

The add_submenu_page function itself has a lot of parameters and it requires that you use all but one of them. If you’re not using a shortcut function like the add_options_page function, you first need to give it a parent_slug. That’s the slug of the parent menu that the admin page will show up on.

You then need to give the function a page_title and a menu_title. The first is the title tag when you go to the admin page. The other is the title of the admin page will have in the parent menu that we defined in the first parameter.

Next, you have to give the function a capability. That’s the WordPress capability that the user must have to see the menu in the WordPress admin. That said, there are a lot of possible capabilities to choose from. This can be a bit overwhelming when you’re trying to find the right one to use.

In general, a lot of plugins use the manage_options capability to only display an admin page to administrators. Outside that common scenario, it varies on the context of the admin page. For example, a plugin that deals with comments might want to use the moderate_comments.

The last mandatory parameter is menu_slug. This is the slug that the admin page will use. You need this slug to be unique otherwise you’ll run into weird display bugs.

The final parameter is function. It’s a callable that WordPress will use to display the content of the admin page. The parameter is optional, but, in practice, you need to use it if you want to display your admin page. (And who doesn’t want that!?)

Rendering the admin page

The function parameter is a good way to bring up the most problematic element of an admin page. It’s how to render the HTML of the admin page. There’s no consensus among WordPress developers on the best way to do this.

This means that there are almost as many ways to do this as there are WordPress developers! Ok, this might be a slight exaggeration. But it’s true that WordPress developers use a lot of different methods for rending the HTML of an admin page.

Some (albeit few!) use the settings API. While others choose to render it all themselves using PHP. And some do something even more extreme and build a single-page application for their admin page!

There are advantages and disadvantages to each of these methods. (That’s why everyone has a different way of doing it!) That said, we want to use as many WordPress systems as we can for this. That’s why we’ll use the settings API to do this.

Generating HTML content

There’s one big problem with the settings API. (And that’s one of the reasons why it’s so unpopular.) It’s that it doesn’t help generate the HTML of the admin page. We still have to find a way to do that.

Lucky for us, we’ve covered how to generate HTML with object-oriented programming before. Now, there’s no need to read this article. (That’s some pretty horrible self-promotion right?) It’s just there for reference.

This article is just going to reuse some of the ideas seen in that article. But we’re not going to design a class to do generate HTML like we did in it. It’ll keep things a bit simpler. That said, feel free to do that in your code though if you prefer!

Creating our admin page class

Alright, so we have a pretty good idea of what makes up an admin page now! The next step is to create a class that can handle these different elements of an admin page. We’ll name it MyPlugin_AdminPage.

class MyPlugin_AdminPage
{
}

Our MyPlugin_AdminPage class is empty for now. But it won’t stay that way for very long! Let’s start adding things to it.

Registering our admin page class and the plugin API

The first thing that we’re going to look at is how to register our admin page class with WordPress. This will require that we use the add_submenu_page function that we saw earlier. That said, one thing that we didn’t mention with the add_submenu_page function is that we can’t call it whenever we want.

No, we need to call it at a specific time using the plugin API. If you’re adding a normal admin page, you want to use the admin_menu hook. (This is what you’ll do 99% of the time.) But if you’re looking to add an admin page for the WordPress network admin, you want to use the network_admin_menu hook.

This means that we’ll need a way to use the plugin API with our MyPlugin_AdminPage. For the sake of keeping our design simple, we’re not going to design our MyPlugin_AdminPage class so that it uses the plugin API. We’ll just imagine that our plugin uses a function like this:

function myplugin_add_admin_page() {
    $admin_page = new MyPlugin_AdminPage();

    // Call add_submenu_page function
}
add_action('admin_menu', 'myplugin_add_admin_page');

That said, you might want to push the design of your MyPlugin_AdminPage class further and use the plugin API. That’s great! There are quite a few articles on the plugin API on this site to help you with that. These articles cover a wide range of options where some are easier to implement than others.

Passing arguments to the “add_submenu_page” function

There’s a good chance that you noticed that the myplugin_add_admin_page function that we saw earlier didn’t add an admin page. We just had a comment mentioning that we’d call the add_submenu_page function. Well, that comment was there so we could talk about it. (Quite the clever ruse on my part!)

That’s because we saw earlier that the add_submenu_page function uses a lot of parameters. We need a way for our MyPlugin_AdminPage class to pass arguments to it. There are a few ways to do this so let’s take a moment to look at them!

One method with all the arguments

One way we can pass arguments to theadd_submenu_page function is by using the call_user_func_array function. This is a special PHP function that not a lot of PHP developers know about. It lets you call a specific function or method while passing it an array of arguments.

The call_user_func_array has two parameters: callback and param_arr. It expects callback to be a callable. That callable will be the function or the method that the call_user_func_array function will call.

Of course, we already know what function or method we want to call! It’s the add_submenu_page function. So we’ll just pass add_submenu_page as a string for the first argument of the call_user_func_array function.

The second argument is param_arr. It’s an array of arguments passed to the call_user_func_array function will pass to the callback. It’s important to mention that the call_user_func_array function expects the param_arr array to be an indexed array. You can’t use an associative array.

Using the “call_user_func_array” function

So that’s all nice. But how do we make our MyPlugin_AdminPage class work with the call_user_func_array function? Well, take a look our updated myplugin_add_admin_page function below:

function myplugin_add_admin_page() {
    $admin_page = new MyPlugin_AdminPage();

    call_user_func_array('add_submenu_page', $admin_page->get_page_arguments());
}
add_action('admin_menu', 'myplugin_add_admin_page');

As you can see, we replaced our comment with the call to the call_user_func_array function. The first argument is a string with add_submenu_page as we said it would be. The second argument is the array returned by the get_page_arguments method of our MyPlugin_AdminPage class.

The get_page_arguments method is how our MyPlugin_AdminPage class will communicate with the add_submenu_page function. The method returns the values that we want to pass to it as an array. And, as we saw earlier, the call_user_func_array function wants the array formatted a specific way.

class MyPlugin_AdminPage
{
    // ...

    /**
     * Get the arguments for the "add_submenu_page" function.
     *
     * @return array
     */
    public function get_page_arguments()
    {
        return array(
            'options-general.php',
            'My Plugin Admin Page',
            'My Plugin',
            'install_plugins',
            'myplugin',
            array($this, 'render_page'),
        );
    }
}

Above is our updated MyPlugin_AdminPage class with the get_page_arguments method. The method returns an array with all the arguments that we want to pass to the add_submenu_page function. Each of these array elements is a parameter that we covered earlier.

  • options-general.php is the parent_slug of the parent menu of our admin page.
  • My Plugin Admin Page and My Plugin are the page_title and menu_title of our admin page respectively.
  • install_plugins is the capability required to view our admin page.
  • myplugin is the menu_slug of our admin page.
  • array($this, 'render_page') is the function callback that WordPress will use to render our admin page.

These are all pretty straightforward except for the function callback. We’re going to talk about rendering our admin page a bit later. But, for now, this is one way that we pass arguments for our MyPlugin_AdminPage class to the add_submenu_page function.

One method for each argument

The other alternative is to do the complete opposite! Instead of using one method to pass all the arguments of the add_submenu_page function, we use one method for each argument. This makes our MyPlugin_AdminPage class a bit heavier, but you don’t have to rely on a PHP function to call the add_submenu_page function.

function myplugin_add_admin_page() {
    $admin_page = new MyPlugin_AdminPage();

    add_submenu_page(
        $admin_page->get_parent_slug(),
        $admin_page->get_page_title(),
        $admin_page->get_menu_title(),
        $admin_page->get_capability(),
        $admin_page->get_slug(),
        array($admin_page, 'render_page')
    );
}
add_action('admin_menu', 'myplugin_add_admin_page');

As you can see, the updated myplugin_add_admin_page function above is a lot like our get_arguments method from earlier. But instead of returning an array with all our arguments, each argument gets its own method. The only exception is the function callback which stays the same.

class MyPlugin_AdminPage
{
    // ...

    /**
     * Get the capability required to view the admin page.
     *
     * @return string
     */
    public function get_capability()
    {
        return 'install_plugins';
    }

    /**
     * Get the title of the admin page in the WordPress admin menu.
     *
     * @return string
     */
    public function get_menu_title()
    {
        return 'My Plugin';
    }

    /**
     * Get the title of the admin page.
     *
     * @return string
     */
    public function get_page_title()
    {
        return 'My Plugin Admin Page';
    }

    /**
     * Get the parent slug of the admin page.
     *
     * @return string
     */
    public function get_parent_slug()
    {
        return 'options-general.php';
    }

    /**
     * Get the slug used by the admin page.
     *
     * @return string
     */
    public function get_slug()
    {
        return 'myplugin';
    }
}

And here’s our MyPlugin_AdminPage class with all the methods used as arguments for the add_submenu_page function. All these methods are pretty straightforward. They all return a string from our earlier arguments array.

Pick whichever you like

There’s no best way to pass arguments to the add_submenu_page function. Both have their advantages and disadvantages. (This is often the case when designing something!)

Using a single method is simpler because you only have one method as opposed to several. But having multiple methods is useful too. You might want to reuse some of these methods in your code.

So, in the end, it comes down to a personal choice. But, for this article, we’re going to use latter option. That’s because we’re going to reuse some of these methods in our code.

Configuring our admin page

Now that we’ve seen how to register our admin page, we still have to configure it. This is a required step before we look at how we’ll render it. As we mentioned earlier in the article, we’re going to use the settings API to do this.

The settings API is an umbrella for a bunch of different tasks. But there are only two that we’re interested in for the configuration step. Let’s take a look at them.

Registering a setting

The first task that we need to do is register a setting with the API. You can do this is by using the register_setting function. You can see how we do that below:

class MyPlugin_AdminPage
{
    // ...

    /**
     * Configure the admin page using the Settings API.
     */
    public function configure()
    {
        // Register settings
        register_setting($this->get_slug(), 'myplugin_option');
    }

    // ...
}

You can see that first step in the configure method above. We used the register_setting function to register a setting with the API. The register_setting has two mandatory parameters: option_group and option_name.

option_group is the settings group that the setting will belong to. We’re going to use the string returned our get_slug method for that argument. (This is one reason why we’re using individual methods for our call to the add_submenu_page function.)

The option_name parameter is the actual setting that we want to register. It’s the name of the option that we’ll use in the admin page code. In the code above, we passed the myplugin_option string as the option_name argument.

Registering sections and fields

The second task is registering sections and fields with the settings API. These sections and fields are the way to control how WordPress will display our settings. Let’s update our configure method before discussing them further.

class MyPlugin_AdminPage
{
    // ...

    /**
     * Configure the admin page using the Settings API.
     */
    public function configure()
    {
        /// Register settings
        register_setting($this->get_slug(), 'myplugin_option');

        // Register section and field
        add_settings_section(
            $this->get_slug() . '-section',
            __('Section Title', 'myplugin'),
            array($this, 'render_section'),
            $this->get_slug()
        );
        add_settings_field(
            $this->get_slug() . '-option', 
            __('My option', 'myplugin'),
            array($this, 'render_option_field'), 
            $this->get_slug(), 
            $this->get_slug() . '-section'
        );
    }

    // ...
}

We added two new function calls in our configure method above. The first one is to the add_settings_section function. The second is the add_settings_field function.

Adding a section

The add_settings_section function adds a new section to our admin page. It has four parameters: id, title, callback and page. id is the slug used to identify the section. title is the title of the section. callback is the callable used to output the content of the section. page is the slug of the admin page that the section belongs to.

Now, let’s talk about the arguments that we pass to the add_settings_section function. As you can see, we made more use of our get_slug method. We used it with the id and page arguments.

Meanwhile, we passed a translated string for our title. (Always be translating!) The callback is an array referring to the render_section method. We’ll look at that method a bit later when we look at rendering the admin page.

Adding a field

The add_settings_field function has a similar parameters to the add_settings_section function. It has six parameters: id, title, callback, page, section and args. The first four ones are the same as the ones from the add_settings_section function.

The two new ones are section and args. section is the slug of the section where our field will appear. args is an array of extra arguments that you can pass to the add_settings_field function.

For this article, we’re only interested in the section parameter. We passed it the same id value that we gave to the add_settings_section function. This was the value returned by our get_slug method concatenated with -section.

Meanwhile, the first four arguments are almost identical to the ones from the add_settings_section function. The one that’s worth pointing out is the argument passed as the callback. It’s a reference to a new method called render_option_field.

Rendering our admin page

Alright! So, at this point, we haven’t talked about any of our rendering methods at all. But now that we’ve done all the work of registering and configuring our admin page, we can look at this last part of the puzzle.

In case you hadn’t been keeping track, our code had three undefined rendering methods in total. In order of appearance, they were the render_page, render_section and render_option_field methods. Their job is to generate the HTML for parts of our admin page.

Rendering HTML using templates

This brings us to the most interesting decision that we have to take while designing our MyPlugin_AdminPage class. It’s how are we going to render the HTML of our admin pages. This is really the design part where you have the most creative liberties.

As we mentioned earlier though, we’re going to use a solution that we explored in a previous article. (Linking it again in case you missed it!) That solution revolves around using PHP templates to generate our HTML content. This removes the coupling between our MyPlugin_AdminPage class and the HTML templating code.

class MyPlugin_AdminPage
{
    /**
     * Path to the admin page templates.
     *
     * @var string
     */
    private $template_path;

    /**
     * Constructor.
     *
     * @param string $template_path
     */
    public function __construct($template_path)
    {
        $this->template_path = rtrim($template_path, '/');
    }

    // ...
}

The first thing that we want to do to implement our HTML templating solution is to add a path to our admin page templates. We did this by adding template_path as a private property. We also added a constructor that has template_path as a parameter. The constructor assigns our template_path after passing it through rtrim to ensure that it doesn’t end with a slash.

Next, we want to use this template_path property to render our PHP templates. We’ll need to create a method to do that. You can see it below:

class MyPlugin_AdminPage
{
    // ...

    /**
     * Renders the given template if it's readable.
     *
     * @param string $template
     */
    private function render_template($template)
    {
        $template_path = $this->template_path . '/' . $template . '.php';

        if (!is_readable($template_path)) {
            return;
        }

        include $template_path;
    }
}

The render_template method above has one parameter: template. We use the template argument to generate template_path. This is the complete path to the PHP template file that we want to render.

We then pass the template_path through a guard clause. In it, we use is_readable to check if we can read the template file. If we can’t read it, we return right away. Otherwise, we include it.

Rendering the different parts of our admin page

Now that we have our render_template method, we can use it to render the different parts of our admin page. We render these parts using the render_page, render_section and render_option_field methods. These are the last three methods of our MyPlugin_AdminPage class that we’ve seen but not yet implemented.

class MyPlugin_AdminPage
{
    // ...

    /**
     * Renders the option field.
     */
    public function render_option_field()
    {
        $this->render_template('option_field');
    }

    /**
     * Render the plugin's admin page.
     */
    public function render_page()
    {
        $this->render_template('page');
    }

    /**
     * Render the top section of the plugin's admin page.
     */
    public function render_section()
    {
        $this->render_template('section');
    }

    // ...
}

Above is our MyPlugin_AdminPage class with these three methods added. As you can see, these methods don’t have a lot of code. All that they do is call the render_template method that we created in the last section.

Each method asks it to render a specific template. That’s also where most of the magic happens. So that’s what we’ll look at next.

Page template

The first and largest template that we need is the one rendered by the render_page method. This is the template that contains most of the admin page. Based on the convention in render_template, you need to name this template page.php.

<div class="wrap" id="myplugin-admin">
    <div id="icon-tools" class="icon32"><br></div>
    <h2><?php echo $this->get_page_title(); ?></h2>
    <?php if (!empty($_GET['updated'])) : ?>
        <div id="setting-error-settings_updated" class="updated settings-error notice is-dismissible">
            <p><strong><?php _e('Settings saved.') ?></strong></p><button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>
        </div>
    <?php endif; ?>
    <form action="options.php" method="POST">
        <?php settings_fields($this->get_slug()); ?>
        <?php do_settings_sections($this->get_slug()); ?>
        <?php submit_button(__('Save')); ?>
    </form>
</div>

So here’s the content of page.php. There’s quite a bit going on, but most of it is just HTML markup. That said, there are a few things going on that are worth going over.

First, you probably noticed a lot of use of this inside the template. That’s because, as far as PHP knows, the template is still within the scope of our MyPlugin_AdminPage class. (Woah! Trippy!) That means that we can do the same things that we would do inside a regular class method.

That’s why we’re using our get_page_title inside the h2 tag. We also make use the get_slug method later with the settings API functions. But we’ll cover those in a second.

Next, we have an if statement that checks if the $_GET superglobal has a value for the updated key. This is the parameter that WordPress passes as a query string to tell you that it’s updated the options on the page. If it’s not empty, we display a message to say that we’ve updated the plugin’s settings.

Last, we have a form and inside are the settings API functions that we mentioned earlier. You must put these two functions inside the form tag. Otherwise, the HTML that they output won’t work.

The first one is the settings_fields function. This is a function that outputs the nonce and option_page hidden values for the admin page. Without them, WordPress won’t process our submitted admin page form.

The second function is the do_settings_sections. It renders all the HTML for the sections and fields that we registered earlier. The only HTML that it doesn’t render is the submit button. That’s why we use the submit_button function after.

Section and option field templates

The other two templates don’t share the same level of complexity as the page.php template. The section.php template will often just be nothing more than a few words. Something to go under your section header to describe the fields registered in that section.

<p><?php _e('A small description about this section of the admin page', 'myplugin'); ?></p>

The code sample above is all that there is to our section.php template. You shouldn’t forget that you need to put your text inside a p tag. You also shouldn’t forget to translate your text if your plugin supports internationalization. (Which it should!)

<input type="text" name="myplugin_option" value="<?php get_option('myplugin_option', ''); ?>" />

The option_field.php template is a bit more complicated. (But not too much!) It needs to render the input element with the value of the option. We use the get_option function to get that value from the WordPress database. (If you don’t want to use a WordPress always, create a class to interact with WordPress options.)

The name myplugin_option used in the input tag and the get_option function comes from earlier in the article. It’s the name of the setting that we registered using the register_setting function. (This was a little while ago so you might have forgotten where it came from!)

A good starting point

And this wraps things up for our MyPlugin_AdminPage class! We had to go over a lot of different things to build it. But this is something that can happen when designing classes that are more complex.

And one last thing. There’s also no need for you to go through the article to piece all the code samples together. You can find the code for the entire MyPlugin_AdminPage class below:

class MyPlugin_AdminPage
{
    /**
     * Path to the admin page templates.
     *
     * @var string
     */
    private $template_path;

    /**
     * Constructor.
     *
     * @param string $template_path
     */
    public function __construct($template_path)
    {
        $this->template_path = rtrim($template_path, '/');
    }

    /**
     * Configure the admin page using the Settings API.
     */
    public function configure()
    {
        /// Register settings
        register_setting($this->get_slug(), 'myplugin_option');

        // Register section and field
        add_settings_section(
            $this->get_slug() . '-section',
            __('Section Title', 'myplugin'),
            array($this, 'render_section'),
            $this->get_slug()
        );
        add_settings_field(
            $this->get_slug() . '-api-status',
            __('My option', 'myplugin'),
            array($this, 'render_option_field'),
            $this->get_slug(),
            $this->get_slug() . '-section'
        );
    }

    /**
     * Get the capability required to view the admin page.
     *
     * @return string
     */
    public function get_capability()
    {
        return 'install_plugins';
    }

    /**
     * Get the title of the admin page in the WordPress admin menu.
     *
     * @return string
     */
    public function get_menu_title()
    {
        return 'My Plugin';
    }

    /**
     * Get the title of the admin page.
     *
     * @return string
     */
    public function get_page_title()
    {
        return 'My Plugin Admin Page';
    }

    /**
     * Get the parent slug of the admin page.
     *
     * @return string
     */
    public function get_parent_slug()
    {
        return 'options-general.php';
    }

    /**
     * Get the slug used by the admin page.
     *
     * @return string
     */
    public function get_slug()
    {
        return 'myplugin';
    }

    /**
     * Renders the option field.
     */
    public function render_option_field()
    {
        $this->render_template('option_field');
    }

    /**
     * Render the plugin's admin page.
     */
    public function render_page()
    {
        $this->render_template('page');
    }

    /**
     * Render the top section of the plugin's admin page.
     */
    public function render_section()
    {
        $this->render_template('section');
    }

    /**
     * Renders the given template if it's readable.
     *
     * @param string $template
     */
    private function render_template($template)
    {
        $template_path = $this->template_path . '/' . $template . '.php';

        if (!is_readable($template_path)) {
            return;
        }

        include $template_path;
    }
}

We also saw early in the article that we needed a way to register our admin page. We had to use the add_submenu_page function to do it. Below is the way that we’re doing it with the MyPlugin_AdminPage that we designed:

function myplugin_add_admin_page() {
    $admin_page = new MyPlugin_AdminPage();

    add_submenu_page(
        $admin_page->get_parent_slug(),
        $admin_page->get_page_title(),
        $admin_page->get_menu_title(),
        $admin_page->get_capability(),
        $admin_page->get_slug(),
        array($admin_page, 'render_page')
    );
}
add_action('admin_menu', 'myplugin_add_admin_page');

This should give you all the building blocks that you need to build admin page classes of your own. All that’s left is for you to implement it in your own project. Feel free to leave any specific implementation questions you might have down in the comments!

Creative Commons License