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

Improving a system: Different types of WordPress admin pages

In a previous article, we saw how to design a system for WordPress admin pages. The article was an excellent resource for anyone looking for a system to solve that specific type of problem. But it was only a starting point. (That said, you should take the time to read it before reading this article.)

All that we did was convert code from another article into the WordPress admin page system. We didn’t look at any other use cases besides the ones covered in that article already. But the reality is that there are a lot of them.

For example, the article only looked at how to add submenu pages. But there are other types of admin pages besides that one. The admin page system should be able to handle them all.

This is one of several advanced use cases that you might encounter with the WordPress admin page system. That’s why the system as we saw it so far is a great place to start. But handing these uses cases like different types of admin pages might be necessary depending on the kind of project that you’re working on.

Top-level menus

The first type of admin page that we’re going to look at is how to how to add a top-level menu page. Top-level menu pages are admin pages that appear in the sidebar of the WordPress admin panel. Most large plugins and themes have a top-level menu page, so it’s often essential to be able to handle that scenario.

add_menu_page vs add_submenu_page

What distinguishes a top-level menu page from a submenu page is the function that you use to add the page. You register a top-level menu page using the add_menu_page function. Submenu pages use the add_submenu_page function. This is the function that our WordPress admin page system uses at the moment.

Splitting our interface into multiple ones

Going back to that system, we’d created the MyPlugin_AdminPageInterface interface. The job of that interface was to allow us to map arguments of the add_submenu_page function to our admin page objects. We’re going to have to do the same thing for the add_menu_page function.

This means that we’re going to need two interfaces for admin pages instead of one. We’re going to need one to identify top-level admin pages and one for submenu pages. So let’s go ahead and do that.

/**
 * A WordPress top-level menu page.
 */
interface MyPlugin_MenuPageInterface
{

}

/**
 * A WordPress submenu page.
 */
interface MyPlugin_SubmenuPageInterface
{

}

So you can see the two interfaces above. The MyPlugin_MenuPageInterface interface will be the interface that we use with the add_menu_page function. Meanwhile, the MyPlugin_SubmenuPageInterface interface is the new interface that we’ll use with the add_submenu_page function.

Next, we have to decide what to do with our old MyPlugin_AdminPageInterface interface. Do we have to keep using it? The answer to that is “Yes, we have to keep using it.”

We need a way to identify both our MyPlugin_MenuPageInterface and MyPlugin_SubmenuPageInterface interfaces. And the best way to do that is by having both of them inherit another interface. This is what we’re going to use our old MyPlugin_AdminPageInterface interface for.

/**
 * A WordPress admin page.
 */
interface MyPlugin_AdminPageInterface
{
    // ...
}

/**
 * A WordPress top-level menu page.
 */
interface MyPlugin_MenuPageInterface extends MyPlugin_AdminPageInterface
{

}

/**
 * A WordPress submenu page.
 */
interface MyPlugin_SubmenuPageInterface extends MyPlugin_AdminPageInterface
{

}

So you can see the change to the MyPlugin_MenuPageInterface and MyPlugin_SubmenuPageInterface interfaces above. They both now inherit the MyPlugin_AdminPageInterface interface. This means that we can continue to use the MyPlugin_AdminPageInterface interface to identify WordPress admin page objects. It also means that we can keep all the type hints that we had in the WordPress admin page system already. (Yay!)

Rearranging all interface methods

Right now, the only interface with code in it is MyPlugin_AdminPageInterface interface. It still has all the methods that we’d added when it was the only interface used by the WordPress admin page system. But we can’t keep it like this anymore.

We need to modify it so that it only has the methods that are common to both MyPlugin_MenuPageInterface and MyPlugin_SubmenuPageInterface interfaces. We can figure out what those common methods are by looking at the arguments of the two functions that the interfaces map to. If we look at the add_menu_page and add_submenu_page functions, we find the following common arguments:

  • page_title
  • menu_title
  • capability
  • menu_slug
  • callable

Now that we’ve figured out the arguments are common to both functions, we can edit the MyPlugin_AdminPageInterface interface. All that we have to do is remove the one non-common argument method which was the get_parent_slug method. You can see the updated MyPlugin_AdminPageInterface interface below:

/**
 * A WordPress admin page.
 */
interface MyPlugin_AdminPageInterface
{
    /**
     * Get the capability required to view the admin page.
     *
     * @return string
     */
    public function get_capability();

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

    /**
     * Get the title of the admin page.
     *
     * @return string
     */
    public function get_page_title();

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

    /**
     * Renders the admin page.
     */
    public function render_page();
}

The get_parent_slug method that we removed will go in the MyPlugin_SubmenuPageInterface interface like this:

```
/**
 * A WordPress submenu page.
 */
interface MyPlugin_SubmenuPageInterface extends MyPlugin_AdminPageInterface
{
    /**
     * Get the parent slug of the admin page.
     *
     * @return string
     */
    public function get_parent_slug();
}
```

The last step is to add the missing methods for the add_menu_page function to the MyPlugin_MenuPageInterface interface. Looking at the function, there are two missing arguments: icon_url and position. Let’s add methods for those two arguments in the MyPlugin_MenuPageInterface interface.

