How to avoid dependency conflicts using Composer and PHP-Scoper

In the world of WordPress, there are a myriad of plugins that offer various out-of-the-box features to help jumpstart a new website. As of January 2022, there are almost 60,000 free plugins available at WordPress.org alone, and then there’s all the paid and bespoke plugins out there that we have no way of accurately counting!

While all these WordPress plugins provide different features, many of them share a lot of low-level developer functionality with others. For example, for the purposes of autoloading PHP classes, many of them might rely on Composer to create a complete and automatically generated autoloader. Or, if the plugin is built by incorporating dependency injection, it might use an already established library such as the PHP League Container or PHP-DI. As of January 2022, there are over 331,000 free libraries registered on Packagist.org alone, and that gives developers a wide array of pre-built tools to handle common tasks.

Why scope anything at all?

It’s a recommended best practice to rely on heavily used, tested, and maintained libraries like the ones mentioned above when working on a software project (WordPress plugins are software projects too!). However, when you use a library, you need to bundle it with your plugin and thus have it installed on all the sites that your plugin is installed and active on. That’s all good when your plugin is the only one activated that uses a certain library, but things can get messy if other plugins use the same library.

Going back to the example of the PHP League Container from above, there was a famous issue in August of 2020 when two very popular plugins, WooCommerce and WP Rocket, clashed and caused many websites to stop working. Basically what happened is that WooCommerce 4.4.0 upgraded their libraries and bundled version 3 of the PHP League Container library. WP Rocket was still using version 2 of the same library (you can read more about it in this GitHub issue).

Since neither WooCommerce nor WP Rocket scoped their dependencies, the two libraries clashed with each other and triggered PHP fatal errors that brought down many sites. The issue was fixed by WP Rocket in version 3.6.4 by scoping their dependencies and WooCommerce releasing a patch that undid the upgrade to avoid such conflicts until they found a way to better handle this.

What is PHP scoping actually?

The way to avoid issues like the one mentioned above entirely is to scope all PHP code used by your plugin. Scoping basically means prefixing all your function names, class names, and constants with something (hopefully) unique to your plugin such that there can be no name clashes with other plugins. For example, here at Deep Web Solutions, we prefix all our plugins’ code with dws_:

function foo_bar() {
    // this is wrong
}

function dws_foo_bar() {
    // this is correct
}

Of course, there can be other WordPress plugin developers out there using the same 3 letters, so to lower the chance of our plugins clashing with others, we also include the plugin’s initials in the prefix. For example, for our Linked Order for WooCommerce plugin, the prefix would thus be dws_lowc_ (as you can check in all the function files on GitHub). This is not a guarantee that no other plugin out there is using the same prefix and ends up with the same function name, but the chances of that are slim, and the chances of both plugins ending up on the same site are even slimmer.

Scoping your own code is easy though — you have control over all of it (and you should’ve been doing it all along), but scoping 3rd-party code is harder. You could download the code of your library of choice, sift through it file-by-file, and prefix all there is to prefix. Then repeat the process every time you want to upgrade the library. It’s a lot of work (and very, very error-prone), but doable and definitely not what we want to do!

Scoping WordPress-centric libraries

Scoping a library such as the above-mentioned PHP League Container is relatively easy because everything has to be scoped. The same is true of any framework-agnostic library, such as Monolog, the PHP-FIG standard libraries, Guzzle, and so on. But sometimes you might need to scope a library that provides deeper integration with a non-scoped framework (such as WordPress).

For example, here at Deep Web Solutions we build our plugins on top of our in-house framework for developing WordPress plugins. Since our framework is designed specifically for use within WordPress, it makes use of WordPress functions and classes. Moreover, we decided early on that we would bundle the framework with each plugin separately and have each plugin load its own version of the framework. That way, even if one website uses two or more of our plugins, there would be no risk of one plugin’s framework being used by the other and thus potentially breaking each other’s functionality (like in the case of WordPress frameworks that detect when more than one version is installed and always load the newest one only).

When we scope our own framework libraries, we need to exclude from scoping all the functions and classes from WordPress itself, for obvious reasons. And we’ve automated that too!

Automating the PHP scoping

We’ll be using Humbug’s PHP-Scoper tool to do the scoping and thus automate the process. Moreover, since Composer is an industry standard and the recommended way to install PHP-Scoper, we’ll be using that to install our dependencies and generate the autoloader. This is a multi-step procedure, and not all steps might be relevant to you. Feel free to skip a step if it doesn’t apply to your use-case.

Internally we’ve compiled a collection of configuration files for all these tasks, hosted here. Throughout the steps below, we’ll be referencing this package as the DWS WordPress configs. When building your own scoping workflow, feel free to use our package or to build your own based on it.

Step 1 – Isolating the WordPress references in our dependencies

As mentioned above, all our plugins come bundled with a version of our WordPress framework and we needed to leave all the functions and classes from WordPress itself un-scoped. To do that, we first had to compile a list of all the used WordPress functions and classes to let the scoping program know to leave those alone.

