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

Что же делать, если исправление вам срочно нужно катить в прод? Напрашивается очевидное решение — использовать форк библиотеки или фреймворка. Однако с форками не всё просто. Использовать наследования для переопределения функциональности, которую нужно изменить, не всегда возможно и часто требует больших изменений. На помощь приходят плагины для 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.
(обратите внимание: у вас будет свой форк, замените mougrim на своё user name).cd ../
git clone git@github.com:mougrim/php-code-coverage.git
cd php-code-coverage
git checkout 7.0.8
git checkout -b 7.0.8-myFix
git apply ../myFix.patch
git add src/CodeCoverage.php
git commit --gpg-sign --message='My fix'
git push -u origin 7.0.8-myFix
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
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.
Что с этим делать? Есть три варианта:
phpunit/php-code-coverage, форкнуть ещё и PHPUnit и прописать для зависимости phpunit/php-code-coverage версию dev-7.0.8-myFix. Этот путь довольно сложный с точки зрения поддержки и тем сложнее, чем больше библиотек зависит от phpunit/php-code-coverage.phpunit/php-code-coverage, чтобы тег 7.0.8 ссылался на другой коммит. Это как минимум неочевидно, а как максимум — в Git неудобно работать с тегами, ссылающимися на разные коммиты с одним названием в разных удалённых репозиториях.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. В вашем случае этот шаг, скорее всего, можно пропустить.
Подведём итоги использования форков.
Плюсы этого подхода:
Минусы этого подхода:
roave/security-advisories [4], то вы не увидите информацию о том, что версия зависимости, которую вы форкнули и модифицировали, содержит уязвимость;dev-* для неё не подойдёт и придётся шаманить с версиями или делать форки конфликтующих зависимостей;phpunit/php-code-coverage будет строго зафиксирована, что не всегда приемлемо;Я думаю, вы уже поняли, что форк зависимости не является такой уж хорошей идеей.
В очередной раз испытывая боль и страдания от использования форков, я наткнулся на cweagans/composer-patches [5] в PHP-Дайджесте № 101 [6] (кстати, у pronskiy [7] полезный блог, рекомендую подписаться). Это плагин для Cоmposer, который позволяет применять патчи к зависимостям. Прочитав описание, я подумал, что это именно то, что нужно.
Как использовать cweagans/composer-patches:
cd ../
rm -rf php-code-coverage
git clone git@github.com:sebastianbergmann/php-code-coverage.git
cd php-code-coverage
git checkout 7.0.8
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
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'
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
}
}
composer.phar update
$ cat vendor/phpunit/php-code-coverage/src/CodeCoverage.php | grep example
// for example some changes here
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 не будет строго зафиксирована;Минусы:
enable-patching=true, чтобы патчи применялись из зависимостей;Последний пункт стал для меня барьером. Сначала я завёл feature request [10]. Мейнтейнер написал, что не хочет добавлять эту фичу в основной код, но во второй версии можно будет написать плагин (да, плагин для плагина для Composer). Перспективы выхода второй версии были туманными, поэтому я решил поискать альтернативы. Среди небольшого списка я не нашёл плагина, который бы поддерживался.
Лезть в код плагина не хотелось, поэтому я решил прошерстить форки — наверняка кто-то уже сталкивался с проблемой и решил её.
В большинстве форков не было вообще никаких отличий от оригинала (зачем они вообще форкают?). Часть форков была сделана для pull requests, которые уже были слиты с основной библиотекой. Однако всё-таки нашёлся один интересный кандидат, который решал мою проблему, — Vaimo Composer Patches [11]. На тот момент он ещё был оформлен как форк, но его мейнтейнер, похоже, делать pull requests не собирался. Среди прочего, например, он уже поменял имя пакета на vaimo/composer-patches. Но была и проблема: issues были отключены, то есть обратной связи с автором не было вообще. Также плагин не был размещён на Packagist [12].
Такой хороший форк не должен теряться в куче других бесполезных форков. Поэтому я связался с автором с просьбой включить issues и добавить пакет на Packagist. Спустя почти месяц автор ответил и всё это сделал. :)
Использование vaimo/composer-patches не отличается от использования предыдущего плагина, но можно указывать разные патчи для разных версий.
vendor необходимо, так как плагины cweagans/composer-patches и vaimo/composer-patches не очень совместимы между собой):
cd ../php-composer-patches-example
git checkout master
rm -rf vendor/
composer.phar update
vaimo/composer-patches:
cd ../php-composer-patches-example
git checkout -b vaimoComposerPatches
composer.phar require vaimo/composer-patches '^4.20.2'
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"
}
}
}
}
}
composer.phar update
$ cat vendor/phpunit/php-code-coverage/src/CodeCoverage.php | grep example
// for example some changes here
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.
Плюсы этого плагина почти такие же, как у предыдущего, но ещё включают следующие:
Минусы:
Подведём общие итоги:
cweagans/composer-patches [5] — хороший плагин, но развивается плохо, поэтому я его не рекомендую;Также я сделал косвенный вывод: если какая-то зависимость не предоставляет необходимый функционал, то, возможно, есть форки, которые реализовали этот функционал и даже больше.
В Badoo мы используем Vaimo Composer Patches в двух случаях:
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
Нажмите здесь для печати.