PHP Composer: Fix dependencies without pain

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.







PHP Composer: Fix dependencies without pain







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.







Hidden text
 $ 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.







  1. We go to the PHP Code Coverage page on GitHub .
  2. Push the Fork button Fork button (note: you will have your fork, replace mougrim with your user name).
  3. Clone fork:

     cd ../ git clone git@github.com:mougrim/php-code-coverage.git cd php-code-coverage
          
          



  4. Go to the version we want to patch:

     git checkout 7.0.8
          
          



  5. Create a branch for fix:

     git checkout -b 7.0.8-myFix
          
          



  6. 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
          
          



  7. 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
          
          



  8. 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:







  1. In addition to the phpunit/php-code-coverage



    fork, fork PHPUnit and write the version dev-7.0.8-myFix



    for the dependency phpunit/php-code-coverage



    . This path is rather complicated in terms of support and the more complicated the more libraries depend on phpunit/php-code-coverage



    .
  2. 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.
  3. Make phpunit/php-code-coverage



    in your fork so that the 7.0.8



    tag 7.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.
  4. In your fork phpunit/php-code-coverage



    use the alpha release tag, for example 7.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:









Cons of this approach:









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:







  1. Clone PHP Code Coverage:

     cd ../ rm -rf php-code-coverage git clone git@github.com:sebastianbergmann/php-code-coverage.git cd php-code-coverage
          
          



  2. Go to the version we want to patch:

     git checkout 7.0.8
          
          



  3. We make the necessary changes.
  4. 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
          
          



  5. 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'
          
          



  6. To configure cweagans/composer-patches



    add the following to composer.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 } }
          
          



  7. Update dependencies:

     composer.phar update
          
          



  8. 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
          
          



  9. 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:









Minuses:









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.







  1. We roll back our library (deleting the vendor



    folder is necessary, since the plugins cweagans/composer-patches



    and vaimo/composer-patches



    not very compatible with each other):

     cd ../php-composer-patches-example git checkout master rm -rf vendor/ composer.phar update
          
          



  2. We carry out points 1-4 from the previous section.
  3. 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'
          
          



  4. To configure vaimo/composer-patches



    add the following to composer.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" } } } } }
          
          



  5. Update dependencies:

     composer.phar update
          
          



  6. 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
          
          



  7. 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:









Minuses:









conclusions



To summarize the general results:









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:









Rinat Akhmadeev, Sr. PHP Developer







UPD1 : Thanks BoShurik for the link to aliases . Added a point about aliases to the article.








All Articles