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

Designing classes that use the WordPress plugin API

The plugin API is one of the cornerstones of WordPress development. There’s no understating its importance. That’s why it’s such a common WordPress development topic (even here).

This importance doesn’t stop when you start using object-oriented programming. If anything, it gets worse! There are quite a few challenges with using the plugin API with object-oriented programming. The first one often being “What do I do with them?

It’s the type problem that can stop you in your tracks. You want to learn to use object-oriented programming, but the plugin API gets in your way. You get frustrated with it and go back to what you’re comfortable with. Lucky for you, we’ve already explored it here (using the same link for emphasis!).

So we know how to handle WordPress hooks in a class. That’s good, but, now, we’re going to dig a bit deeper. We’re going to start thinking about the jobs of our classes. Where does the plugin API fit when we start thinking about the responsibility of our classes?

Listening to WordPress

In most cases, the job of a class is something concrete. They talk to the WordPress REST API, manage WordPress posts, etc. The plugin API doesn’t fit that well in the context of those jobs.

That said, there are times where a class needs to just listen and react to what WordPress is doing. In those situations, the plugin API goes from an awkward fit to a necessary component of the job of our class. We call these types of classes “event listeners” or “event handlers”. But before getting to those, let’s talk about events in general.

The classical definition of events

In event-driven programming, an event is an action that an application can recognize. Anything can trigger them. It could be the application itself, but also the user by clicking on a menu.

These user-generated events are one of the main reasons that event-driven programming exists. That’s because one of its goals is to make software feel interactive. And nowhere is that more important than with graphical user interfaces.

That said, WordPress isn’t an application with a graphical user interface. At least, it isn’t in the regular sense. So how do events work in that context?

Events in WordPress

In the WordPress world, events don’t work the same as in event-driven programming. That’s because of WordPress is a server-side application. It can only react to events that happen while it’s processing an HTTP request.

It can’t react to a mouse click or a keypress. That’s all handled with JavaScript on the client-side. WordPress isn’t alone with this issue. It affects all standard PHP applications. It’s also in part why the “model-view-controller” architectural pattern doesn’t work well with WordPress.

So what are events in the context of a server-side application? In that context, they’re closer to a signal than an event in the traditional sense. A signal is a type of event that interrupts the normal execution of an application. It only resumes once the application has finished processing the signal.

They serve as a form of application-wide communication tool. They notify other parts of the application when something happens. And these other parts of the application can listen for these signals and react if they wish to.

What do event listeners do?

Now that we’ve talked a bit about events. Let’s look at event listeners. What’s their role in all this?

In traditional event-driven programming, event-listeners respond to keystrokes, mouse clicks, screen touches, etc. Their goal is to make an application feel responsive. (This is a bit of a simplification, but it’s still a predominant goal.)

For server-side applications (like WordPress), there are no real-time events to react to. Instead, they’re used as an extension mechanism for the application. The event listeners react to events so that they can make changes to the application.

The idea of aspect

To design event listeners for WordPress, we have to expand our view of responsibility. That’s because it’s harder to define the job of a WordPress event listener. It’s not as concrete as, let’s say, an API client. But you can still do it if you start thinking of event listeners as classes in charge of an aspect of an application.

What’s an aspect of an application? An aspect is a term used to define a job that touches several parts of an application. It’s a way to define jobs that are more abstract like “handling login errors”.

That’s why these classes need to listen for signals sent by the application. It’s how they can coordinate the work that they need to do. Below is a graph that shows this idea in WordPress.

Aspect Graph

On the right, we have a snapshot of the WordPress process. It starts and will emit signals to the plugin API. The plugin API then dispatches these signals to our event listener. It can then responds to the plugin API who communicates back to WordPress.

That’s what an aspect does in a nutshell. It’ll listen to specific signals from the application and do its job. If it needs to communicate back, it does so. That’s it.

Let’s look at a small example to put things in perspective further.

Logging events

Let’s imagine that you have a plugin that you’re distributing online. Someone that wants to use it comes to you and says, “I installed your plugin, but it doesn’t work!”. If you’ve ever distributed a plugin, this is something that’s bound to happen.

The frustrating thing is that we often have no idea what’s going on that person’s site. It isn’t our development environment after all. We can’t test things as we would there.

You might ask them for site access so you can check things on your own. But that’s not always possible. So is there anything else we could do about it?

Well, we could create an event listener that records specific events for us. It could then email us a log of those events. That way we’d have a better idea of what’s going on while our plugin is running on another site. This would help us troubleshoot issues like the one mentioned earlier.

