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

Designing a system: WordPress admin pages

In a previous article, we saw how to design a class to represent a WordPress admin page. The article was an excellent resource for anyone looking to design a class to solve that specific type of problem. But it was only a starting point.

For example, we used the settings API to handle forms. But the settings API doesn’t quite do the job for every use cases. For those other cases, you’re going to need to manage the submission of a form yourself.

But to handle these different use cases, we’re going to need a more solid foundation. So we’re going to take the work that we did to design our admin page class, and we’re going to take it one step further. We’re going to design a whole system for WordPress admin pages.

Reviewing our admin page class

But first, let’s review some of what we had at the end of the previous article. Instead of focusing on creating a system like we’re going to do now, that article focused on creating a class. That class was the MyPlugin_AdminPage class.

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;
    }
}

As you can see above, the MyPlugin_AdminPage class is pretty big. That’s because it has a method for each argument of the add_submenu_page function. This is a design decision that allows the class to map to the add_submenu_page function in a simple way.

The MyPlugin_AdminPage class also made use of the settings API. We used it to create the different fields of the admin page. We’d register them with the settings API, and it would put everything together for us.

The last few methods of the MyPlugin_AdminPage class had to do with HTML generation. The settings API doesn’t generate any HTML for us, so we needed a way to do that. The solution was to reuse a method to generate HTML from PHP template that we’d seen in a previous article.

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');

Finally, the last little bit of code from the previous article was the myplugin_add_admin_page function. This is the function that we used to register the MyPlugin_AdminPage class with WordPress. All that it did was call the add_submenu_page using the methods from the MyPlugin_AdminPage class for the function arguments.

The myplugin_add_admin_page function gets called by the plugin API. We register it to the admin_menu hook using the add_action function. The admin_menu hook that you should always use whenever you want to add admin pages.

Creating an interface

An essential aspect of designing an object-oriented system is to have it rely on interfaces and not concrete classes. This is an important element the dependency inversion principle. This means that we need to extract an interface from our MyPlugin_AdminPage class.

/**
 * 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 parent slug of the options page.
     *
     * @return string
     */
    public function get_parent_slug();

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

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

Above is the MyPlugin_AdminPageInterface interface. It’s the result of this extraction process. It contains the relevant methods from MyPlugin_AdminPage class.

It’s worth taking a moment to discuss why we chose these methods and not the others. All these methods have one thing in common. They’re all methods that we use to map our admin page class to the add_submenu_page function.

We omitted every other method. (At least for now.) The reason for doing this is that we want to start with the smallest interface possible. This will allow our system to be much more flexible.

So for now, MyPlugin_AdminPageInterface interface only has those methods. There’s no support for the settings API like the original MyPlugin_AdminPage class has. We’ll see how to handle the settings API using an interface a bit later.

Adding our admin pages

Next, we want to look at how we’ll register our admin pages with WordPress. This is the task that the myplugin_add_admin_page function did in our previous article. If you want you can continue using it, you can with this small change:

function myplugin_add_admin_pages() {
    $admin_pages = array(
        // Put admin page objects here
    );
 
    foreach ($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')
        );
    }
}
add_action('admin_menu', 'myplugin_add_admin_pages');

Here’s the modified myplugin_add_admin_page function. First, we changed the name and made it plural. This is to represent the fact that it can add more than one admin page object.

We also changed the admin_page variable and also made it plural. Instead of containing a single admin page object, it’s now an array of admin page objects. We use a foreach loop to add them all using the add_submenu_page function.

Adding back the rendering system

Now that we have a foundation for our admin page system, we can start fleshing it out. The first thing we can look at doing is adding back the rendering system that we had in the MyPlugin_AdminPage class. To do that, we’re going to use an abstract class.

/**
 * A WordPress admin page rendered using a PHP template.
 */
