- PVSM.RU - https://www.pvsm.ru -

PHP Composer: фиксим зависимости без боли

Многие из вас наверняка сталкивались с ситуацией, когда в библиотеке или фреймворке, который вы используете, есть баг или нет необходимой функциональности. Предположим, вы даже не поленились и сформировали pull request. Но примут его далеко не сразу, а следующий релиз продукта вообще может произойти через год.

PHP Composer: фиксим зависимости без боли

Что же делать, если исправление вам срочно нужно катить в прод? Напрашивается очевидное решение — использовать форк библиотеки или фреймворка. Однако с форками не всё просто. Использовать наследования для переопределения функциональности, которую нужно изменить, не всегда возможно и часто требует больших изменений. На помощь приходят плагины для Composer, которые умеют патчить зависимости.

В этой статье я расскажу подробнее о том, почему форки — это неудобно, а также рассмотрю два плагина для Composer для патчинга зависимостей: чем они отличаются, как ими пользоваться и в чём их преимущества. Если вы сталкивались подобными проблемами или вам просто интересно, добро пожаловать под кат.

Проблему удобнее всего рассматривать на примере. Давайте предположим, что мы хотим что-то изменить в библиотеке PHP Code Coverage [1], которая используется во фреймворке тестирования PHPUnit [2] для измерения уровня покрытия кода тестами. Допустим, мы хотим исправить в версии 7.0.8 как-то так (файл myFix.patch):

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();
         }

Создадим нашу библиотеку-пример. Пусть это будет php-composer-patches-example [3]. Детали здесь не очень важны, но на случай если вы решите посмотреть, что из себя представляет библиотека, я привожу консольный вывод под спойлером.

Скрытый текст