Notice that this isn’t a concrete job. Our class is taking care of “logging” for the plugin. That’s the idea of aspect that we talked about earlier.

Creating an event listener

class LoggingListener
{
}

We’re going to start with our event listener right away. We’ll name it LoggingListener since it’s in charge of “logging events”. While it’s empty right now, we know enough to create our constructor for it.

Constructor

So what do we know that lets us work on the constructor right away? Well, we know that we need an email addresses to send the log to. We also need a variable to store the log entries that we’ll send to those email addresses.

class LoggingListener
{
    /**
     * Email addresses to send the log entries to.
     *
     * @var array
     */
    private $emails;

    /**
     * Log entries.
     *
     * @var array
     */
    private $log_entries;

    /**
     * Constructor.
     *
     * @param array $emails
     */
    public function __construct(array $emails)
    {
        $this->emails = $emails;
        $this->log_entries = array();
    }
}

Here’s our LoggingListener class with a constructor with two class variables: emails and log_entries. emails is the list of all the email addresses that it’ll send the logs to. Meanwhile, log_entries is the array that’ll store the entries.

That said, our constructor only takes one argument. That’s the list of emails. There’s no need to prepopulate our listener with log entries.

class LoggingListener
{
    // ...

    /**
     * Initialize the logging listener with WordPress.
     *
     * @param array $emails
     */
    public static function init(array $emails)
    {
        $self = new self($emails);
    }
}

We’ll also need a custom constructor since an event listener depends a lot on the plugin API. Our init static method is going to be in charge of that. It takes the same emails argument as our constructor. We pass that argument to new self which creates an instance of LoggingListener.

Our init static method is pretty barebone at the moment. But that won’t last for long!

Emailing the log entries

This is a bit unusual, but we’re going to start by looking at the last step that our LoggingListener needs to do. That’s email all the log entries to our list of email addresses. That’s because it’s the only step that doesn’t depend on the events we want to watch.

class LoggingListener
{
    // ...

    /**
     * Email all our log entries to the registered email addresses.
     */
    public function email_log_entries()
    {
        if (empty($this->log_entries)) {
            return;
        }

        $message = "Log Entries:\r\n\r\n";

        foreach ($this->log_entries as $log_entry) {
            $message .= "$log_entry\r\n";
        }

        foreach($this->emails as $email) {
            wp_mail($email, sprintf('Your plugin logs [%s]', date('Y-m-d H:i:s')), $message);
        }
    }
}

email_log_entries is the method that’ll be in charge of sending those emails. It starts by checking if our log_entries array is empty or not. We don’t want to send anyone anything if it’s empty.

If it’s not empty, we start crafting our message. We start by giving it a header. You might be wondering what \r\n means. These are ASCII characters used to define a line break. So for our header, we’re adding two new lines to add a space between our header and our log entries.

Next, we need to add our log entries to the message variable. We do that by looping through them all. The loop appends each log entry to message with a line break.

The last step is to go through all our list of email addresses and email them the logs. We’ll use the built-in wp_mail function to send it. The title of the email is Your plugin logs [%s]. The %s is the timestamp inserted by sprintf function.

When do we email the logs?

Now that we have our email_log_entries method, we need to find a time to use it. The best time to do it is right before PHP shuts down. That way, LoggingListener can log event until the last possible moment.

class LoggingListener
{
    // ...

    /**
     * Initialize the logging listener with WordPress.
     *
     * @param array $emails
     */
    public static function init(array $emails)
    {
        $self = new self($emails);

        add_action('shutdown', array($self, 'email_log_entries'));
    }
}

This is the updated version of our init static method. We a call to add_action. It hooks our email_log_entries method to the shutdown hook mentioned earlier.

Logging an option change

At this point, our LoggingListener class doesn’t log any event. We just coded the logic around emailing those logs when the WordPress process terminates. Let’s start looking into a theoretical event that we might want to log: an option change.

Picking the right hook

You might want to track when options change in a WordPress installation. There are a lot of hooks we can use to check this. That said, there’s one stands out more than the others. It’s the updated_option hook.

Why pick that hook? It’s because it’s the hook that runs after WordPress updated the option. Before that, another plugin might change the option value. We’d log inaccurate information. But at that point, we know that’s the value WordPress stored in the database.

The updated_option hook passes three variables to the registered hook functions. You have option which is the option name. The old_value and value which are the previous and new option value.

