How a Conversation About Filterable Autoloader Paths Pleasantly Derailed My Sunday Afternoon

My good friend Gary Kovar posed a really interesting question on Twitter yesterday:

This led to some conversation both on Twitter and separately on Slack, and I began to realize that this dovetails nicely into that series about re-envisioning PHP development for WordPress I’ve been alluding to for months. Consider this post the first in that series.

Dependency Management in WordPress

Here is a real-life problem I encountered on a client project while I was working in agency: the client was using one plugin to save their media uploads to Amazon S3, and they wanted another custom plugin developed that, when creating a new site in WordPress multisite, a hook would be triggered that creates a new bucket on S3 for that site’s media.

The client had one of the most sophisticated build, testing, and deployment setups for WordPress I have encountered yet in my career. They were using Composer for installing their WordPress plugins, and so naturally, when I began building this custom plugin, I created a composer.json file and required the latest version of the Amazon SDK as a dependency, then got to work on building out the functionality to automatically create the buckets.

Everything was going great. That was, at least, until I got to the point where I’d actually attempted to install my plugin alongside all of the other plugins in their multisite installation. It turned out that the plugin that was uploading media to Amazon was using an older version of the SDK, whereas I had coded my plugin against a new version. When their build processes attempted to pull my plugin into the site alongside the other one, everything appeared to work correctly because the other plugin was shipping the Amazon library alongside its code, instead of downloading it using Composer.

If you’re not familiar with Composer, typically what happens when two libraries (or in this case, WordPress plugins) require incompatible versions of the same library, Composer will exit the install process and inform you of the incompatibility. In this case, because the other plugin wasn’t using Composer, I wasn’t aware of the incompatibility.

There’s an additional wrinkle here, too, because in WordPress, the core application does not have a process to check for and resolve all of the dependencies within a plugin or theme. Thus, if one plugin is installing a library via Composer, and another one ships it with the plugin, it’s possible that whichever plugin loads first will load the dependency. In a well-coded plugin, developers guard against loading a library again if it’s already been loaded to avoid throwing errors in PHP, and well written libraries (such as Amazon’s) can avoid throwing errors if there’s good compatibility in their APIs across versions.

This was the case for this particular project. Ultimately, I came to discover that my code wasn’t working correctly because the other plugin’s library was loading first. We wound up forking that library, checking out its beta version (which used the newer version of the SDK and was nearly released), and went on our merry way.

Why is this a WordPress problem?

The short answer is that it’s complicated. Nearly 40% of the top 10 million websites on the internet today are powered by WordPress. Not every WordPress site is maintained by a developer – in many cases, they’re maintained by non-technical people for whom WordPress was an intuitive solution to help them get their vision online. Those folks don’t care about Composer or PHP dependency resolution or namespace collisions. They (rightfully) just want things to work.

For the rest of us, this means we try to come up with innovative solutions to be able to use the tools that make our jobs easier. Because WordPress core doesn’t have a mechanism for resolving a plugin and theme’s dependencies before it loads them, as plugin developers who work in sophisticated ecosystems like that of my former client, we have to rely on alternative solutions like Mozart or Imposter to package our vendor directory with our project and prefix its class namespaces with our own to avoid naming collisions in PHP.

There has to be a better way, but it’s clear that whatever the solution might be, it’s neither easy nor obvious, or as a community, we may have resolved it by now.

Let’s Talk About Extensibility

So, back to Gary’s question and the conversation that followed, which still pertains to workarounds for the dependency management issues in WordPress, but also includes some nuances around class inheritance, interfaces, and incorporating developer guardrails into open-source projects that I find particularly compelling.

In our conversation, we were discussing class property and method visibility in PHP, and Gary shared with me this filter in the MySQL class of SquareOne, an open-source framework developed by the fine folks at Modern Tribe. Though not about autoloading, I’m hoping the correlation here is clear. Let’s look at this snippet (condensed from the link above):

class MySQL implements Backend {
	const DB_TABLE = 'queue';

	private $table_name;

	public function __construct() {
		global $wpdb;

		$table_name = $wpdb->base_prefix . self::DB_TABLE;

		/**
		 * Filter the table name used for queues on this backend.
		 *
		 * @param string $table_name Table name with $wpdb->prefix added
		 */
		$table_name = apply_filters( 'tribe/queues/mysql/table_name', $table_name );

		$this->table_name = $table_name;
	}

There are two important bits in this example:

  1. The private keyword on the $table_name variable
  2. The call to apply_filters, which provides developers with the ability to override the default value of $table_name

Historically, actions and filters in WordPress have been offered as an easy way to override the default behavior of the code that plugin authors write. Like the end users of WordPress itself, developers who work with the platform have a broad range of experience – especially when it comes to object-oriented programming – and these hooks offer an arguably simpler way of overriding the original programmer’s logic.

A neat thing about PHP, however, is that it also accommodates for changes. Consider this alternative to the snippet above:

class MySQL implements Backend {
    const DB_TABLE = 'queue';

    private $table_name;

