Using Composer with patches

A few days ago I wrote about a WordPress plugin that was broken under PHP 8.1. We need to patch it in a sustainable way in production until the vendor issues a fix. Because we deploy with Composer in a continuous deployment environment, it’s not so simple as just commiting the change or (gasp) modifying the file on the server.

One approach would be to create a git copy of the plugin in our private GitLab repository, commit the changes there, and then deploy that version instead. There’s a lot of overhead involved in that approach:

  1. Create the repository, including (optionally) using svn2git to import the plugin from the WordPress plugins repository. You can also just commit the current state.
  2. Add the plugin to our Satis-based Packagist repository.
  3. Redeploy referencing that repository instead of the WordPress upstream via WordPress Packagist.

That’s a lot of work for a four-line change, especially when we think it’s likely that the vendor will provide a fix. It’s not as through we’re maintaing a complex fork here.

Fortunately, my friend Andy Zito made me aware of the composer-patches project, which offers a lightweight alternative. With composer-patches, you can define patches to be applied to composer packages post-installation. These can be at a remote URL, or stored locally in your project. The default current working directory for applying the patch is the root installation directory of the package. For example, given this block:

1
2
3
4
5
6
7
8
"extra": {
...
"patches": {
"wpackagist-plugin/simply-static": {
"Hook textdomain function": "patches/simply-static.patch"
}
}
},

Composer will clone the simply-static plugin from the WordPress plugins repository (via WordPress packagist), and then apply the patch stored at patches/simply-static.patch within the project repository.

To create the patch, I took a copy of the plugin code and initialized a git repository. I then added everything but did not commit. I then made the necessary changes, and used git diff to create the patch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
diff --git a/simply-static.php b/simply-static.php
index 22d235e..3425c69 100755
--- a/simply-static.php
+++ b/simply-static.php
@@ -28,8 +28,11 @@ if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
}

// Localize.
-$textdomain_dir = plugin_basename( dirname( __FILE__ ) ) . '/languages';
-load_plugin_textdomain( 'simply-static', false, $textdomain_dir );
+function simply_static_load_textdomain() {
+ $textdomain_dir = plugin_basename( dirname( __FILE__ ) ) . '/languages';
+ load_plugin_textdomain( 'simply-static', false, $textdomain_dir );
+}
+add_action( 'init', 'simply_static_load_textdomain' );

// Run autoloader.
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) && ! class_exists( 'Simply_Static\Plugin' ) ) {

Composer then applies the patch; functionally, it’s doing patch -p1 < /path/to/your/patch in the root of the plugin directory.

This is not a replacement for proper version control if you’re actually managing a fork. In this case, I’m taking the line that the version control is the responsibility of the vendor, and they’re managing the changes. Given a more complex set of changes, and in particular a real change in functionality, I’d probably go the longer route and create a separate repository.