Some late Drupal 7 projects use Composer for project structure and tooling, even though they don't use Composer for the Drupal requirements proper. In that case, the normal Drupal 8/9 patching process using cweagans/composer-patches is not available, since dependencies are not handled with Composer. So is there a way to apply patches cleanly from Composer ? Sure there is! Let's see how.
The problem
A typical situation happens like this:
- Legacy Drupal 7 project keeps on being used, but not upgraded to Drupal 8 / Drupal 9 or BackdropCMS/li>
- Since it has to be maintainable, it is best on the then-current technique of git submodules, with each contrib being brought in as a separate submodule
- But then, more recent updates bring in Composer, e.g. to start using modern
PHP components, starting the autoloader from
settings.local.php
; or using it for additional tools like Drush or PHPunit. - When a module needs to be patched, the legacy drush-based process becomes
an issue with submodules and Composer, and yet, because components are not
downloaded by Composer, using
cweagans/composer-patches
is not an options
Now, how can the patch process be handled in composer.json
, just as
is would be in any modern Composer deployment ?
How to use Composer patches with zero plugin
There is actually a very simple mechanism for this: using Composer hooks on command events.
That's all we need. Let's take an example: how to add the PHP 7.4 compatibility patch for Gmap, available on the https://www.drupal.org/project/gmap/issues/3118279 issue ?
First step: creating a composer patch
command
First we need to defined our patches and how to apply them.
Here is what needs to be added to composer.json
for a start:
- The
scripts
section allows adding custom commands to Composer - In this case, we add a new
patch
command, to apply all patches - Since commands can be just shell scripts, we create one line for each patch, made
of two parts:
cd (the module directory)
- apply the patch straight from its download URL, taking advantage of the fact
that the
patch
command takes patches form its standard input
- These two have to be kept as a single shell command, because the working directory
is only retained for the direction of the shell subprocess, hence the
;
- With just that
composer patch
to apply our patches on
demand. But we can do better: apply the command automatically.
Second step: applying patches on composer install/update
Remember, Composer includes these command hooks for install and update. To quote the Composer documentation:
post-install-cmd
: occurs after the install command has been executed with a lock file present.post-update-cmd
: occurs after the update command has been executed, or after the install command has been executed without a lock file present.
Beyond invoking shell scripts, Composer can also invoke its custom commands
by prepending them with an @
like Symfony services
Now, any time we run composer install
or composer update
,
Composer will apply the patches without having to type composer patch
manually.
However, there are still two issues, which is especially annoying if the site uses submodules, but would also apply if all code is committed to the project:
- After one of
composer install
orcomposer update
, the files are patches. So any subsequent install or update will find them modified, and attempt to preserve them or rollback, requiring many interactions - The modified files are visible to git, which could invite us to commit them, which is probably not desired.
Third step: cleaning up
So we can just add an automatic cleanup before the install/update commands, so that our patch command, which runs after these tasks, will find the git checkout pristine
For projects committing contrib dependencies, a simple git reset --hard
is all it takes, but that will not work with submodules, so we need to be a bit
smarter:
This time we define a composer subclean
command to clean up
submodules, and we register it to run before both install and update.
Now, we can run composer install
and composer update
multiple times, and find the project clean everytime.
Finally, come commit time, we can just run composer subclean
before committing, or even as a git pre-commit hook, and the patch-induced
changes in our contributions will be reset, at no risk of getting committed.