    public function __construct() {
        global $wpdb;

        $this->table_name = $wpdb->prefix . self::DB_TABLE;
}

But Jeremy… you just took out the filter.

You got me. :)

In this case, MySQL is instantiated somewhere, and it implements an interface, which means it has a whole set of methods that can be called on it. Instead of filtering just the table name, Modern Tribe could consider allowing for modification of the entire class by filtering the places where it is instantiated, and requiring an interface in that callback.

Here’s a contrived example. Let’s say I’m a developer who wants to extend the MySQL class of SquareOne in my own plugin. Let’s assume, for now, that I only want to override the value of the table name. I could create a class that looks like this:

use Tribe\Libs\Queues_Mysql\Backends\MySQL;

class MyPersonalMySQL extends MySQL {
    protected $table_name;

    public function __construct() {
        global $wpdb;

        parent::__construct();

        $this->table_name = $wpdb->prefix . 'my_personal_queue';
    }
}

Notably, PHP allows you to weaken the access level of class properties and methods for extending classes. Thus, it’s okay here to change the visibility of $table_name from private in the parent class to protected here. Additionally, because I only want to change the table name here, my class can inherit all of the other default behavior from the MySQL class. That said, this does complicate both my approach and Modern Tribe’s. In their case, if they want me to continue to be able to override the class, they have to add a filter to wherever that object is being instantiated. It’d look something like:

$mysql = apply_filters( 'tribe/queues/mysql', new MySQL() );

And I, of course, would need to add the filter myself, AND extend the class, when I could have just filtered the table name in the first place:

add_filter( 'tribe/queues/mysql', new MyPersonalMySQL() );

Versus:

global $wpdb;

add_filter( 'tribe/queues/mysql/table_name', $wpdb->prefix . 'my_personal_queue' );

If extensibility is the goal, then there’s an argument to be made that we as open-source developers could be creating more filters in order to accommodate both procedural and object-oriented developers alike. However, in my mind, the argument for making codebases easier to understand and to raise the bar for WordPress developers alike lies in interfaces.

A Case for Interfaces

In his tweet, Gary asked if there were any open-source examples where the path to a Composer class autoloader is filterable. Using the previous examples as a guide, let’s examine what plugin instantiation might look like from the perspective of a WordPress developer who wants to modify plugins with their own autoloaders. Consider the following plugin:

/**
 * Plugin Name: Incredible Plugin
 * Description: Does something incredible!
 */

namespace JMichaelWard\IncrediblePlugin;

require_once __DIR__ . '/src/IncrediblePlugin.php';

function init( Autoloadable $plugin ) {
    $plugin->init();
    
    return $plugin;
}

add_action( 'plugins_loaded', function() {
    $plugin = apply_filters( 'init_incredible_plugin', new IncrediblePlugin( __FILE__ ) );
    init( $plugin );
} );

The plugin itself might look like this:

namespace JMichaelWard\IncrediblePlugin;

class IncrediblePlugin implements Autoloadable {
    private $plugin_file;

    public function __construct( $plugin_file ) {
        $this->plugin_file = $plugin_file; 
    }

    public function init() {
        $this->autoload();
    }

    public function autoload() {
        require_once plugin_dir_path( $this->plugin_file ) . '/vendor/autoload.php';
    }
}

The beauty here is that this code accomplishes a couple of things.

First, the init method in the main plugin file uses a type-hint against the Autoloadable interface. In this scenario, this means that another plugin author who needs to modify this plugin’s primary class in some fashion could do so simply by extending the IncrediblePlugin class (since it already implements Autoloadable), or that – if somehow needed – they could pass in an entirely new plugin object that meets the requirements of the behaviors for the rest of the plugin, so long as it also extends Autoloadable. If Gary simply didn’t want the autoloader to run, he could override the method and simply have his new class do nothing.

Second, what this achieves – and this is notable because no concept like it exists in WordPress core today – is that because the init method in the main plugin file uses a type-hint, if another developer extended it and forgot to implement that interface, PHP will throw a fatal error and warn that developer that they did not meet the requirements to initialize the plugin.

Believe it or not, errors are good. Great, even. Or, I should clarify, that’s the case when they are part of the development process. Errors provide context for what you haven’t done correctly, and through the process of creating interfaces for your classes, you are establishing a contract for what is required to alter that application, whether you’re the one doing the alteration, or if you’ve created a developer API intended for others to extend.

Conclusion

In this post, I covered the following topics:

  • Challenges associated with dependency management in WordPress
  • Proposed approaches for adopting broader filter usage
  • Weakening restrictive property and method visibility during class extension
  • A case for using PHP interfaces

I expect that a few of these topics will surface again in future editions of this series. For instance, the benefits of interface usage in software development are a significant part of the first 5 principles of S.O.L.I.D. Type-hints, too, help ensure that the values we pass into functions throughout our codebases are of the type we expect, and using them can really help us spot errors during the course of developing new features.

I’ll close this inaugural post in the Rethinking PHP Development in WordPress series by thanking Gary once again for the discussion that helped get my wheels turning. As I seek one or more mentors to increase my own knowledge in 2021, I’m reminded that our friends, colleagues, and peers are often the best mentors of all.