In a previous article, we went over the concept of events and event listeners. These were classes who were in charge of a specific aspect of an application. This was a more abstract job that touched several parts of an application.
We also saw how you could design these event listeners in WordPress. It required that you rethink how you use the plugin API. (Yes, this is going to be another plugin API article :P)
We’re going to keep going with this idea of events and event listeners. We’re going to design a system for them. We’ll call it an “event management” system. It’s an important tool in your journey to master object-oriented programming in WordPress.
On actions and filters
Before we begin, let’s do a small recap on the plugin API language. When we talk about the plugin API, we often talk about action hooks and filter hooks. Actions are is a type of signal that the plugin API emits. Anyone can listen for those signals and react to them.
Filters are a bit different. They’re also a type of signal that the plugin API emits. But that signal also contains data that you can change and send back.
Now while those two types of signals do different things, they’re the same under the hood. They use the same system and much of the same code. That’s why this system won’t distinguish between the two of them. We’re going to simplify the language of the plugin API by replacing them by a single term: hook.
This also means that we won’t always have methods for both types of signals. In most cases, having methods for each is redundant. Using the same term for both lets us remove some of that redundancy.
Why do we need this?
When you try to create classes for WordPress, the plugin API is always hanging over your head. You need it to interact with a lot of WordPress systems in a safe way, but it’s always messy to design around it. That’s why it’s a common topic here.
It’s also too much to build a system to manage this when you’re starting off. Instead, you focus on fixing the problem while designing each of your classes. Each class has its own methods and logic to connect to it.
But this creates a new problem. All your classes end up coupled to the plugin API. If you need to make a change to how all your classes connect to it, you need to change them all.
This is why we need this system. We need to extract this messy logic out into classes and interfaces. This leaves your classes free to only worry about the hooks they want to connect to. The system will take care of the rest.
The responsibilities of an event management system
Now that we’ve justified the need for the event management system. Let’s look at how we’ll break it down the responsibilities of the system. This is always a good exercise to do so that we ensure that each class focuses on a single responsibility.
Interact with the plugin API
The most important job of the event management system is to interact with the plugin API. As we discussed earlier, having each class do it is messy and problematic. To fix this, we need a point of contact between our classes and the plugin API. This will let us extract all the calls to the plugin API out of our classes.
Listen for events
That said, we still need classes to act as event listeners. Removing all the calls to the plugin API doesn’t remove the need for this responsibility. It’s still part of our event management system.
Manage event listeners
We also need a class to manage our event listeners. We want to be able to add and remove listeners. When you add a listener, it’ll register all its hooks with the plugin API. When you remove it, it’ll remove them all.
The big picture
Below is a graph showing all the components of the event management system. You have the event listeners who deal with the event manager when they want to talk with the plugin API. The event manager is the only one allowed to talk with the plugin API. The plugin API then dispatches a signal to the right event listeners when an event occurs and the cycle repeats itself.
Coding our event management system
Now that we’ve broken down the responsibilities of our system, let’s proceed to the next step. We need to transform these responsibilities into working code.
PluginAPIManager class
The first thing that we’ll do is create a class to manage the plugin API. It’ll be our point of contact that we described earlier. We’ll call it PluginAPIManager
to reflect the job that it does.
class PluginAPIManager { /** * Adds a callback to a specific hook of the WordPress plugin API. * * @uses add_filter() * * @param string $hook_name * @param callable $callback * @param int $priority * @param int $accepted_args */ public function add_callback($hook_name, $callback, $priority = 10, $accepted_args = 1) { add_filter($hook_name, $callback, $priority, $accepted_args); } /** * Executes all the callbacks registered with the given hook. * * @uses do_action() * * @param string $hook_name */ public function execute() { $args = func_get_args(); return call_user_func_array('do_action', $args); } /** * Filters the given value by applying all the changes from the callbacks * registered with the given hook. Returns the filtered value. * * @uses apply_filters() * * @param string $hook_name * @param mixed $value * * @return mixed */ public function filter() { $args = func_get_args(); return call_user_func_array('apply_filters', $args); } /** * Get the name of the hook that WordPress plugin API is executing. Returns * false if it isn't executing a hook. * * @uses current_filter() * * @return string|bool */ public function get_current_hook() { return current_filter(); } /** * Checks the WordPress plugin API to see if the given hook has * the given callback. The priority of the callback will be returned * or false. If no callback is given will return true or false if * there's any callbacks registered to the hook. * * @uses has_filter() * * @param string $hook_name * @param mixed $callback * * @return bool|int */ public function has_callback($hook_name, $callback = false) { return has_filter($hook_name, $callback); } /** * Removes the given callback from the given hook. The WordPress plugin API only * removes the hook if the callback and priority match a registered hook. * * @uses remove_filter() * * @param string $hook_name * @param callable $callback * @param int $priority * * @return bool */ public function remove_callback($hook_name, $callback, $priority = 10) { return remove_filter($hook_name, $callback, $priority); } }
If you remove all the documentation, our PluginAPIManager
class is quite small. That’s normal since it’s just a wrapper around the plugin API. That said, there are a few changes worth pointing out.
Using a descriptive language
The change that’ll stand out the most is the language used throughout the class. The documentation, method names and variables names all share the same language. The goal is to better define what each method does to the plugin API.
Gone are words like action or filter. Like we mentioned earlier, actions and filters use the same underlying code. In fact, most action functions are just wrappers around the same filter function. That’s why we introduced the term “hook” earlier. To the plugin API, an action or a filter is just a hook.
But naming a method add_hook
doesn’t describe what it’s doing that well either. We’re not adding a hook to the plugin API. We’re adding a callback which is a function that the plugin API can call at a later time.
This callback gets added to the wp_filter
array. This an array that the plugin API uses as storage. The graph below shows you how the plugin API stores each time you call add_filter
or add_action
.
You’ll notice that there are quite a few elements to it. What matters to us is what’s on the right. We’re storing a callback (function on the graph) and the number of accepted arguments. And that’s what we’re changing most of the time when we interact with the plugin API. That’s why most of the method names use the term “callback”.
Pushing things further
But there are two methods where pushed this new descriptive language to its limits. Those are the execute
and filter
method. These replace the apply_filters
and do_action
functions.
Why not just call these methods apply_callbacks
and do_callbacks
and move on? Because apply_callbacks
and do_callbacks
aren’t good method names either. They also don’t describe what the plugin API is doing that well. This gives us an opportunity to create a clearer language and improve that.
So why call it execute
and not do
? First, we can’t use do
as a method name. That’s because do
is a reserved term in PHP. You need it to define “do-while” loops.
Second, execute
describes what the plugin API does when you call do_action
. It reuses the same term used in the documentation. It says, “Execute functions hooked on a specific action hook.”
filter
has a similar origin as execute
. It also comes from the apply_filters
documentation. That said, its documentation isn’t as clear as the one for do_action
. But, if you read through it, there’s this description of the returned value at the end. It says, “The filtered value after all hooked functions are applied to it.”
This is a great description of what the plugin API is doing when you call apply_filters
. The plugin API is filtering a value by applying changes to it. It’s not applying hooks to a value. So that’s the reasoning behind the filter
name.
More on the execute and filter methods
You might also have noticed that the execute
and filter
methods are a bit different from the rest. They don’t make a direct call to apply_filters
or do_action
. Instead, they use call_user_func_array
to call their respective function.
We have to do that because you can pass as many arguments as you want to apply_filters
and do_action
. This makes it impossible to use method parameters like you would in normal circumstances. Instead, we have to use func_get_args
to get all the arguments passed to our methods. We then pass them all to call_user_func_array
as the second argument.
To support PHP 5.2, we h ave to do this in two steps. That version of PHP won’t let you call func_get_args
insides call_user_func_array
. To workaround that problem, we assign arguments returned by func_get_args
to the args
variable. We then pass args
as the second argument of call_user_func_array
. If you don’t need to support PHP 5.2, you don’t have to do it in two lines like we did in the code sample.
Subscriber interface
The next step is to create a contract for our event listeners classes. It’ll let them define which hooks they want to listen to. Let’s take a look at it.
interface SubscriberInterface { /** * Returns an array of hooks that this subscriber wants to register with * the WordPress plugin API. * * The array key is the name of the 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('hook_name' => 'method_name') * * array('hook_name' => array('method_name', $priority)) * * array('hook_name' => array('method_name', $priority, $accepted_args)) * * @return array */ public static function get_subscribed_hooks(); }
Again, this isn’t a large piece of code. With interfaces, it’s often all in the documentation (so always write good documentation!). It’s where we explain the interface contract. In case you were curious, this contract is almost identical to the one from Symfony.
So what does contract say?
The SubscriberInterface
contract revolves around a single static method called get_subscribed_hooks
. The contract requires that the method return an array. The array itself contains all the callbacks that the class wants to register with the plugin API.
It must also follow a specific format to handle all the possible registration scenarios. These are:
- Registering a class method to a specific hook.
- Registering a class method to a specific hook, but with a different priority.
- Registering a class method to a specific hook, but with a different priority and a different number of accepted arguments.
Why SubscriberInterface?
So far, we’ve only used the term “event listener” throughout this article. Yet we’re calling our interface SubscriberInterface
. Why not call it EventListenerInterface
? Let’s take a moment to explain the name choice.
The name comes from Symfony and Doctrine. Both libraries make the distinction between an “event listener” and an “event subscriber”. To them, an event listener only listens to a single event. Meanwhile, a subscriber listens to several events.
This changes how you look at the array returned by get_subscribed_hooks
. It’s not just an array with all the callbacks that our class wants to register to. It’s an array of event listeners that our class wants to have registered.
EventManager class
We have one final piece of class design left for our event management system. We need to connect our SubscriberInterface
to our PluginAPIManager
class. We’ll explore three ways that we can achieve that.
Standalone EventManager
The easiest way to solve our problem is by modifying our existing PluginAPIManager
class. We’ll just add new methods to it so that it can handle our SubscriberInterface
. That said, to reflect its new job, we’ll rename it from PluginAPIManager
to EventManager
.
class EventManager { /** * Contains all the code from PluginAPIManager */ }
Adding an event subscriber
class EventManager { // ... /** * Add an event subscriber. * * The event manager registers all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->add_subscriber_callback($subscriber, $hook_name, $parameters); } } /** * Adds the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber * @param string $hook_name * @param mixed $parameters */ private function add_subscriber_callback(SubscriberInterface $subscriber, $hook_name, $parameters) { if (is_string($parameters)) { $this->add_callback($hook_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->add_callback($hook_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } }
add_subscriber
and add_subscriber_callback
are the two methods that let you add an event subscriber. add_subscriber
is the public method that others will interact with. It loops through all the callbacks returned by get_subscribed_hooks
. It passes each callback to add_subscriber_callback
.
add_subscriber_callback
does most of the work for registering a callback for a subscriber. It’s a private method that takes in three parameters: subscriber
, hook_name
and parameters
. subscriber
is our SubscriberInterface
object. hook_name
is the name of the hook that subscriber
wants to register to. parameters
contains the callback registration details.
All the logic in add_subscriber_callback
revolves around the parameters
variable. We need to determine which type of registration scenario it contains. Based on the contract that we wrote, it can be one of two things: an array or a string. It’s a string containing the method name if we’re dealing with the first scenario. Otherwise, it’s an array for the other two scenarios.
We start by checking if it’s a string using is_string
. If it is, we pass it to our add_callback
method. The first argument that we pass it is the name of the hook stored in hook_name
. The second is a callable array. It contains our SubscriberInterface
object and the parameters
variable which stores our method name.
If we don’t have a string, we then use is_array
to check if parameters
is an array. If it’s an array, we also want to verify that there’s a value stored at index 0. That’s because, per our interface contract, the method name always needs to be at index 0.
The next line passes the data stored in the parameters
array to our add_hook
method. The first argument is still the hook_name
variable. The second variable is also a callable array. We just replaced parameters
with parameters[0]
since that’s where the method name is now.
Things get a bit more complex here. Besides the method name at index 0, we’re not sure what else is inside the parameters
array. Is it just a priority? Or is it a priority and a number of accepted arguments?
To figure that out, we have to use ternary operators to pass the last two arguments. That way, we can pass default values if the parameters
array doesn’t have anything. The default values that we use are the same as our add_callback
method.
Removing an event subscriber
class EventManager { // ... /** * Remove an event subscriber. * * The event manager removes all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function remove_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->remove_subscriber_callback($subscriber, $hook_name, $parameters); } } /** * Removes the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber * @param string $hook_name * @param mixed $parameters */ private function remove_subscriber_callback(SubscriberInterface $subscriber, $hook_name, $parameters) { if (is_string($parameters)) { $this->remove_callback($hook_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->remove_callback($hook_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10); } } }
Next, we’ll look at how to remove an event subscriber. The code is almost the same as the one used to add an event subscriber. We also use two methods: remove_subscriber
and remove_subscriber_callback
.
They both have the same parameters as add_subscriber
and add_subscriber_callback
. The only difference is in the remove_subscriber_callback
method. It calls our remove_callback
method instead of our add_callback
method.
Complete EventManager class
Before we move on, you might want a copy of the code for this version of the EventManager
class. You can find it below.
class EventManager { /** * Adds a callback to a specific hook of the WordPress plugin API. * * @uses add_filter() * * @param string $hook_name * @param callable $callback * @param int $priority * @param int $accepted_args */ public function add_callback($hook_name, $callback, $priority = 10, $accepted_args = 1) { add_filter($hook_name, $callback, $priority, $accepted_args); } /** * Add an event subscriber. * * The event manager registers all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->add_subscriber_callback($subscriber, $hook_name, $parameters); } } /** * Executes all the callbacks registered with the given hook. * * @uses do_action() * * @param string $hook_name */ public function execute() { $args = func_get_args(); return call_user_func_array('do_action', $args); } /** * Filters the given value by applying all the changes from the callbacks * registered with the given hook. Returns the filtered value. * * @uses apply_filters() * * @param string $hook_name * @param mixed $value * * @return mixed */ public function filter() { $args = func_get_args(); return call_user_func_array('apply_filters', $args); } /** * Get the name of the hook that WordPress plugin API is executing. Returns * false if it isn't executing a hook. * * @uses current_filter() * * @return string|bool */ public function get_current_hook() { return current_filter(); } /** * Checks the WordPress plugin API to see if the given hook has * the given callback. The priority of the callback will be returned * or false. If no callback is given will return true or false if * there's any callbacks registered to the hook. * * @uses has_filter() * * @param string $hook_name * @param mixed $callback * * @return bool|int */ public function has_callback($hook_name, $callback = false) { return has_filter($hook_name, $callback); } /** * Removes the given callback from the given hook. The WordPress plugin API only * removes the hook if the callback and priority match a registered hook. * * @uses remove_filter() * * @param string $hook_name * @param callable $callback * @param int $priority * * @return bool */ public function remove_callback($hook_name, $callback, $priority = 10) { return remove_filter($hook_name, $callback, $priority); } /** * Remove an event subscriber. * * The event manager removes all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function remove_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->remove_subscriber_callback($subscriber, $hook_name, $parameters); } } /** * Adds the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber * @param string $hook_name * @param mixed $parameters */ private function add_subscriber_callback(SubscriberInterface $subscriber, $hook_name, $parameters) { if (is_string($parameters)) { $this->add_callback($hook_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->add_callback($hook_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } /** * Removes the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber * @param string $hook_name * @param mixed $parameters */ private function remove_subscriber_callback(SubscriberInterface $subscriber, $hook_name, $parameters) { if (is_string($parameters)) { $this->remove_callback($hook_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->remove_callback($hook_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10); } } }
Extending PluginAPIManager
Now, we’re going to complicate things a bit. Instead of adding our code to our PluginAPIManager
class, we’re going to extend it. The reason to do this has more to do with design philosophy than anything else. We want to acknowledge that PluginAPIManager
is a valid class with its own job. And that its job might be different from EventManager
.
class EventManager extends PluginAPIManager { /** * Add an event subscriber. * * The event manager registers all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->add_subscriber_callback($subscriber, $hook_name, $parameters); } } /** * Remove an event subscriber. * * The event manager removes all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function remove_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->remove_subscriber_callback($subscriber, $hook_name, $parameters); } } /** * Adds the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber * @param string $hook_name * @param mixed $parameters */ private function add_subscriber_callback(SubscriberInterface $subscriber, $hook_name, $parameters) { if (is_string($parameters)) { $this->add_callback($hook_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->add_callback($hook_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } /** * Removes the given subscriber's callback to a specific hook * of the WordPress plugin API. * * @param SubscriberInterface $subscriber * @param string $hook_name * @param mixed $parameters */ private function remove_subscriber_callback(SubscriberInterface $subscriber, $hook_name, $parameters) { if (is_string($parameters)) { $this->remove_callback($hook_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->remove_callback($hook_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10); } } }
As you can see, this version of EventManager
has the same four methods as our previous version. Their code hasn’t changed at all either. What’s changed is our ability to reuse PluginAPIManager
.
You can now use PluginAPIManager
in other scenarios where you might not care about the SubscriberInterface
. For most of us, that might never be the case. That said, it’s a design decision for you to take.
Using PluginAPIManager as a dependency
Our last version EventManager
is going to push this idea that it has a separate job even further. We’re going to remove the direct relationship between PluginAPIManager
and EventManager
. Instead, EventManager
will use PluginAPIManager
as a dependency.
class EventManager { /** * The WordPress plugin API manager. * * @var PluginAPIManager */ private $plugin_api_manager; /** * Constructor. * * @param PluginAPIManager $plugin_api_manager */ public function __construct(PluginAPIManager $plugin_api_manager) { $this->plugin_api_manager = $plugin_api_manager; } }
This is a big change. Our EventManager
didn’t even have a constructor before. Now, it has one that takes an instance of PluginAPIManager
as an argument.
Focusing on the unique event management language
Because we’re not tied to the plugin API anymore, we don’t have to use its language anymore either. Instead, we can use the language that we described earlier. The one that focuses on event listeners and event subscribers.
class EventManager { // ... /** * Adds the given event listener to the list of event listeners * that listen to the given event. * * @param string $event_name * @param callable $listener * @param int $priority * @param int $accepted_args */ public function add_listener($event_name, $listener, $priority = 10, $accepted_args = 1) { $this->plugin_api_manager->add_callback($event_name, $listener, $priority, $accepted_args); } }
Let’s look at a new method called add_listener
. The method itself isn’t any different from what you’ve seen so far. It’s just a wrapper around add_callback
.
What’s changed is the language surrounding add_listener
. It describes what our event management system is doing and not the plugin API. We’re not adding a callback to a hook. Instead, we’re adding an event listener to a list of event listeners for a specific event.
The parameter names have also changed to reflect this new language. hook_name
is now event_name
. Meanwhile, we changed callback
to listener
.
Updating our SubscriberInterface
This language change also carries over to our SubscriberInterface
. get_subscribed_hooks
isn’t a good method name to use anymore. We’re going to rename it to get_subscribed_events
and update its documentation.
interface 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(); }
We also need to add the SubscriberInterface
methods from our previous versions of EventManager
. That said, we’ll have to make a few changes. These methods were using plugin API terminology which we’re not using anymore.
class EventManager { // ... /** * Adds an event subscriber. * * The event manager adds the given subscriber to the list of event listeners * for all the events that it wants to listen to. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_events() as $event_name => $parameters) { $this->add_subscriber_listener($subscriber, $event_name, $parameters); } } /** * Adds the given subscriber listener to the list of event listeners * that listen to the given event. * * @param SubscriberInterface $subscriber * @param string $event_name * @param mixed $parameters */ private function add_subscriber_listener(SubscriberInterface $subscriber, $event_name, $parameters) { if (is_string($parameters)) { $this->add_listener($event_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->add_listener($event_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } }
As you can see above, we replaced all uses of the term “callback” with “event” or “listener”. We also renamed get_subscribed_hooks
since we changed the method name in the interface. We also rewrote the documentation to use our new event specific language.
Code for this version
Below, you’ll find a copy of the complete version of the EventManager
class.
class EventManager { /** * The WordPress plugin API manager. * * @var PluginAPIManager */ private $plugin_api_manager; /** * Constructor. * * @param PluginAPIManager $plugin_api_manager */ public function __construct(PluginAPIManager $plugin_api_manager) { $this->plugin_api_manager = $plugin_api_manager; } /** * Adds the given event listener to the list of event listeners * that listen to the given event. * * @param string $event_name * @param callable $listener * @param int $priority * @param int $accepted_args */ public function add_listener($event_name, $listener, $priority = 10, $accepted_args = 1) { $this->plugin_api_manager->add_callback($event_name, $listener, $priority, $accepted_args); } /** * Adds an event subscriber. * * The event manager adds the given subscriber to the list of event listeners * for all the events that it wants to listen to. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_events() as $event_name => $parameters) { $this->add_subscriber_listener($subscriber, $event_name, $parameters); } } /** * Removes the given event listener from the list of event listeners * that listen to the given event. * * @param string $event_name * @param callable $listener * @param int $priority */ public function remove_listener($event_name, $listener, $priority = 10) { $this->plugin_api_manager->remove_callback($event_name, $listener, $priority); } /** * Removes an event subscriber. * * The event manager removes the given subscriber from the list of event listeners * for all the events that it wants to listen to. * * @param SubscriberInterface $subscriber */ public function remove_subscriber(SubscriberInterface $subscriber) { foreach ($subscriber->get_subscribed_events() as $event_name => $parameters) { $this->remove_subscriber_listener($subscriber, $event_name, $parameters); } } /** * Adds the given subscriber listener to the list of event listeners * that listen to the given event. * * @param SubscriberInterface $subscriber * @param string $event_name * @param mixed $parameters */ private function add_subscriber_listener(SubscriberInterface $subscriber, $event_name, $parameters) { if (is_string($parameters)) { $this->add_listener($event_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->add_listener($event_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10, isset($parameters[2]) ? $parameters[2] : 1); } } /** * Adds the given subscriber listener to the list of event listeners * that listen to the given event. * * @param SubscriberInterface $subscriber * @param string $event_name * @param mixed $parameters */ private function remove_subscriber_listener(SubscriberInterface $subscriber, $event_name, $parameters) { if (is_string($parameters)) { $this->remove_listener($event_name, array($subscriber, $parameters)); } elseif (is_array($parameters) && isset($parameters[0])) { $this->remove_listener($event_name, array($subscriber, $parameters[0]), isset($parameters[1]) ? $parameters[1] : 10); } } }
Showing you design tradeoffs
So what’s the point of exploring these different versions of EventManager
? Well, it’s about showing you possible solutions to the same problem. This is all part of teaching the design process.
All three version are correct and use pretty much the same code. The differences between them come down to different design philosophies. Which one you use comes down to your personal preferences.
The first version requires a lot less thinking than the last one. But the last one allows you to use a richer and more descriptive language. The first one is more rigid and not as easy to extend. The last one is as flexible and decoupled as we can make it. The second one lives between those two extremes.
That said, everything is up to you in the end. You have to decide how you want to solve the problem. You have to judge how each solution fits your particular context and pick the one that works best within it.
What if you want to chain events?
So far, we’ve avoided a pretty important scenario. How do you call other event listeners within your own? You might want to apply changes to your data or even trigger your own event.
Except that, you can’t! Your classes that use the SubscriberInterface
don’t touch the plugin API anymore. So how can we make our event management system work around that constraint?
Passing the EventManager to our subscriber
There are a couple of solutions to this problem. We won’t look at them all today (I can hear your sigh of relief!). That said, they all have something in common. They pass our EventManager
to our event subscribers.
After all, we worked hard to create a class in charge of the plugin API! We don’t want to change that. So it needs to make it into our event subscribers somehow. So how can we do that?
To solve this problem, we’re going to look at something called “setter injection“. This is a type of dependency injection where you can add a class dependency using a set
method.
This might seem complicated, but don’t worry. You don’t need a strong understanding of dependency injection itself to use setter injection. It comes down to creating a larger contract for SubscriberInterface
.
Extending our SubscriberInterface
So how do we create a larger contract for SubscriberInterface
? We use interface inheritance and extend it. That’s how! This is an ideal scenario to show you this more obscure object-oriented feature.
interface EventManagerAwareSubscriberInterface extends SubscriberInterface { /** * Set the WordPress event manager for the subscriber. * * @param EventManager $event_manager */ public function set_event_manager(EventManager $event_manager); }
So first things first, why name it EventManagerAwareSubscriberInterface
? Good question! We use the term “aware” to highlight a relationship with another class or interface.
In this case, EventManagerAwareSubscriberInterface
wants to highlight the relationship between SubscriberInterface
and EventManager
. Or put another way, EventManagerAwareSubscriberInterface
is “aware” of the existence of EventManager
. That’s why we use the term “aware”.
From a code perspective, EventManagerAwareSubscriberInterface
just adds one new method to the SubscriberInterface
. That’s the set_event_manager
method. It takes an instance of EventManager
as an argument. This is the method that we’ll use for setter injection.
This new method also changes the basic SubscriberInterface
contract in a significant way. Before, we just needed to create the get_subscribed_hooks
static method. And that method just needed to return an array with values in it. It didn’t have much of an impact on the class itself.
But set_event_manager
has a larger impact on how you build classes that implement it. While you can’t put variables inside an interface, you might still need them to make it work and do its job. This is the case here. set_event_manager
requires that you store the EventManager
inside the class. Let’s look at how that can look.
Creating a manager aware abstract class
This is also an ideal scenario for an abstract class. The logic behind set_event_manager
is something that’s reusable by all classes implementing EventManagerAwareSubscriberInterface
. So why not just create an abstract class to contain that reusable logic!?
abstract class AbstractEventManagerAwareSubscriber implements EventManagerAwareSubscriberInterface { /** * The WordPress event manager. * * @var EventManager */ protected $event_manager; /** * Set the WordPress event manager for the subscriber. * * @param EventManager $event_manager */ public function set_event_manager(EventManager $event_manager) { $this->event_manager = $event_manager; } }
That’s what AbstractEventManagerAwareSubscriber
does. It just contains the implemented set_event_manager
method and an internal event_manager
variable. All that the method does is assign the passed EventManager
object to that variable.
Updating our EventManager class
The last thing to do is to update our EventManager
class. That’s where the setter injection will take place. To be more precise, it takes place in our add_subscriber
method.
class EventManager extends PluginAPIManager { /** * Add an event subscriber. * * The event manager registers all the hooks that the given subscriber * wants to register with the WordPress Plugin API. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { if ($subscriber instanceof EventManagerAwareSubscriberInterface) { $subscriber->set_event_manager($this); } foreach ($subscriber->get_subscribed_hooks() as $hook_name => $parameters) { $this->add_subscriber_callback($subscriber, $hook_name, $parameters); } } // ... }
This is what setter injection looks like for our event management system. We just check if the given subscriber implements EventManagerAwareSubscriberInterface
. If it does, we call set_event_manager
and pass it the instance of EventManager
itself. It’s as simple as that!
What about our third EventManager?
You might have noticed already, but it’s worth pointing out in case you didn’t. The code you’ve seen so far doesn’t work with our third version of the EventManager
. That’s because the relationship is different.
Instead of having a relationship with EventManager
, we need it to be with PluginAPIManager
. This makes sense with everything that we’ve discussed surrounding the event management language. EventManager
doesn’t care about the plugin API in that scenario. It’s the PluginAPIManager
that does.
interface PluginAPIManagerAwareSubscriberInterface extends SubscriberInterface { /** * Set the WordPress Plugin API manager for the subscriber. * * @param PluginAPIManager $plugin_api_manager */ public function set_plugin_api_manager(PluginAPIManager $plugin_api_manager); } abstract class AbstractPluginAPIManagerAwareSubscriber implements PluginAPIManagerAwareSubscriberInterface { /** * WordPress Plugin API manager. * * @var PluginAPIManager */ protected $plugin_api_manager; /** * Set the WordPress Plugin API manager for the subscriber. * * @param PluginAPIManager $plugin_api_manager */ public function set_plugin_api_manager(PluginAPIManager $plugin_api_manager) { $this->plugin_api_manager = $plugin_api_manager; } } class EventManager { // ... /** * Adds an event subscriber. * * The event manager adds the given subscriber to the list of event listeners * for all the events that it wants to listen to. * * @param SubscriberInterface $subscriber */ public function add_subscriber(SubscriberInterface $subscriber) { if ($subscriber instanceof PluginAPIManagerAwareSubscriberInterface) { $subscriber->set_plugin_api_manager($this->plugin_api_manager); } foreach ($subscriber->get_subscribed_events() as $event_name => $parameters) { $this->add_subscriber_listener($subscriber, $event_name, $parameters); } } }
Here’s all the code that we’ve seen so far, but modified for our other EventManager
version. There aren’t too many changes to highlight. We replaced all mentions of EventManagerAware
with PluginAPIManagerAware
. That’s for both the interface and the abstract class.
We also renamed the set_event_manager
to set_plugin_api_manager
. The add_subscriber
method calls it, but doesn’t pass itself to the subscriber anymore. Instead, it passes it the PluginAPIManager
instance stored inside our EventManager
class.
Putting our event management system to work
Let’s look at a small example to show you how to use the event management system. We won’t do anything too complicated for this article. We’re just going to create an event subscriber that adds a new error to the login page.
Initializing our event manager
$event_manager = new EventManager();
First, we need to initialize our EventManager
. We’re going to use a simpler version of it without the event specific language. That means that we’ll also use the version of SubscriberInterface
with get_subscribed_hooks
.
Creating our LoginErrorSubscriber class
class LoginErrorSubscriber implements SubscriberInterface { // Empty! }
Here’s our empty LoginErrorSubscriber
class implementing our SubscriberInterface
. Its job is to add support for a custom error on the WordPress login screen. We want it to display a custom error message while also doing the shake animation.
class LoginErrorSubscriber implements SubscriberInterface { /** * Returns an array of hooks that this subscriber wants to register with * the WordPress plugin API. * * @return array */ public static function get_subscribed_hooks() { return array( 'shake_error_codes' => 'add_error_code', 'wp_login_errors' => 'add_error', ); } }
To do that, we need to subscribe to two hooks: shake_error_codes
and wp_login_errors
. We do that through our trusty get_subscribed_hooks
method. It tells the event manager to subscribe two LoginErrorSubscriber
methods with the plugin API. These are add_error
and add_error code
.
class LoginErrorSubscriber implements SubscriberInterface { /** * The error code that our login error subscriber creates. */ const ERROR_CODE = 'my-login-error-code'; // ... }
We’re going to start by adding an error code constant to our LoginErrorSubscriber
class. It stores the custom error code that we want it to manage. We can then use it in the add_error
and add_error code
methods.
class LoginErrorSubscriber implements SubscriberInterface { // ... /** * Add our error to the login errors. * * @param WP_Error $error * * @return WP_Error */ public function add_error(WP_Error $error) { if (!isset($_GET[self::ERROR_CODE])) { return $error; } $error->add(self::ERROR_CODE, 'The error message displayed on the login screen.'); return $error; } }
add_error
is the method that adds the error message that we want to display on the login page. To do that, it uses isset
to inspect the $_GET
global variable to see if our error code is there. If it isn’t, it returns the instance of WP_Error
passed to the method.
If our error code is there, we add our error message to the WP_Error
using its add
method. You need to pass it our ERROR_CODE
constant as the first argument. The second argument is the error message that you want to display. We then return the modified WP_Error
object.
class LoginErrorSubscriber implements SubscriberInterface { // ... /** * Add our error code to the handled login error codes. * * @param array $error_codes * * @return array */ public function add_error_code(array $error_codes) { $error_codes[] = self::ERROR_CODE; return $error_codes; } }
add_error_code
is the other method that we subscribe with the plugin API. Its job is to add our ERROR_CODE
constant to the list of valid error codes for the login page. If we didn’t do this, the login form wouldn’t shake when it displayed our error message. (And why have a login form error if it doesn’t make the form shake!?)
Adding our LoginErrorSubscriber class to our event manager
$event_manager = new EventManager(); $event_manager->add_subscriber(new LoginErrorSubscriber());
The last step is to add our LoginErrorSubscriber
to EventManager
. We just create a new instance of it inside the call to the add_subscriber
method. And this will add your new error message on the WordPress login page.
The foundation of object-oriented WordPress
When you code with WordPress, you can’t escape the need to use the plugin API. This doesn’t change when you want to use object-oriented programming with WordPress either. You just can’t escape it. It’s as much a part of WordPress as the GPL.
The event management system acknowledges this reality. It’s like having a great boss. A great boss lets you do your best work while shielding you from management needs. The event management system shields you from having to always worry about the plugin API. That way, you can focus on designing meaningful classes that don’t have to depend on it.
That’s why it’s the foundation of object-oriented WordPress. And now you can use it now too! You can refer to the code from this article as a starting point.