Tip of the day: patching legacy Drupal 7 projects with Composer
Tags: drupal, composer, legacy
Published: 2022-01-06 19:45:25

---
title: Tip of the day: patching legacy Drupal 7 projects with Composer
published: true
date: 2021-01-26 09:30:58 UTC
tags: ['drupal', 'composer', 'legacy']
canonical_url: https://blog.riff.org/2021_01_26_tip_of_the_day_patching_legacy_drupal_7_projects_with_composer
---

- [PHP](https://blog.riff.org/topic/computing/php_0)
- [Drupal tips and tricks](https://blog.riff.org/category/drupal_tips_and_tricks)
- [DRUPAL-7](https://blog.riff.org/tags/drupal7)
- [Composer](https://blog.riff.org/tags/composer)

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](https://github.com/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:

1. Legacy Drupal 7 project keeps on being used, but not upgraded to Drupal 8 / Drupal 9 or BackdropCMS/li> 
2. 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
3. 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. 
4. 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](https://getcomposer.org/doc/articles/scripts.md#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](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:

```json
{  
  ...misc sections...  
  "require": { ... },  
  "require-dev: { ... },  
  "scripts": {    
    "patch": [      
      "cd www/sites/all/modules/contrib/gmap; curl https://www.drupal.org/files/issues/2020-03-06/3118279-3.patch | patch -p1"   
    ],  
  },  
  "type": "project"
}
```

- 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 `;`


At this step, we can now use `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

```json
{
  ...snip...
  "scripts": {    
    "patch": [      
      "cd www/sites/all/modules/contrib/gmap; curl https://www.drupal.org/files/issues/2020-03-06/3118279-3.patch | patch -p1"   
    ],    
    "post-install-cmd": ["@patch"],    
    "post-update-cmd": ["@patch"]  
  },`
   ...snip...
}
```

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` or `composer 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:

```json
{
  ...snip...
  "scripts": {
    "patch": [
      "cd www/sites/all/modules/contrib/gmap; curl https://www.drupal.org/files/issues/2020-03-06/3118279-3.patch | patch -p1"
    ],
    "subclean": ["git submodule update --init -f"],
    "pre-install-cmd": ["@subclean"],
    "pre-update-cmd": ["@subclean"],
    "post-install-cmd": ["@patch"],
    "post-update-cmd": ["@patch"]
  },
  ...snip...
}
```

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.