Our logging method

class LoggingListener
{
    // ...

    /**
     * Log the details of an option change.
     *
     * @param string $option
     * @param string $old_value
     * @param string $value
     */
    public function log_option_change($option, $old_value, $value)
    {
        $old_value = maybe_serialize($old_value);
        $value = maybe_serialize($value);

        $this->log_entries[] = sprintf('Option "%s" changed from "%s" to "%s".', $option, $old_value, $value);
    }
}

log_option_change is the method that’ll log an option change for LoggingListener. It uses the three parameters that we described earlier. It logs the option change in our log_entries array using sprintf.

You’ll notice that we pass both values through the maybe_serialize function beforehand. That’s because we want to make sure that we log a string value. old_value and value could be anything when WordPress passes us those arguments.

Logging only our option changes

Now, it’s possible that you don’t want to log every option change going on in WordPress. That can be a bit excessive. Instead, you might not want to log every option change for your plugin.

class LoggingListener
{
    // ...

    /**
     * Log the details of an option change.
     *
     * @param string $option
     * @param string $old_value
     * @param string $value
     */
    public function log_option_change($option, $old_value, $value)
    {
        if (0 === stripos($option, 'myplugin_')) {
            return;
        }

        $old_value = maybe_serialize($old_value);
        $value = maybe_serialize($value);

        $this->log_entries[] = sprintf('Option "%s" changed from "%s" to "%s".', $option, $old_value, $value);
    }
}

This isn’t hard to do if you’re prefixing your options. (You’re doing that, right!?) You just need to make a small change to log_option_change. That’s what’s shown above.

We do a small check at the beginning of the method using strpos. It checks if the option name starts with myplugin_. If it does, strpos will return the starting index of the string which is 0. That’s why we have to use === and not == as the comparison operator.

Registering our logging method

class LoggingListener
{
    // ...

    /**
     * Initialize the logging listener with WordPress.
     *
     * @param array $emails
     */
    public static function init(array $emails)
    {
        $self = new self($emails);

        add_action('updated_option', array($self, 'log_option_change'), 10, 3);
        add_action('shutdown', array($self, 'email_log_entries'));
    }
}

All that’s left is to update our init static method. We added the code to register log_option_change. It’s added to updated_option hook that we mentioned earlier.

A new way to think about the plugin API

Events and event listeners are a powerful concept. They make you rethink how you can use hooks in your code. The plugin API becomes more than just a tool that you use to extend WordPress or that others use to extend your code.

With this idea of aspect, you can design around them in ways you might not have before. You just need to look back at our LoggingListener class to see this. You could add hooks to your code for the sole purpose of logging events you care about.

That’s an important realization. So much so that it won’t be the last time that you hear of it. We’ll keep exploring it in the future. For now, you can find the complete LoggingListener class below.

class LoggingListener
{
    /**
     * Email addresses to send the log entries to.
     *
     * @var array
     */
    private $emails;

    /**
     * Log entries.
     *
     * @var array
     */
    private $log_entries;

    /**
     * Constructor.
     *
     * @param array $emails
     */
    public function __construct(array $emails)
    {
        $this->emails = $emails;
        $this->log_entries = array();
    }

    /**
     * Initialize the logging listener with WordPress.
     *
     * @param array $emails
     */
    public static function init(array $emails)
    {
        $self = new self($emails);

        add_action('updated_option', array($self, 'log_option_change'), 10, 3);
        add_action('shutdown', array($self, 'email_log_entries'));
    }

    /**
     * Email all our log entries to the registered email addresses.
     */
    public function email_log_entries()
    {
        if (empty($this->log_entries)) {
            return;
        }

        $message = "Log Entries:\r\n\r\n";

        foreach ($this->log_entries as $log_entry) {
            $message .= "$log_entry\r\n";
        }

        foreach($this->emails as $email) {
            wp_mail($email, sprintf('Your plugin logs [%s]', date('Y-m-d H:i:s')), $message);
        }
    }

    /**
     * Log the details of an option change.
     *
     * @param string $option
     * @param string $old_value
     * @param string $value
     */
    public function log_option_change($option, $old_value, $value)
    {
        if (0 === stripos($option, 'myplugin_')) {
            return;
        }

        $old_value = maybe_serialize($old_value);
        $value = maybe_serialize($value);

        $this->log_entries[] = sprintf('Option "%s" changed from "%s" to "%s".', $option, $old_value, $value);
    }
}
Creative Commons License