Many of you have probably come across a situation where in the library or framework that you are using, there is a bug or not the necessary functionality. Suppose you were not too lazy and formed a pull request. But they will not accept it right away, and the next release of the product in general may happen in a year.
What to do if you urgently need to roll the correction to the product? The obvious solution is to use a fork of a library or framework. However, forks are not simple. Using inheritance to override the functionality that needs to be changed is not always possible and often requires major changes. Plugins for Composer that can patch dependencies come to the rescue.
In this article I will talk more about why forks are inconvenient, and also consider two plugins for Composer for dependency patching: how they differ, how to use them and what are their advantages. If you have encountered similar problems or are just wondering, welcome to cat.
The problem is most conveniently considered by example. Let's say that we want to change something in the PHP Code Coverage library, which is used in the PHPUnit testing framework to measure the level of code coverage by tests. Suppose we want to fix something like this in version 7.0.8 ( myFix.patch
file):
diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 2c92ae2..514171e 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -190,6 +190,7 @@ public function filter(): Filter */ public function getData(bool $raw = false): array { + // for example some changes here if (!$raw && $this->addUncoveredFilesFromWhitelist) { $this->addUncoveredFilesFromWhitelist(); }
Let's create our example library. Let it be php-composer-patches-example . The details here are not very important, but in case you decide to see what the library is like, I bring the console output under the spoiler.
$ git clone git@github.com:mougrim/php-composer-patches-example.git «php-composer-patches-example»… remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 : 100% (3/3), . $ cd php-composer-patches-example/ $ $ composer.phar init --name=mougrim/php-composer-patches-example --description="It's an example for article with using forks and patches for changing dependencies" --author='Mougrim <rinat@mougrim.ru>' --type=library --require='phpunit/phpunit:^8.4.2' --license=MIT --homepage='https://github.com/mougrim/php-composer-patches-example' Welcome to the Composer config generator This command will guide you through creating your composer.json config. Package name (<vendor>/<name>) [mougrim/php-composer-patches-example]: Description [It's an example for article with using forks and patches for changing dependencies]: Author [Mougrim <rinat@mougrim.ru>, n to skip]: Minimum Stability []: Package Type (eg library, project, metapackage, composer-plugin) [library]: License [MIT]: Define your dependencies. Would you like to define your dev dependencies (require-dev) interactively [yes]? no { "name": "mougrim/php-composer-patches-example", "description": "It's an example for article with using forks and patches for changing dependencies", "type": "library", "homepage": "https://github.com/mougrim/php-composer-patches-example", "require": { "phpunit/phpunit": "^8.4.2" }, "license": "MIT", "authors": [ { "name": "Mougrim", "email": "rinat@mougrim.ru" } ] } Do you confirm generation [yes]? yes Would you like to install dependencies now [yes]? yes Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 29 installs, 0 updates, 0 removals - Installing sebastian/version (2.0.1): Loading from cache - Installing sebastian/type (1.1.3): Loading from cache - Installing sebastian/resource-operations (2.0.1): Loading from cache - Installing sebastian/recursion-context (3.0.0): Loading from cache - Installing sebastian/object-reflector (1.1.1): Loading from cache - Installing sebastian/object-enumerator (3.0.3): Loading from cache - Installing sebastian/global-state (3.0.0): Loading from cache - Installing sebastian/exporter (3.1.2): Loading from cache - Installing sebastian/environment (4.2.2): Loading from cache - Installing sebastian/diff (3.0.2): Loading from cache - Installing sebastian/comparator (3.0.2): Loading from cache - Installing phpunit/php-timer (2.1.2): Loading from cache - Installing phpunit/php-text-template (1.2.1): Loading from cache - Installing phpunit/php-file-iterator (2.0.2): Loading from cache - Installing theseer/tokenizer (1.1.3): Loading from cache - Installing sebastian/code-unit-reverse-lookup (1.0.1): Loading from cache - Installing phpunit/php-token-stream (3.1.1): Loading from cache - Installing phpunit/php-code-coverage (7.0.8): Loading from cache - Installing doctrine/instantiator (1.2.0): Loading from cache - Installing symfony/polyfill-ctype (v1.12.0): Loading from cache - Installing webmozart/assert (1.5.0): Loading from cache - Installing phpdocumentor/reflection-common (2.0.0): Loading from cache - Installing phpdocumentor/type-resolver (1.0.1): Loading from cache - Installing phpdocumentor/reflection-docblock (4.3.2): Loading from cache - Installing phpspec/prophecy (1.9.0): Loading from cache - Installing phar-io/version (2.0.1): Loading from cache - Installing phar-io/manifest (1.0.3): Loading from cache - Installing myclabs/deep-copy (1.9.3): Loading from cache - Installing phpunit/phpunit (8.4.2): Loading from cache sebastian/global-state suggests installing ext-uopz (*) phpunit/phpunit suggests installing phpunit/php-invoker (^2.0.0) phpunit/phpunit suggests installing ext-soap (*) Writing lock file Generating autoload files $ $ echo 'vendor/' > .gitignore $ echo 'composer.lock' >> .gitignore $ git add .gitignore composer.json $ $ git commit --gpg-sign --message='Init composer' [master ce800ae] Init composer 2 files changed, 18 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json $ git push origin master : 4, . Delta compression using up to 4 threads. : 100% (3/3), . : 100% (4/4), 1.21 KiB | 1.21 MiB/s, . Total 4 (delta 0), reused 0 (delta 0) To github.com:mougrim/php-composer-patches-example.git f31c342..ce800ae master -> master
What's wrong with fork addiction
Let's see how fork dependency occurs. Let's try fork the PHP Code Coverage.
- We go to the PHP Code Coverage page on GitHub .
- Push the Fork button
(note: you will have your fork, replace mougrim with your user name).
- Clone fork:
cd ../ git clone git@github.com:mougrim/php-code-coverage.git cd php-code-coverage
- Go to the version we want to patch:
git checkout 7.0.8
- Create a branch for fix:
git checkout -b 7.0.8-myFix
- We make the necessary changes, commit, push:
git apply ../myFix.patch git add src/CodeCoverage.php git commit --gpg-sign --message='My fix' git push -u origin 7.0.8-myFix
- Add fork as a repository in composer.json for our library (this is necessary so that when connecting the
phpunit/php-code-coverage
package, not the original package is connected, but the fork):
cd ../php-composer-patches-example git checkout -b useFork composer.phar config repositories.phpunit/php-code-coverage vcs https://github.com/mougrim/php-code-coverage.git
- Change the version for dependency to brunch:
composer.phar require phpunit/php-code-coverage 'dev-7.0.8-myFix'
But actually it’s still more complicated: Composer says that the installation is impossible, since phpunit/phpunit
requires phpunit/php-code-coverage
version ^7.0.7
, and for our project requires dev-7.0.8-myFix
:
$ composer.phar require phpunit/php-code-coverage 'dev-7.0.8-myFix' ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Your requirements could not be resolved to an installable set of packages. Problem 1 - phpunit/phpunit 8.4.2 requires phpunit/php-code-coverage ^7.0.7 -> satisfiable by phpunit/php-code-coverage[7.0.x-dev]. - phpunit/phpunit 8.4.2 requires phpunit/php-code-coverage ^7.0.7 -> satisfiable by phpunit/php-code-coverage[7.0.x-dev]. - phpunit/phpunit 8.4.2 requires phpunit/php-code-coverage ^7.0.7 -> satisfiable by phpunit/php-code-coverage[7.0.x-dev]. - Can only install one of: phpunit/php-code-coverage[7.0.x-dev, dev-7.0.8-myFix]. - Installation request for phpunit/php-code-coverage dev-7.0.8-myFix -> satisfiable by phpunit/php-code-coverage[dev-7.0.8-myFix]. - Installation request for phpunit/phpunit ^8.4.2 -> satisfiable by phpunit/phpunit[8.4.2]. Installation failed, reverting ./composer.json to its original content.
What to do with it? There are four options:
- In addition to the
phpunit/php-code-coverage
fork, fork PHPUnit and write the versiondev-7.0.8-myFix
for the dependencyphpunit/php-code-coverage
. This path is rather complicated in terms of support and the more complicated the more libraries depend onphpunit/php-code-coverage
. - Use alias when connecting
phpunit/php-code-coverage
. But aliases are not pulled from the dependencies, which means that they will always need to be written manually. - Make
phpunit/php-code-coverage
in your fork so that the7.0.8
tag7.0.8
to another commit. This is at least not obvious, but at the maximum - in Git it is inconvenient to work with tags that refer to different commits with the same name in different remote repositories. - In your fork
phpunit/php-code-coverage
use the alpha release tag, for example7.0.8-a+myFix
(there may be collisions with alpha releases of the source library).
All options have their drawbacks. I also tried using a tag like 7.0.8.1
, but Composer does not accept such tags.
The second and fourth options seem the lesser of evils. By the number of actions they are approximately the same, in this article we will consider only one - the fourth. Create an alpha release tag:
cd ../php-code-coverage git tag 7.0.8-a+myFix git push origin 7.0.8-a+myFix cd ../php-composer-patches-example composer.phar require phpunit/php-code-coverage '7.0.8-a+myFix' git add composer.json git commit --gpg-sign --message='Use fork' git push -u origin useFork
Suppose we want to use our library mougrim/php-composer-patches-example
in a project that depends on phpunit/phpunit
. Here, one can not do without shamanism, you will again have to specify the repository https://github.com/mougrim/php-code-coverage.git
for phpunit/php-code-coverage
, and also explicitly indicate the dependence on phpunit/php-code-coverage
version 7.0.8-a+myFix
(otherwise the installation will not succeed):
cd ../ mkdir php-project cd php-project/ composer.phar require phpunit/phpunit '^8.4.2' composer.phar config repositories.mougrim/php-composer-patches-example vcs https://github.com/mougrim/php-composer-patches-example.git composer.phar config repositories.phpunit/php-code-coverage vcs https://github.com/mougrim/php-code-coverage.git composer.phar require phpunit/php-code-coverage 7.0.8-a+myFix composer.phar require mougrim/php-composer-patches-example dev-useFork
Please note that php-composer-patches-example is connected as a repository, since this repository is just an example and therefore has not been added to Packagist. In your case, this step is most likely to be skipped.
To summarize the use of forks.
The advantages of this approach:
- No need to install plugins for Composer.
Cons of this approach:
- if you use
roave/security-advisories
, then you will not see information that the version of the dependency that you forked and modified contains a vulnerability; - when a new version of the dependency comes out, the fork story will have to be repeated anew;
- if you want to fix the dependency dependency, as in the considered example, then
dev-*
will not work for it and you have to shamanize with versions or fork for conflicting dependencies; - if there are projects that depend on your library, you will not have to install the library in the project in the most obvious and convenient way;
- if there are projects that depend on your library, for them the
phpunit/php-code-coverage
version will be strictly fixed, which is not always acceptable; - Moreover, if the projects from the points above already forked PHP Code Coverage for some other reason, then everything becomes even more complicated.
I think you already realized that forking addiction is not such a good idea.
Using cweagans / composer-patches
Once again experiencing pain and suffering from using forks, I came across cweagans/composer-patches
in PHP Digest No. 101 (by the way, pronskiy has a useful blog, I recommend subscribing). This is a plugin for omposer, which allows you to apply patches to dependencies. After reading the description, I thought that this is exactly what you need.
How to use cweagans / composer-patches:
- Clone PHP Code Coverage:
cd ../ rm -rf php-code-coverage git clone git@github.com:sebastianbergmann/php-code-coverage.git cd php-code-coverage
- Go to the version we want to patch:
git checkout 7.0.8
- We make the necessary changes.
- Create a patch:
mkdir -p ../php-composer-patches-example/patches/phpunit/php-code-coverage git diff HEAD > ../php-composer-patches-example/patches/phpunit/php-code-coverage/myFix.patch
- In our project we connect
cweagans/composer-patches
:
cd ../php-composer-patches-example git checkout master composer.phar update git checkout -b cweagansComposerPatches composer.phar require cweagans/composer-patches '^1.6.7'
- To configure
cweagans/composer-patches
add the following tocomposer.json
(you can specify several patches for one package):
{ "config": { "preferred-install": "source" }, "extra": { "patches": { "phpunit/php-code-coverage": { "My fix description": "patches/phpunit/php-code-coverage/myFix.patch" } }, "enable-patching": true } }
- Update dependencies:
composer.phar update
- If something went wrong, this can be seen in the output of the previous command, but just in case, you can check that our changes have been applied:
$ grep example vendor/phpunit/php-code-coverage/src/CodeCoverage.php // for example some changes here
- Commit and push the result:
git add composer.json patches/phpunit/php-code-coverage/myFix.patch git commit --gpg-sign --message='Use cweagans/composer-patches' git push -u origin cweagansComposerPatches
We make sure that when installing our library in the project, the patch will also apply.
Create a project:
cd ../ rm -rf php-project mkdir php-project cd php-project composer.phar require phpunit/phpunit '^8.4.2'
Add the following lines to composer.json
:
{ "extra": { "enable-patching": true } }
Install mougrim/php-composer-patches-example
:
composer.phar config repositories.mougrim/php-composer-patches-example vcs https://github.com/mougrim/php-composer-patches-example.git composer.phar require mougrim/php-composer-patches-example dev-cweagansComposerPatches
It would seem that when connecting the package there should have been an attempt to apply the patch, but no.
We update the packages so that the patch applies, but this does not happen:
$ composer.phar update Removing package phpunit/php-code-coverage so that it can be re-installed and re-patched. - Removing phpunit/php-code-coverage (7.0.8) Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 1 install, 0 updates, 0 removals No patches supplied. Gathering patches for dependencies. This might take a minute. - Installing phpunit/php-code-coverage (7.0.8): Loading from cache - Applying patches for phpunit/php-code-coverage patches/phpunit/php-code-coverage/myFix.patch (My fix description) Could not apply patch! Skipping. The error was: The "patches/phpunit/php-code-coverage/myFix.patch" file could not be downloaded: failed to open stream: No such file or directory Writing lock file Generating autoload files
Having rummaged in a bug tracker, I found a File based patches aren't resolved in dependencies bug. It turns out that you need to either specify the URL before the patch (which means downloading it from somewhere), or manually specify the path to the patch in each project where you install the dependency that requires patches.
To summarize the use of cweagans/composer-patches
.
The advantages of this approach:
- the plugin has a community;
-
roave/security-advisories
will not stop working; - when a new version of the dependency is released, if the patch is successfully applied, it will be enough to make sure that everything works with the new version (for minor releases, with a high probability it will work all by itself, for major releases it is also likely that nothing will have to be done);
- if there are projects that depend on your library, for them the version of
phpunit/php-code-coverage
will not be strictly fixed; - Moreover, in the case of the paragraph above, such a project will be able to apply its patches in PHP Code Coverage.
Minuses:
- This is a plugin for Composer, which means that when updating Composer it may break;
- need to specify setting
enable-patching=true
, so that patches are applied from the dependencies; - the main project maintainer does not have much time to deal with it, therefore, as a rule, he accepts pull requests, but does not particularly develop the project (for example, he had ideas for the second version in the task , but little has changed after three years);
- there is a File based patches aren't resolved in dependencies bug, which is inconvenient and has been hanging in the backlog for three years now;
- You cannot use different patches for different versions of dependencies.
The last point has become a barrier for me. First I made a feature request . The maintainer wrote that he did not want to add this feature to the main code, but in the second version it would be possible to write a plug-in (yes, a plug-in for the plug-in for Composer). The outlook for the second version was vague, so I decided to look for alternatives. Among the small list I did not find a plugin that would be supported.
I didn’t want to get into the plugin code, so I decided to fork forks - for sure, someone had already encountered the problem and solved it.
Using Vaimo Composer Patches
In most forks there were no differences at all from the original (why do they even fork?). Part of the forks were made for pull requests, which were already merged with the main library. However, there was still one interesting candidate who was solving my problem - Vaimo Composer Patches . At that time it was still framed as a fork, but its maintainer, it seemed, was not going to do pull requests. Among other things, for example, he already changed the package name to vaimo/composer-patches
. But there was a problem: issues were disabled, that is, there was no feedback from the author at all. Also, the plugin was not hosted on Packagist .
Such a good fork should not be lost in a pile of other useless forks. Therefore, I contacted the author with a request to include issues and add a package to Packagist. After almost a month, the author answered and did all this. :)
Using vaimo/composer-patches
no different from using the previous plugin, but you can specify different patches for different versions.
- We roll back our library (deleting the
vendor
folder is necessary, since the pluginscweagans/composer-patches
andvaimo/composer-patches
not very compatible with each other):
cd ../php-composer-patches-example git checkout master rm -rf vendor/ composer.phar update
- We carry out points 1-4 from the previous section.
- In our project we connect
vaimo/composer-patches
:
cd ../php-composer-patches-example git checkout -b vaimoComposerPatches composer.phar require vaimo/composer-patches '^4.20.2'
- To configure
vaimo/composer-patches
add the following tocomposer.json
(the documentation can be seen here ):
{ "extra": { "patches": { "phpunit/php-code-coverage": { "My fix description": { "< 7.0.0": "patches/phpunit/php-code-coverage/myFix-leagcy.patch", ">= 7.0.0": "patches/phpunit/php-code-coverage/myFix.patch" } } } } }
- Update dependencies:
composer.phar update
- If something went wrong, this can be seen in the output of the previous command, but just in case, you can make sure that our changes are applied:
$ grep example vendor/phpunit/php-code-coverage/src/CodeCoverage.php // for example some changes here
- Commit and push the result:
git add composer.json patches/phpunit/php-code-coverage/myFix.patch git commit --gpg-sign --message='Use vaimo/composer-patches' git push -u origin vaimoComposerPatches
We make sure that when installing our library in the project, the patch will also apply.
Create a project and install mougrim/php-composer-patches-example
:
cd ../ rm -rf php-project mkdir php-project cd php-project composer.phar require phpunit/phpunit '^8.4.2' composer.phar config repositories.mougrim/php-composer-patches-example vcs https://github.com/mougrim/php-composer-patches-example.git composer.phar require mougrim/php-composer-patches-example dev-vaimoComposerPatches
Just in case, you can make sure that our changes have been applied:
$ grep example vendor/phpunit/php-code-coverage/src/CodeCoverage.php // for example some changes here
To summarize the use of vaimo/composer-patches
.
The advantages of this plugin are almost the same as the previous ones, but also include the following:
- the maintainer is actively developing the plugin and has already released the fourth major version;
- there is no need to prescribe anything additionally for patches from dependencies to be applied;
- You can use different patches for different versions of dependencies;
- the plugin has a lot of settings, so if the functionality described in the article is not enough for you, then look in the documentation - maybe the feature you need is already implemented.
Minuses:
- like the previous one, this is a plugin for Composer, which means that when updating Composer it may break;
- unlike the previous plugin, this community has less.
conclusions
To summarize the general results:
- using fork for some minor fixes is inconvenient;
-
cweagans/composer-patches
is a good plugin, but develops poorly, so I do not recommend it; - Vaimo Composer Patches is an excellent plugin that solves the problem of fixing dependencies well, and also has a bunch of settings;
- Vaimo Composer Patches has a small community, but I hope this article will increase it;
- if a lot of changes are required in the dependency, then it may be easier to resort to a hard fork (keep the fork independent of the original dependency).
I also made an indirect conclusion: if some kind of dependency does not provide the necessary functionality, then there may be forks that implemented this functionality and even more.
At Badoo, we use Vaimo Composer Patches in two cases:
- in SoftMocks for patching PHPUnit and PHP Code Coverage;
- in the internal repository for the Webmozart Assert fix for compatibility with SoftMocks as a temporary fix (while SoftMocks do not support
array_map(array('static', 'valueToString')
constructionsarray_map(array('static', 'valueToString')
).
Rinat Akhmadeev, Sr. PHP Developer
UPD1 : Thanks BoShurik for the link to aliases . Added a point about aliases to the article.