$ 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 (e.g. 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

Что не так с форком зависимости

Давайте посмотрим, как происходит форк зависимости. Попробуем форкнуть PHP Code Coverage.

  1. Заходим на страницу PHP Code Coverage на GitHub [1].
  2. Нажимаем на кнопку Fork Fork button (обратите внимание: у вас будет свой форк, замените mougrim на своё user name).
  3. Клонируем форк:
    cd ../
    git clone git@github.com:mougrim/php-code-coverage.git
    cd php-code-coverage
  4. Переходим в версию, которую мы хотим пропатчить:
    git checkout 7.0.8
  5. Создаём ветку для фикса:
    git checkout -b 7.0.8-myFix
  6. Вносим необходимые изменения, коммитим, пушим:
    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. Добавляем форк как репозиторий в composer.json для нашей библиотеки (это нужно для того, чтобы при подключении пакета phpunit/php-code-coverage подключался не оригинальный пакет, а форк):
    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. Меняем версию для зависимости на бранч:
    composer.phar require phpunit/php-code-coverage 'dev-7.0.8-myFix'

Но на самом деле всё ещё сложнее: Composer говорит, что установка невыполнима, так как phpunit/phpunit требует phpunit/php-code-coverage версии ^7.0.7, а для нашего проекта требуется 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.

Что с этим делать? Есть три варианта:

  1. Помимо форка phpunit/php-code-coverage, форкнуть ещё и PHPUnit и прописать для зависимости phpunit/php-code-coverage версию dev-7.0.8-myFix. Этот путь довольно сложный с точки зрения поддержки и тем сложнее, чем больше библиотек зависит от phpunit/php-code-coverage.
  2. Сделать в своём форке phpunit/php-code-coverage, чтобы тег 7.0.8 ссылался на другой коммит. Это как минимум неочевидно, а как максимум — в Git неудобно работать с тегами, ссылающимися на разные коммиты с одним названием в разных удалённых репозиториях.
  3. В своём форке phpunit/php-code-coverage использовать тег альфа-релиза, например 7.0.8-a+myFix (здесь могут быть коллизии с альфа-релизами исходной библиотеки).

У всех вариантов есть свои недостатки. Я также пробовал использовать тег типа 7.0.8.1, но Composer не принимает такие теги.

Третий вариант кажется меньшим из зол, рассмотрим его подробнее. Создадим тег альфа-релиза:

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

Допустим, мы хотим использовать нашу библиотеку mougrim/php-composer-patches-example в проекте, который зависит от phpunit/phpunit. Здесь тоже не обойтись без шаманства, придётся опять указать репозиторий https://github.com/mougrim/php-code-coverage.git для phpunit/php-code-coverage, а также явно указать зависимость от phpunit/php-code-coverage версии 7.0.8-a+myFix (иначе установка не завершится успехом):

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

Прошу обратить внимание на то, что php-composer-patches-example подключается как репозиторий, поскольку этот репозиторий является лишь примером и потому не был добавлен в Packagist. В вашем случае этот шаг, скорее всего, можно пропустить.

Подведём итоги использования форков.

Плюсы этого подхода:

  • не нужно устанавливать плагины для Composer.

Минусы этого подхода:

  • если вы используете roave/security-advisories [4], то вы не увидите информацию о том, что версия зависимости, которую вы форкнули и модифицировали, содержит уязвимость;
  • когда выйдет новая версия зависимости, историю с форком придётся повторить заново;
  • если вы хотите зафиксить зависимость зависимости, как в рассмотренном примере, то dev-* для неё не подойдёт и придётся шаманить с версиями или делать форки конфликтующих зависимостей;
  • при наличии проектов, которые зависят от вашей библиотеки, устанавливать библиотеку в проект придётся не самым очевидным и удобным способом;
  • при наличии проектов, которые зависят от вашей библиотеки, для них версия phpunit/php-code-coverage будет строго зафиксирована, что не всегда приемлемо;
  • более того, если проекты из пунктов выше уже форкнули PHP Code Coverage по какой-то другой причине, то всё становится ещё сложнее.

Я думаю, вы уже поняли, что форк зависимости не является такой уж хорошей идеей.

Использование cweagans/composer-patches

В очередной раз испытывая боль и страдания от использования форков, я наткнулся на cweagans/composer-patches [5] в PHP-Дайджесте № 101 [6] (кстати, у pronskiy [7] полезный блог, рекомендую подписаться). Это плагин для Cоmposer, который позволяет применять патчи к зависимостям. Прочитав описание, я подумал, что это именно то, что нужно.

Как использовать cweagans/composer-patches:

  1. Клонируем PHP Code Coverage:
    cd ../
    rm -rf php-code-coverage
    git clone git@github.com:sebastianbergmann/php-code-coverage.git
    cd php-code-coverage
  2. Переходим в версию, которую мы хотим пропатчить:
    git checkout 7.0.8
  3. Вносим необходимые изменения.
  4. Создаём патч:
    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. В своём проекте подключаем 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. Для настройки cweagans/composer-patches добавляем следующее в composer.json (для одного пакета можно указать несколько патчей):
    {
        "config": {
            "preferred-install": "source"
        },
        "extra": {
            "patches": {
                "phpunit/php-code-coverage": {
                    "My fix description": "patches/phpunit/php-code-coverage/myFix.patch"
                }
            },
            "enable-patching": true
        }
    }
  7. Обновляем зависимости:
    composer.phar update
  8. Если что-то пошло не так, это можно будет увидеть в выводе предыдущей команды, но на всякий случай можно проверить, что наши изменения применились:
    $ cat vendor/phpunit/php-code-coverage/src/CodeCoverage.php | grep example
        // for example some changes here
  9. Коммитим и пушим результат:
    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

Убеждаемся, что при установке нашей библиотеки в проекте патч тоже применится.

Создаём проект:

cd ../
rm -rf php-project
mkdir php-project
cd php-project
composer.phar require phpunit/phpunit '^8.4.2'

Добавляем в composer.json следущие строки:

{
    "extra": {
        "enable-patching": true
    }
}

Устанавливаем 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

Казалось бы, при подключении пакета должна была быть попытка применить патч, но нет.
Обновляем пакеты, чтобы применился патч, но этого не происходит:

$ 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

Порывшись в баг-трекере, я нашёл баг File based patches aren't resolved in dependencies [8]. Получается, нужно либо указывать URL до патча (а значит, скачивать его откуда-то), либо указывать путь до патча вручную в каждом проекте, где вы устанавливаете зависимость, требующую патчей.

Подведём итоги использования cweagans/composer-patches.

Плюсы этого подхода:

  • у плагина есть комьюнити;
  • roave/security-advisories не перестанет работать;
  • при выходе новой версии зависимости, если патч успешно применится, достаточно будет убедиться, что с новой версией всё работает (для минорных релизов с большой вероятностью всё заработает само, для мажорных — тоже есть вероятность, что ничего делать не придётся);
  • при наличии проектов, которые зависят от вашей библиотеки, для них версия phpunit/php-code-coverage не будет строго зафиксирована;
  • более того, в случае из пункта выше такой проект сможет применить свои патчи в PHP Code Coverage.

Минусы:

  • это плагин для Composer, а значит, при обновлении Composer он может сломаться;
  • нужно указывать настройку enable-patching=true, чтобы патчи применялись из зависимостей;
  • у основного мейнтейнера проекта нет много времени, чтобы им заниматься, поэтому он, как правило, принимает pull requests, но не особо развивает проект (например, у него были идеи для второй версии в задаче [9], но спустя три года мало что изменилось);
  • есть баг File based patches aren't resolved in dependencies [8], который доставляет неудобства и который уже три года висит в бэклоге;
  • нельзя использовать разные патчи для разных версий зависимостей.

Последний пункт стал для меня барьером. Сначала я завёл feature request [10]. Мейнтейнер написал, что не хочет добавлять эту фичу в основной код, но во второй версии можно будет написать плагин (да, плагин для плагина для Composer). Перспективы выхода второй версии были туманными, поэтому я решил поискать альтернативы. Среди небольшого списка я не нашёл плагина, который бы поддерживался.

Лезть в код плагина не хотелось, поэтому я решил прошерстить форки — наверняка кто-то уже сталкивался с проблемой и решил её.

Использование Vaimo Composer Patches

В большинстве форков не было вообще никаких отличий от оригинала (зачем они вообще форкают?). Часть форков была сделана для pull requests, которые уже были слиты с основной библиотекой. Однако всё-таки нашёлся один интересный кандидат, который решал мою проблему, — Vaimo Composer Patches [11]. На тот момент он ещё был оформлен как форк, но его мейнтейнер, похоже, делать pull requests не собирался. Среди прочего, например, он уже поменял имя пакета на vaimo/composer-patches. Но была и проблема: issues были отключены, то есть обратной связи с автором не было вообще. Также плагин не был размещён на Packagist [12].

Такой хороший форк не должен теряться в куче других бесполезных форков. Поэтому я связался с автором с просьбой включить issues и добавить пакет на Packagist. Спустя почти месяц автор ответил и всё это сделал. :)

