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