Thoughts on Composer, dependency management, and “practical” autoloading in WordPress

As Tomodomo (my company that builds websites for magazines) grows, I’m spending more and more of my time thinking through “bigger picture” technical architecture decisions, as opposed to actual implementation. I try hard to make sure our technical architecture follows broad industry best practices, while leveraging the best that WordPress has to offer.

In the broader world of PHP development, dependencies are typically managed with a tool called Composer. Composer is awesome; like many similar dependency managers (Bundler, npm, yarn, etc.) it allows you to define a list of dependencies your project requires and quickly install them from a number of sources (package repositories, source control repos, direct downloads of zip files, etc).

There are even ways you can leverage Composer in a WordPress workflow. WordPress can be installed into a particular folder in your project (we use public/wp/), plugins can be installed from your own Git repos or the WordPress Plugin Directory via wpackagist.org, etc.

For many sites that want a “standard” WordPress experience, while being able to lock versions of their plugins and Core (capital-C, meaning WordPress Core), this is typically enough. However, to truly borrow and learn from the rest of the PHP ecosystem, you probably also want to bring in third party code libraries.

In our work, we regularly want to pull in third-party libraries to use in our code. I’m a Stringy fanatic, for instance. I love Carbon for handling dates and date-related math. QueryPath has saved me from DOMDocument hell on multiple occasions.

If you’re not too worried about distributing any custom plugins on a project, you can probably stop reading here. We’ve been in this position almost exclusively since we started adopting Composer (which is why I haven’t given it much thought until recently). We include Composer’s autoloader at the top of our wp-config.php file in a project, and whenever we need to reference a dependency in a custom plugin embedded within a plugin, we just reference it. We know the autoloader was already active in wp-config.php, and we knew the plugins weren’t intended for use outside the context of this project and its repo, so who cared?

We sure didn’t, but over the past few months I’ve started looking at ways that we could release more open source code — particularly WordPress plugins — which has revealed a complex binary between situations where you want to resolve dependencies at the project level (great when building a website where your plugins and Core are also managed via Composer) and when you want to resolve dependencies at the plugin level (when you want to bundle a WordPress plugin for distribution).

How it’s usually done

Typically, WordPress plugins that require third-party dependencies optimise their workflow for packaging and distribution. They will either…

  1. Download the dependencies, possibly stripping out the parts they don’t need and maybe re-namespacing them, and adding them alongside their own plugin code
  2. Managing the dependencies in development with Composer, but committing the vendor folder to their repo (diffs be damned)
  3. Adding an extra build step before pushing their plugin to the WordPress Plugin Directory (or anywhere else for distribution)

For us, Option 1 was never viable. While it probably works fine in practice much of the time, having to make sure your plugin’s embedded dependencies are properly incorporating upstream changes sounds (to me at least) like a nightmare… not to mention a giant waste of time. This is the exact problem that dependency managers are intended to solve, after all!

Option 2 has its own downsides too. Even if you isolate your dependency update commits and just ignore those diffs, committing the vendor folder means your git repo’s file size will grow beyond your control. The vendor folder in one of our projects is around 50 MB; imagine if we had to store deltas and track history for all those files… woof.

Option 3, adding a build step to prepare a plugin for “normal” distribution, was reasonable for us — but the “normal” distribution method is only a fallback for us. The default, preferred approach is to have the plugin define dependencies in its composer.json, require the plugin in our project’s composer.json, and let the project-level composer.json resolve the dependencies that different plugins (or our “app”) might have.

We need to be able to accommodate cases where a plugin might go through a build step, but also might have its dependencies managed at a project level.

How do we do it? I don’t have the answer, but I might have an answer…

Conditionals

After messing around with “intelligent” approaches to detecting whether or not Composer is loaded, I came up with a simpler — and comparatively dumber — idea. What if we just define a constant after including Composer’s autoloader in our project’s wp-config.php? And then in each plugin, simply check if the constant is set and conditionally load the plugin’s Composer autoloader based on the result of that check?

It might look a little something like this:

wp-config.php

<?php

require_once( ABSPATH . '../../vendor/composer/autoload.php' );
define( 'IS_AUTOLOADING', true );

// More wp-config stuff

my-plugin.php

<?php
/**
 * Name: My Plugin
 * ...
 */

if ( defined( 'IS_AUTOLOADING' ) && ! IS_AUTOLOADING ) {
    require_once plugin_dir_path( __FILE__ ) . 'vendor/composer/autoload.php';
}

// More plugin stuff

There are some obvious benefits to this approach:

  • You can easily bundle your plugin’s vendor folder for distribution, and if someone installs it via “normal” WordPress mechanisms (uploading the zip or adding it through the WordPress Plugin Directory), everything should, theoretically, Just Work™.
  • You don’t waste resources actually checking to see if a Composer autoloader is running. Trying to find if a Composer autoloader is active can be tedious, can require you to know where the autoloader is actually located (which is configurable in a project’s composer.json), etc.

And of course, there are downsides:

  • The IS_AUTOLOADING constant is not a standard. If someone is attempting to load your plugin via a project-wide composer.json but isn’t setting that constant, your plugin could try to load its own autoloader and get confused. (It might be worth adding an additional file_exists check for your autoloader, if you don’t mind the slight overhead.)
  • Corollary: IS_AUTOLOADING being true does not guarantee that autoloading is in fact happening. A bit of trust is required there.
  • If someone has multiple plugins installed the “normal” way, with multiple autoloaders and vendor folders in parallel, there could be dependency issues because there’s no project-wide dependency conflict resolution to sort out the situation. This isn’t really different than the current state of play in WordPress, but just to be clear: this solution doesn’t solve that problem either.

If you’re developing a plugin that you intend to begin by developing within a project (a common way to start when you know you want to open source a particular plugin you’re building for a specific project, but you’re not ready just yet) you can add your plugin-specific composer.json and then use Composer’s path repository method to require your nested plugin code in your project-wide composer.json. Nice and easy!

Will it work…?

To be honest, this blog post isn’t a “this is how we solved this!” blog post, but more of a “what if this solved this?” type of post. We haven’t tried this in a production environment yet, and there may be other issues I haven’t considered. (There likely are.)

But I think it’s definitely worth considering, and I’d love to hear the community’s thoughts. If you’re interested in autoloading, dependency management, WordPress, and tying it all together in a way that preserves the separate boundaries that are key for WordPress, feel free to shoot me an email (chris@tomodomo.co) or leave a comment on this post. I’d love to get your feedback!