Let’s talk about the plugin API. You can’t write a plugin without using it. Well that’s not quite true. You could, but you’d have a hard (and unpleasant) time without it though. That’s because it’s the cornerstone of your interaction with WordPress.
This means that you can’t build an object-oriented plugin without taking it in consideration. That’s also when the headaches start. You try to find a solution to the problem. You often go back to the usual solutions such as:
- Turning your classes into singletons.
- Using the API in your constructor.
- Flipping a table.
None of the these are great solutions to the problem (especially that last one). But that’s because you’ve been focusing on your problems. You want that sweet plugin you’ve been coding to be object-oriented (who doesn’t?).
Meanwhile, you didn’t stop and think about the plugin API. What does it want?
What does the Plugin API want?
You see the plugin API didn’t just appear there. Someone decided that WordPress needed a way for you to alter its behaviour without “hacking core”. This wasn’t a new requirement either. The ability to add filters was around when Matt forked the b2 project in 2003.
That’s because the ability to extend a platform is what gives it strength. You get a community of developers to build for it. This creates a healthy ecosystem around that platform.
They couldn’t give you the keys to the car and let you go on a joy ride though. That’d be sweet (but also crazy). So you get a controlled interaction with WordPress through the plugin API. It’s like a polite conversation with your parents (or Matt if that’s your thing).
You: Hey, I’d love to hear when you publish or delete a post.
Plugin API: Sure thing Timmy. I’ll let you know.
It’s also a two-sided conversation. The plugin API will notify you when an event you asked about occurs.
Plugin API: Hey Timmy, this post just got published. Do you need to do anything?
You: Hot diggity, I do! Thanks, Plugin API!
This conversation can happen with anyone too. WordPress could ask about itself. Your plugin could also ask about another plugin.
That’s the mediator pattern in a nutshell
That polite conversation reflects the problem that the mediator pattern solves. You need it to communicate to everyone. Otherwise, you have to do it all yourself.
A world without a mediator
What would happen if the plugin API wasn’t around? You’d have to “hack core” to work with WordPress (always a bad idea). Each plugin would also have to talk directly to each plugin it wants to interact with.
A situation like that creates a lot of coupling. It’s a way of characterizing how much a piece of code relies on another. This is what the direct lines represent in the diagram above. A high degree of coupling is bad news for a platform like WordPress.
It makes WordPress as a whole brittle. That’s never a desirable quality for any piece of software. That’s why there was already a filter system in b2 back in 2003.
Back to reality
So now that you’ve seen what a world without the plugin API. Let’s get back to the real world. The idea behind the mediator pattern is to design an intermediary to control the interactions with a system. We call the objects talking with that intermediary colleagues.
The plugin API here acts as a control tower. Everyone (including WordPress) talks to it and not to anyone else. They’re the colleagues. This behaviour creates what we call loose coupling. Your code only interacts with that piece of code (the plugin API).
That’s why you sometimes get weird results when sharing a hook with other plugins. Your code has no idea who else is sharing that hook with you. You only get the information passed by the plugin API when it’s your turn.
So how do you work with it?
So now that you understand what the plugin API wants. Let’s get back to you. You want to use object-oriented programming, but you also need the plugin API. How can you reach a common ground?
Empathize with it
There’s a common pattern to all the solutions (well not table flipping) I brought up at the beginning of the article. They just cram the plugin API into any class. That’s why they use a singleton or use the API in the constructor.
Poor plugin API is just trying to do its job. You have to empathize with it. What does that mean? It means you should design code just for it. It’s a special snowflake and you should treat it as such.
Write code just for it
Let’s go back to single responsibility principle. It says that your classes should have a unique responsibility. This implies you should have a class or set of classes who’s responsibility is the plugin API.
Let’s go over some of the code from the article on interfaces. We’ll look at it like we designed it with the mediator pattern in mind.
Colleagues
The first thing you want to tackle is the idea of colleagues. The colleagues can’t talk to each other only to the mediator who defines how they can interact. In the case of plugin API, it’s through actions and filters.
We formalized that idea by creating an interface for classes that acted as colleagues. We even went one step further and split them into the two. Each of them represents one type of interaction possible with the mediator.
/** * Action_Hook_SubscriberInterface is used by an object that needs to subscribe to * WordPress action hooks. */ interface Action_Hook_SubscriberInterface { /** * Returns an array of actions that the object needs to be subscribed to. * * The array key is the name of the action hook. 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('action_name' => 'method_name') * * array('action_name' => array('method_name', $priority)) * * array('action_name' => array('method_name', $priority, $accepted_args)) * * @return array */ public static function get_actions(); } /** * Filter_Hook_SubscriberInterface is used by an object that needs to subscribe to * WordPress filter hooks. */ interface Filter_Hook_SubscriberInterface { /** * Returns an array of filters that the object needs to be subscribed to. * * The array key is the name of the filter hook. 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('filter_name' => 'method_name') * * array('filter_name' => array('method_name', $priority)) * * array('filter_name' => array('method_name', $priority, $accepted_args)) * * @return array */ public static function get_filters(); }
We have Action_Hook_SubscriberInterface
for classes that want to interact with WordPress actions. We then have Filter_Hook_SubscriberInterface
for WordPress filters. That way your classes can only implement one of the two interface methods if they need to.
Mediator
We also need to acknowledge the plugin API as the mediator. That’s why we designed the WP_Plugin_API_Manager
class. Its sole responsibility was to interact with the mediator. That’s why the plugin API code is only found in that class.
We still needed to handle the registration of colleagues. Because we split our interfaces in two, our class needs to take in consideration both interfaces. That meant we needed code to handle the registration of both the actions and the filters.
/** * WP_Plugin_API_Manager handles registering actions and hooks with the * WordPress Plugin API. */ class WP_Plugin_API_Manager { /** * Registers an object with the WordPress Plugin API. * * @param mixed $object */ public function register($object) { if ($object instanceof Action_Hook_SubscriberInterface) { $this->register_actions($object); } if ($object instanceof Filter_Hook_SubscriberInterface) { $this->register_filters($object); } } /** * Register an object with a specific action hook. * * @param Action_Hook_SubscriberInterface $object * @param string $name * @param mixed $parameters */ private function register_action(Action_Hook_SubscriberInterface $object, $name, $parameters) { if (is_string($parameters)) { add_action($name, array($object, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { add_action($name, array($object, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } /** * Regiters an object with all its action hooks. * * @param Action_Hook_SubscriberInterface $object */ private function register_actions(Action_Hook_SubscriberInterface $object) { foreach ($object->get_actions() as $name => $parameters) { $this->register_action($object, $name, $parameters); } } /** * Register an object with a specific filter hook. * * @param Filter_Hook_SubscriberInterface $object * @param string $name * @param mixed $parameters */ private function register_filter(Filter_Hook_SubscriberInterface $object, $name, $parameters) { if (is_string($parameters)) { add_filter($name, array($object, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { add_filter($name, array($object, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } /** * Regiters an object with all its filter hooks. * * @param Filter_Hook_SubscriberInterface $object */ private function register_filters(Filter_Hook_SubscriberInterface $object) { foreach ($object->get_filters() as $name => $parameters) { $this->register_filter($object, $name, $parameters); } } }
Don’t make everyone a colleague
Now that you’ve seen how you can design classes to interact with the plugin API. You should keep applying the single responsibility principle. Keep dividing your code into classes with a unique responsibility. You’ll see that there’s no reason to make everyone a colleague.
For example, let’s take an admin page class using the settings API. It needs to access some hooks like admin_init
. On the other hand, working with the WordPress REST API requires no interactions with the plugin API.
Taking it one step further
Let’s take the single responsibility principle one step further. Let’s suppose that we limit the responsibility of a colleague. It can only deal with the communication with the plugin API.
The result is that your class is now an event subscriber. This is where you start entering the realm of event-driven programming.