/**
 * A WordPress top-level menu page.
 */
interface MyPlugin_MenuPageInterface extends MyPlugin_AdminPageInterface
{
    /**
     * Get the URL of the icon used by the admin page.
     *
     * You can also return:
     * - A base64-encoded SVG using a data URI, which will be colored to match the color scheme. This should begin with 'data:image/svg+xml;base64,'.
     * - The name of a Dashicons helper class to use a font icon, e.g. 'dashicons-chart-pie'.
     * - The value 'none' to leave div.wp-menu-image empty so an icon can be added via CSS.
     *
     * @return string
     */
    public function get_icon_url();

    /**
     * Get the position of the admin page in the menu order of the WordPress admin sidebar.
     *
     * @return int
     */
    public function get_position();
}

Above, you can see the two missing methods that we added to the MyPlugin_MenuPageInterface interface. The first one is the get_icon_url method which maps to the icon_url argument. The second one is the get_position which does the same thing but for the position argument.

Updating our subscriber

Now that we’ve finished splitting our interfaces, we can start using them in our code. To do that, we’re going to update the MyPlugin_AdminPagesSubscriber class. If you’re using the myplugin_add_admin_pages function, the process will be the same as well.

/**
 * Subscriber that registers the plugin's admin pages with WordPress.
 */
class MyPlugin_AdminPagesSubscriber implements MyPlugin_SubscriberInterface
{
    // ...

    /**
     * Adds the plugin's admin pages to the WordPress admin.
     */
    public function add_admin_pages()
    {
        foreach ($this->admin_pages as $admin_page) {
            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')
            );
        }
    }

    // ...
}

Above is the add_admin_pages method that we were using in the previous article to register our admin page objects with WordPress. It would loop through all the admin page objects in the admin_pages class variable. It would then call the add_submenu_page function using all the methods from the old MyPlugin_AdminPageInterface interface as the function arguments.

Calling the right admin menu function

We’re going to want to do a similar thing here. The big difference now is that we don’t just call the add_submenu_page function anymore. We might want to call add_menu_page function instead.

To determine which function we want to call, we’re going to check which of our two interfaces the admin page implements. If it’s the MyPlugin_MenuPageInterface interface, we’re going to call the add_menu_page function. And if it’s the MyPlugin_SubmenuPageInterface interface, we’ll call the add_submenu_page function.

/**
 * Subscriber that registers the plugin's admin pages with WordPress.
 */
class MyPlugin_AdminPagesSubscriber implements MyPlugin_SubscriberInterface
{
    // ...

    /**
     * Adds the plugin's admin pages to the WordPress admin.
     */
    public function add_admin_pages()
    {
        foreach ($this->admin_pages as $admin_page) {
            if ($admin_page instanceof MyPlugin_MenuPageInterface) {
                add_menu_page(
                    $admin_page->get_page_title(),
                    $admin_page->get_menu_title(),
                    $admin_page->get_capability(),
                    $admin_page->get_slug(),
                    array($admin_page, 'render_page'),
                    $admin_page->get_icon_url(),
                    $admin_page->get_position()
                );
            } elseif ($admin_page instanceof MyPlugin_SubmenuPageInterface) {
                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')
                );
            }
        }
    }

    // ...
}

And now you can see how that looks like in the updated add_admin_pages method above. We added a conditional to our foreach loop. We start by checking if admin_page implements the MyPlugin_MenuPageInterface interface using the instanceof operator. If it does, we call the add_menu_page function using all our interface methods as arguments.

If admin_page doesn’t implement the MyPlugin_MenuPageInterface interface, we move to the next conditional statement. This one checks if admin_page implements the MyPlugin_SubmenuPageInterface interface. And if it does, we call the add_submenu_page function.

It’s worth noting that there’s a bit more going on with our conditional than what we just described. First, we structured the conditional in a way that you can’t register an admin page object as both a top-level menu page and a submenu page. Also if the admin page object implements both interfaces, we’re going to register it as a top-level menu page over a submenu page.

If it only implements the MyPlugin_AdminPageInterface interface, nothing will happen. This is another benefit of how we structured our conditional. We’re making it enforce that only the classes that implement either the MyPlugin_MenuPageInterface interface or the MyPlugin_SubmenuPageInterface interface get added. Everything else gets ignored.

Network admin page

In a similar vein as a top-level menu page, we also have network admin pages. These are admin pages that show up in the network administration dashboard. You might want to have your admin page appear there as opposed to the regular admin screen when WordPress is configured as a network.

Leveraging inheritance

Adding a network admin page can be a lot easier than adding a top-level menu page. That’s because there are no new functions to use. You can just make modifications to your existing classes using inheritance.

class MyPlugin_AdminPage implements MyPlugin_SubmenuPageInterface
{
    // ...

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

    // ...
}

First, let’s imagine that we have an existing submenu page class like the MyPlugin_AdminPage class above. It implements our new MyPlugin_SubmenuPageInterface interface so that it gets added as submenu page. Its get_parent_slug method returns options-general.php which is the slug of the “Settings” menu in the regular WordPress admin dashboard.