abstract class MyPlugin_AbstractRenderedAdminPage implements MyPlugin_AdminPageInterface
{
    /**
     * Path to the admin page's templates.
     *
     * @var string
     */
    protected $template_path;

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

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

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

Here is our MyPlugin_AbstractRenderedAdminPage abstract class. This class implements our MyPlugin_AdminPageInterface interface. That said, we’ve only implemented the render_page method. (This is also why the class needs to be abstract.)

The reason why we’ve only implemented the render_page method is that we care about. The goal of the MyPlugin_AbstractRenderedAdminPage class is to be a base class for any class that wants to use PHP templates. And this happens in the render_page method.

The MyPlugin_AbstractRenderedAdminPage class also has the render_template helper method. We’ve changed its visibility from private to protected. This will allow any class that extends our MyPlugin_AbstractRenderedAdminPage class to use it.

Adding back support for the settings API

Next, we’re going to look into adding back the support for the settings API. This is going to be a bit different from what we did with the rendering system. Instead of using an abstract class, we’re going to use inheritance.

/**
 * A WordPress admin page configured using the settings API.
 */
interface MyPlugin_ConfigurableAdminPageInterface extends MyPlugin_AdminPageInterface
{
    /**
     * Configure the admin page using the Settings API.
     */
    public function configure();
}

Here’s what we mean by using inheritance. We created a new interface called MyPlugin_ConfigurableAdminPageInterface. This new interface extends our original MyPlugin_AdminPageInterface.

Why extend?

A valid question would be, “Why do we need to extend the MyPlugin_AdminPageInterface?” After all, we could just have the MyPlugin_ConfigurableAdminPageInterface with just the configure method. And any admin page who wants to use the settings API would have to implement both interfaces instead of just the one. (This would be a type of multiple inheritance.)

The reason not to go this route is subjective. From a design perspective, a standalone MyPlugin_ConfigurableAdminPageInterface interface doesn’t make much sense. There aren’t really any scenarios where you’d use the MyPlugin_ConfigurableAdminPageInterface interface without also using the MyPlugin_AdminPageInterface interface as well.

That said, if you had a standalone system for the settings API, it would make sense for that system to have an interface like the MyPlugin_ConfigurableAdminPageInterface interface. But such a system is out of the scope of this article. So, for now, the MyPlugin_ConfigurableAdminPageInterface interface will extend the MyPlugin_AdminPageInterface interface.

Configuring the settings API

At this point, we have an interface that we can use to configure the settings API, but we still don’t have code to use that interface. This is what we have to do next. We have to create a function that calls the configure method at the right moment in the WordPress loading process.

function myplugin_configure_admin_pages() {
    $admin_pages = array(
        // Put admin page objects here
    );
 
    foreach ($admin_pages as $admin_page) {
        if ($admin_page instanceof MyPlugin_ConfigurableAdminPageInterface) {
            $admin_page->configure();
        }
    }
}
add_action('admin_init', 'myplugin_configure_admin_pages');

Above is the myplugin_configure_admin_pages function used to configure our admin pages that want to use the settings API. It’s very similar to our earlier myplugin_add_admin_pages function. It’s basically a foreach loop through all our admin page objects.

The difference is that inside the loop we do use a conditional to check if the admin page object implements the MyPlugin_ConfigurableAdminPageInterface interface. We use the instanceof operator to do it. If the given admin_page implements the MyPlugin_ConfigurableAdminPageInterface interface, we call the configure method.

It’s also worth mentioning that the myplugin_configure_admin_pages function also gets called using the plugin API. We don’t use the admin_menu hook this time. Instead, the hook that you should use with the settings API is the admin_init hook.

How do you manage your admin page objects?

Now, there’s a problem with the myplugin_configure_admin_pages function that we haven’t discussed. It’s that it has a admin_pages variable which contains our admin page objects. The problem with that is that our myplugin_add_admin_pages function also had that same variable.

So what do we do? We can’t have two admin_pages variables that initialize our admin page objects. One solution is to move the initialization of our admin page objects to its function like this:

function myplugin_get_admin_pages() {
    return array(
        // Put admin page objects here
    );
}

The myplugin_get_admin_pages function does exactly that. It just returns our array of admin page objects. We can then replace the admin_pages array in our functions with this:

$admin_pages = myplugin_get_admin_pages();

We use the myplugin_get_admin_pages function to fetch our admin page objects whenever we need them. In the case of our two earlier functions, we use it to populate our admin_pages array. This solves the issue of which of these functions creates our admin page objects.

Using the event management system instead

That said, this is far from an ideal way to solve the problem. For one, it’s a procedural way of solving the problem. This isn’t bad per se. But it isn’t helpful when the goal of this article is to show you how to create an object-oriented system.

Second, it doesn’t really show you how to combine various systems together. (And that’s one of the goals of the goals of these articles.) So we’re going to take a moment and do that. We’re going to combine this system with the event management system.

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

}

We’re going to start with just an empty MyPlugin_AdminPagesSubscriber class. It implements the MyPlugin_SubscriberInterface from the event management system. This interface has a contract that requires any class that implements it to have a get_subscribed_events static method.

Storing our admin page objects

Now that we have the foundation of our MyPlugin_AdminPagesSubscriber class, let’s flesh it out some more. The first thing that it needs to have is a way to store all our admin page objects. We can do that using a private property.

/**
 * Subscriber that registers the plugin's admin pages with WordPress.
 */
class MyPlugin_AdminPagesSubscriber implements MyPlugin_SubscriberInterface
{
    /**
     * The admin pages that the subscriber manages.
     *
     * @var MyPlugin_AdminPageInterface[]
     */
    private $admin_pages;

    /**
     * Constructor.
     *
     * @param MyPlugin_AdminPageInterface[] $admin_pages
     */
    public function __construct(array $admin_pages)
    {
        $this->admin_pages = $admin_pages;
    }
}

Above is the modified MyPlugin_AdminPagesSubscriber class. We added the admin_pages private property. We also added a constructor that has an array of admin pages as an argument.

But just assigning the admin_pages variable to the admin_pages property like that isn’t great. That’s because we say that the admin_pages array should only contain objects implementing the MyPlugin_AdminPageInterface interface. But the reality is that we have no way of guaranteeing that the way that we coded our constructor.