Obviously, newer versions of WordPress will have more functions and classes than older ones so we have to decide which version of WordPress to target. In the case of our framework, we currently aim for compatibility with WordPress 5.5 and newer, so we can only use functions and classes that were present in the WordPress 5.5 (we don’t have to worry about functions going missing in newer versions due to WordPress’ commitment to backwards compatibility).

It’s easier to work on an example, so let’s consider the DWS Framework Foundations module. There are three things that are important in its composer.json file:

  1. A dependency for the “deep-web-solutions/wordpress-configs” package.
  2. A dependency for the “php-stubs/wordpress-stubs” package at version 5.5.*.
  3. A “post-autoload-dump” script for our “DeepWebSolutions\\Config\\Composer\\IsolateWordPressReferences::postAutoloadDump” configuration script.

The “magic” itself is being done every single time Composer generates the autoloader (so either when asked explicitly to do so, or after an install or update command) by leveraging the capabilities of the “nikic/php-parser” library that the DWS WordPress configs package installs. You can visit that file on GitHub to check the code, but the gist of it is:

  1. Parses all the WordPress stubs to isolate all the functions and classes, and saves them to a wp-stubs.json file.
  2. Parses all the project files to isolate all the functions and classes, then cross-checks them with the WordPress ones.
  3. Saves all the matching function and class names in a new file called wp-references.json.

You can view the current result on GitHub for the Foundations Module. By doing it like this, each of our framework’s modules comes with a list of functions and classes that come from WordPress. We use that list for scoping purposes, but we think it’s good to have either way!

Step 2 – Preparing a PHP-Scoper config file for each library

It’s good practice to provide PHP-Scoper with a configuration file for every library that you intend to scope. If you’re unfamiliar with these files, you should read up about them in the official documentation. Basically, they let you get more granular control over the scoping process by letting you define which files should be scoped (e.g., we usually ignore stuff like test files), or by letting you define custom patchers which are functions to customize the result of the scoped code.

We already have a few PHP-Scoper configuration files in our DWS WordPress configs package for some of the libraries we use ourselves, such as the Guzzle HTTP client, the PHP-DI dependency injection container, and, of course, our own plugin development framework. If you check the config file for our framework, you’ll notice that we have a patcher registered for making use of the wp-references.json file in order to leave those un-scoped.

You can check out our config files here and either use them as well or use them as the basis for your own.

Step 3 – Requiring your libraries and defining the autoload rules

When using Composer, each required package defines its own autoloading rules. Then, upon generating of the autoloader, Composer will ensure that all the rules from all your packages are included. Unfortunately, it has no way of knowing of your own scoped files. Moreover, you don’t actually want to include the autoloading rules of the original packages — you’re using your own scoped ones!

For the second problem, there’s an easy solution. By including your required libraries in the “require-dev” block instead of the “require” block of your plugin’s composer.json file, they will not be installed in your production version of the plugin, and thus they also won’t be included in the autoloader.

For the first problem, however, we need to do some grunt work. Basically, the original authors of the packages already provided us with the autoloading rules in their own composer.json files, but we need to adjust them to our new scoped files.

Again, it’s easier to work with an example, so let’s look at our own Linked Orders for WooCommerce plugin. By inspecting the “autoload” block of its composer.json file, you should notice that we included all the autoloaded files from the dependency packages in our own “files” block, and also included all the PSR-4 rules in our “psr-4” block. The only difference is that the single files now point to our dependencies directory, and the PSR-4 classes are all prefixed by the “DWS_LOWC_Deps” namespace and, also, point to files in the dependencies directory.

It might take a bit of experimentation to get your autoloading rules together, but it should all come together pretty easily if you check the original autoloading rules of your packages.

Step 4 – Bringing it all together

Because it’s easier with an example, let’s continue examining the composer.json file of our Linked Orders for WooCommerce plugin (from step 3). As with step 1, the entire thing is orchestrated by a script from our DWS WordPress Configs package, namely the DeepWebSolutions\\Config\\Composer\\PrefixDependencies script.

There are two important thigs that this script does:

  1. During Composer’s “pre-autoload-dump” event, it makes sure that all the autoloaded files exist. Since the scoped dependencies have not been created yet, it’s easy for Composer to run into an error when trying to bootstrap the scoping script due to missing autoloaded files.
  2. During Composer’s “post-autoload-dump” event, it calls the custom-named “prefix-dependencies” script. In the case of this file, that calls two other scripts “prefix-dependency-injection” and “prefix-dws-framework”. There’s no technical reason for splitting it like this – we just thought it was more organized that way.

Ultimately, if you inspect the two “prefix-dependency-*” scripts, you’ll notice the call to Humbug’s PHP-Scoper. After having isolated the WordPress references, prepared the config files, and set up all the scripts, Composer will auto-magically scope all our dependencies on every install, update, and dump-autoload command.

Summary

It’s not a particularly easy workflow, but it provides immense value for releasing production-ready plugins, and it can be customized to fit your exact needs. If you’re still unsure about the steps above, I encourage you to first clone our free Linked Orders for WooCommerce plugin from GitHub and run a composer install command to see it all in action.

Leave a Comment