Использование vaimo/composer-patches не отличается от использования предыдущего плагина, но можно указывать разные патчи для разных версий.

  1. Откатываем нашу библиотеку (удаление папки vendor необходимо, так как плагины cweagans/composer-patches и vaimo/composer-patches не очень совместимы между собой):
    cd ../php-composer-patches-example
    git checkout master
    rm -rf vendor/
    composer.phar update
  2. Выполняем пункты 1—4 из предыдущего раздела.
  3. В своём проекте подключаем vaimo/composer-patches:
    cd ../php-composer-patches-example
    git checkout -b vaimoComposerPatches
    composer.phar require vaimo/composer-patches '^4.20.2'
  4. Для настройки vaimo/composer-patches добавляем следующее в composer.json (документацию можно увидеть здесь [13]):
    {
        "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. Обновляем зависимости:
    composer.phar update
  6. Если что-то пошло не так, это можно будет увидеть в выводе предыдущей команды, но на всякий случай можно убедиться, что наши изменения применились:
    $ cat vendor/phpunit/php-code-coverage/src/CodeCoverage.php | grep example
        // for example some changes here
  7. Коммитим и пушим результат:
    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

Убеждаемся, что при установке нашей библиотеки в проекте патч тоже применится.

Создаём проект и устанавливаем 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

На всякий случай можно убедиться, что наши изменения применились:

$ cat vendor/phpunit/php-code-coverage/src/CodeCoverage.php | grep example
    // for example some changes here

Подведём итоги использования vaimo/composer-patches.

Плюсы этого плагина почти такие же, как у предыдущего, но ещё включают следующие:

  • мейнтейнер активно развивает плагин и уже выпустил четвёртую мажорную версию;
  • не нужно ничего дополнительно прописывать, чтобы патчи из зависимостей применились;
  • можно использовать разные патчи для разных версий зависимостей;
  • у плагина очень много настроек, так что если описанного в статье функционала вам не хватает, то загляните в документацию — возможно, нужная вам фича уже реализована.

Минусы:

  • как и предыдущий, это плагин для Composer, а значит, при обновлении Composer он может сломаться;
  • в отличие от предыдущего плагина, у этого комьюнити меньше.

Выводы

Подведём общие итоги:

  • использовать форки пакетов для каких-то мелких исправлений неудобно;
  • cweagans/composer-patches [5] — хороший плагин, но развивается плохо, поэтому я его не рекомендую;
  • Vaimo Composer Patches [11] — отличный плагин, хорошо решающий проблему фикса зависимостей, а также имеющий кучу настроек;
  • у Vaimo Composer Patches маленькое комьюнити, но я надеюсь, что эта статья его увеличит;
  • если в зависимости требуется сделать очень много изменений, то, возможно, проще прибегнуть к хардфорку (поддерживать форк независимо от исходной зависимости).

Также я сделал косвенный вывод: если какая-то зависимость не предоставляет необходимый функционал, то, возможно, есть форки, которые реализовали этот функционал и даже больше.

В Badoo мы используем Vaimo Composer Patches в двух случаях:

  • в SoftMocks [14] для патчинга PHPUnit и PHP Code Coverage;
  • во внутреннем репозитории для фикса Webmozart Assert [15] для совместимости с SoftMocks как временный фикс (пока SoftMocks не поддерживают конструкции array_map(array('static', 'valueToString')).

Ринат Ахмадеев, Sr. PHP Developer

Автор: mougrim

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/337298

Ссылки в тексте:

[1] PHP Code Coverage: https://github.com/sebastianbergmann/php-code-coverage

[2] PHPUnit: https://github.com/sebastianbergmann/phpunit

[3] php-composer-patches-example: https://github.com/mougrim/php-composer-patches-example

[4] roave/security-advisories: https://github.com/Roave/SecurityAdvisories

[5] cweagans/composer-patches: https://github.com/cweagans/composer-patches

[6] PHP-Дайджесте № 101: https://habr.com/ru/company/zfort/blog/320756/

[7] pronskiy: https://habr.com/ru/users/pronskiy/

[8] File based patches aren't resolved in dependencies: https://github.com/cweagans/composer-patches/issues/39

[9] задаче: https://github.com/cweagans/composer-patches/issues/93

[10] feature request: https://github.com/cweagans/composer-patches/issues/130

[11] Vaimo Composer Patches: https://github.com/vaimo/composer-patches

[12] Packagist: https://packagist.org/

[13] здесь: https://github.com/vaimo/composer-patches/blob/master/docs/USAGE_BASIC.md

[14] SoftMocks: https://github.com/badoo/soft-mocks/

[15] Webmozart Assert: https://github.com/webmozart/assert

[16] Источник: https://habr.com/ru/post/473654/?utm_source=habrahabr&utm_medium=rss&utm_campaign=473654