Now, let’s say that you wanted to move this page to the “Settings” menu of the WordPress network admin dashboard. Well, you’d need to figure out what the parent_slug of the “Settings” menu is there. You can figure it out by looking at the URL of the “Settings” page in the WordPress network admin dashboard.

If you did that, you’d see that the parent_slug is settings.php. So our network admin page class needs to return that instead of options-general.php. We can do that by extending the original MyPlugin_AdminPage class like this:

class MyPlugin_NetworkAdminPage extends MyPlugin_AdminPage
{
    // ...

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

    // ...
}

Above you can see the MyPlugin_NetworkAdminPage class which extends the MyPlugin_AdminPage class. Next, you’ll notice that we overrode the get_parent_slug method. The method now returns settings.php instead of options-general.php.

Controlling who can see the network page

Now, overriding the get_parent_slug method was all that you needed to turn a submenu page into a network admin page. That said, you might still need to make further changes depending on how you control access to your admin page. Let’s go back to the MyPlugin_AdminPage class.

class MyPlugin_AdminPage implements MyPlugin_SubmenuPageInterface
{
    // ...

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

    // ...
}

You can see above that the get_capability method of the MyPlugin_AdminPage class returns install_plugins. This is the right capability to use for a regular WordPress site since only administrators will have it. But it’s not as restrictive on a WordPress network installation.

For network installations, you might want to use manage_network_plugins instead. This would limit access to the admin page to only administrators of the entire WordPress network installation and not individual site administrators. So if you wanted to do that with the MyPlugin_NetworkAdminPage class, you’d just need to do this:

class MyPlugin_NetworkAdminPage extends MyPlugin_AdminPage
{
    // ...

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

    // ...
}

We’re doing the same thing that we did with the get_parent_slug method before. We overrode it in the MyPlugin_NetworkAdminPage class and changed its return value. It now returns manage_network_plugins instead of install_plugins.

Again, this demonstrates why inheritance is such a powerful tool in object-oriented design. All that you had to do is overwrite the get_capability method, and you changed the behaviour of the class. That’s why creating a network admin page is a lot easier than the top-level menu page.

How to register the network admin page

The last thing that you need to do is determine when to register the correct admin page. To do that, you can just use the is_multisite function. If the is_multisite returns true, you use the MyPlugin_NetworkAdminPage class. Otherwise, you use the MyPlugin_AdminPage class.

$admin_page = is_multisite() 
            ? new MyPlugin_NetworkAdminPage()
            : new MyPlugin_AdminPage();

A simple way to use the is_multisite function is with the ternary operator as shown above. We start by calling the is_multisite function. Then based on what it returns, we create a new MyPlugin_NetworkAdminPage object or a new MyPlugin_AdminPage object.

Replacing inheritance with the ternary operator

Now, the introduction of the ternary operator allows us to explore another way of designing our admin page class. Instead of using inheritance to create our network admin page, we’ll use the ternary operator inside the MyPlugin_AdminPage class. To begin, we’re going to need to add a constructor to our MyPlugin_AdminPage class.

class MyPlugin_AdminPage implements MyPlugin_SubmenuPageInterface
{
    /**
     * Flag whether WordPress 
     *
     * @var bool
     */
    private $is_multisite;

    /**
     * Constructor.
     *
     * @param bool $is_multisite
     */
    public function __construct($is_multisite = false)
    {
        $this->is_multisite = $is_multisite;
    }

    // ...
}

The constructor has a single parameter: is_multisite. We want this to be the value returned by the is_multisite function. We then use the constructor to assign that value to the is_multisite class property.

We then want to use that is_multisite class property with the get_capability and get_parent_slug methods. Instead of overriding both methods like we did earlier, we’ll use the ternary operator to return the correct value for each method. You can see how in the updated MyPlugin_AdminPage class below.

class MyPlugin_AdminPage implements MyPlugin_SubmenuPageInterface
{
    /**
     * Flag whether WordPress 
     *
     * @var bool
     */
    private $is_multisite;

    /**
     * Constructor.
     *
     * @param bool $is_multisite
     */
    public function __construct($is_multisite = false)
    {
        $this->is_multisite = $is_multisite;
    }

    /**
     * Get the capability required to view the admin page.
     *
     * @return string
     */
    public function get_capability()
    {
        return $this->is_multisite ? 'manage_network_plugins' : 'install_plugins';
    }

    // ...

    /**
     * Get the parent slug of the admin page.
     *
     * @return string
     */
    public function get_parent_slug()
    {
        return $this->is_multisite ? 'settings.php' : 'options-general.php';
    }

    // ...
}

It’s worth pointing out that neither approach is better than the other. If you prefer to use inheritance to solve this problem, you should. If the use of the ternary operator makes more sense to you, that’s fine too!

Just one set of improvements

So this wraps up some of the improvements that you can make to the WordPress admin page system. These changes focused on how to add support to different types of WordPress admin pages. But there are other types of improvements that we can make to it. We’ll explore those in other articles.

Photo Credit: José Alejandro Cuffia

Creative Commons License