/**
 * Subscriber that registers the plugin's admin pages with WordPress.
 */
class MyPlugin_AdminPagesSubscriber implements MyPlugin_SubscriberInterface
{
    /**
     * The admin pages that the subscriber manages.
     *
     * @var MyPlugin_AdminPageInterface[]
     */
    private $admin_pages;

    /**
     * Constructor.
     *
     * @param MyPlugin_AdminPageInterface[] $admin_pages
     */
    public function __construct(array $admin_pages)
    {
        $this->admin_pages = array();

        foreach ($admin_pages as $admin_page) {
            $this->add_admin_page($admin_page);
        }
    }

    /**
     * Add a new admin page to the subscriber.
     *
     * @param MyPlugin_AdminPageInterface $admin_page
     */
    private function add_admin_page(MyPlugin_AdminPageInterface $admin_page)
    {
        $this->admin_pages[] = $admin_page;
    }
}

Here’s an improved MyPlugin_AdminPagesSubscriber class with the added validation of the admin_pages array. There’s a new private method called add_admin_page. It’s now the method in charge of adding admin page objects to the admin_pages array. The add_admin_page method validates the admin page object using type hinting.

Using our new add_admin_page method, we can then rework the constructor. We remove the old assignment of the admin_pages variable to the admin_pages property. We replace it with just assigning an empty array to the admin_pages property.

We then use a foreach loop to loop through the admin_pages variable. We then pass each admin page object to our new add_admin_page method. This is what ensures that every admin page object in our admin_pages array implements our MyPlugin_AdminPageInterface interface.

Moving our functions

At this point, all that we’ve done is move everything related to the myplugin_get_admin_pages function to our MyPlugin_AdminPagesSubscriber class. We haven’t touched either function that used the myplugin_get_admin_pages function. Those were the myplugin_add_admin_pages and myplugin_configure_admin_pages functions.

Moving these two functions to our MyPlugin_AdminPagesSubscriber class is quite straightforward. To begin, the MyPlugin_SubscriberInterface interface requires that we create the get_subscribed_events public static method. This method is where we’ll put the hooks that we registered with the add_action function before.

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

    /**
     * Returns an array of events that this subscriber wants to listen to.
     *
     * The array key is the event name. The value can be:
     *
     *  * The method name
     *  * An array with the method name and priority
     *  * An array with the method name, priority and number of accepted arguments
     *
     * For instance:
     *
     *  * array('event_name' => 'method_name')
     *  * array('event_name' => array('method_name', $priority))
     *  * array('event_name' => array('method_name', $priority, $accepted_args))
     *
     * @return array
     */
    public static function get_subscribed_events()
    {
        return array();
    }

    // ...
}

Above you can see the get_subscribed_events method. For now, it returns an empty array. But the array that it returns needs to follow the convention of the MyPlugin_SubscriberInterface interface. This convention is written in the PHPdoc of the method.

Next, we have to move our two functions to the MyPlugin_AdminPagesSubscriber class. Once that’s done, we also need to add the hooks and method names to the array that the get_subscribed_events method returns. You can see this all below.

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

    /**
     * Returns an array of events that this subscriber wants to listen to.
     *
     * The array key is the event name. The value can be:
     *
     *  * The method name
     *  * An array with the method name and priority
     *  * An array with the method name, priority and number of accepted arguments
     *
     * For instance:
     *
     *  * array('event_name' => 'method_name')
     *  * array('event_name' => array('method_name', $priority))
     *  * array('event_name' => array('method_name', $priority, $accepted_args))
     *
     * @return array
     */
    public static function get_subscribed_events()
    {
        return array(
            'admin_init' => 'configure_admin_pages',
            'admin_menu' => 'add_admin_pages',
        );
    }

    /**
     * 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')
            );
        }
    }

    /**
     * Configure the plugin's admin pages using the Settings API.
     */
    public function configure_admin_pages()
    {
        foreach ($this->admin_pages as $admin_page) {
            if ($admin_page instanceof MyPlugin_ConfigurableAdminPageInterface) {
                $admin_page->configure();
            }
        }
    }

    // ...
}

We added two new methods: add_admin_pages and configure_admin_pages. The two methods kept the same name as our earlier functions. The only difference is that we removed the myplugin_ prefix since it’s not useful inside a class.

The code inside the methods hasn’t changed from the earlier version with the functions with one exception. We replaced the use of the admin_pages variable with the admin_pages class property. That class property was the main reason why we created the MyPlugin_AdminPagesSubscriber class in the first place.

A solid foundation

So this is a good place to wrap things up! With what we’ve seen in this article, you have all that you need to create admin pages like in the previous article. But now, you’ve broken things down more and built a system around it.

The great thing with this system is that it lets you handle different use cases. So far, we’ve only covered the use cases from the previous article like rendering templates and using the settings API. But, we’ll look at some more in the future!

You can find a gist with all the complete code sample here.

Photo Credit: Hal Gatewood

Creative Commons License