Muitos de vocês provavelmente já se depararam com uma situação em que há um bug ou não a funcionalidade necessária na biblioteca ou estrutura que você usa. Suponha que você não tenha preguiça e formou uma solicitação de recebimento. Mas eles não o aceitarão imediatamente, e o próximo lançamento do produto em geral pode ocorrer em um ano.

O que fazer se você precisar urgentemente rolar a correção para o produto? A solução óbvia é usar uma bifurcação de uma biblioteca ou estrutura. No entanto, nem tudo é simples com garfos. Usar a herança para substituir a funcionalidade que precisa ser alterada nem sempre é possível e geralmente requer grandes alterações. Plug-ins para o Composer que podem corrigir dependências são úteis.
Neste artigo, falarei mais sobre por que os garfos são inconvenientes e também considerarei dois plugins para o Composer para correção de dependências: como eles diferem, como usá-los e quais são suas vantagens. Se você encontrou problemas semelhantes ou está apenas se perguntando, seja bem-vindo ao gato.
O problema é mais convenientemente considerado com um exemplo. Digamos que queremos alterar algo na biblioteca de cobertura de código PHP , que é usada na estrutura de teste do PHPUnit para medir o nível de cobertura do código pelos testes. Suponha que desejamos corrigir algo como isto na versão myFix.patch
(arquivo 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(); }
Vamos criar nossa biblioteca de exemplos. Seja php-compositor-patches-example . Os detalhes aqui não são muito importantes, mas caso você decida ver qual é a biblioteca, trago a saída do console para o spoiler.
Texto oculto $ 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
O que há de errado com um garfo do vício
Vamos ver como ocorre a dependência do fork. Vamos tentar bifurcar a cobertura do código PHP.
- Vamos para a página de cobertura de código PHP no GitHub .
- Pressione o botão Fork
(nota: você terá seu garfo, substitua mougrim pelo seu nome de usuário). - Clone o garfo:
cd ../ git clone git@github.com:mougrim/php-code-coverage.git cd php-code-coverage
- Vá para a versão que queremos corrigir:
git checkout 7.0.8
- Crie uma ramificação para correção:
git checkout -b 7.0.8-myFix
- Fazemos as alterações necessárias, confirmamos, pressionamos:
git apply ../myFix.patch git add src/CodeCoverage.php git commit --gpg-sign --message='My fix' git push -u origin 7.0.8-myFix
- Adicione fork como repositório em composer.json para nossa biblioteca (isso é necessário para que, ao conectar o
phpunit/php-code-coverage
, não o pacote original esteja conectado, mas o 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
- Altere a versão da dependência para brunch:
composer.phar require phpunit/php-code-coverage 'dev-7.0.8-myFix'
Mas, na verdade, é ainda mais complicado: o Composer diz que a instalação é impossível, pois o phpunit/phpunit
requer a phpunit/php-code-coverage
^7.0.7
e, para o nosso projeto, requer 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.
O que fazer sobre isso? Existem quatro opções:
- Além da
phpunit/php-code-coverage
o PHPUnit e escreva a versão dev-7.0.8-myFix
para a dependência phpunit/php-code-coverage
. Esse caminho é bastante complicado em termos de suporte e, quanto mais complicado, mais bibliotecas dependem da phpunit/php-code-coverage
do phpunit/php-code-coverage
. - Use o alias ao conectar o
phpunit/php-code-coverage
. Mas os aliases não são extraídos das dependências, o que significa que eles sempre precisam ser gravados manualmente. - Faça a
phpunit/php-code-coverage
no seu fork, para que a tag 7.0.8
a outro commit. Isso é pelo menos não óbvio, mas no máximo - no Git, é inconveniente trabalhar com tags que se referem a diferentes commits com o mesmo nome em diferentes repositórios remotos. - Na
phpunit/php-code-coverage
seu fork phpunit/php-code-coverage
use a tag de liberação alfa, por exemplo 7.0.8-a+myFix
(pode haver colisões com liberações alfa da biblioteca de origem).
Todas as opções têm suas desvantagens. Também tentei usar uma tag como 7.0.8.1
, mas o Composer não aceita essas tags.
A segunda e quarta opções parecem o menor dos males. Pelo número de ações, elas são aproximadamente iguais, neste artigo consideraremos apenas uma - a quarta. Crie uma tag de liberação alfa:
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
Digamos que queremos usar nossa biblioteca mougrim/php-composer-patches-example
em um projeto que depende do phpunit/phpunit
. Aqui, não se pode prescindir do xamanismo, você precisará especificar novamente o repositório https://github.com/mougrim/php-code-coverage.git
para phpunit/php-code-coverage
, bem como indicar explicitamente a dependência da phpunit/php-code-coverage
do phpunit/php-code-coverage
versão 7.0.8-a+myFix
(caso contrário, a instalação não será bem-sucedida):
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
Observe que o php-composer-patches-example está conectado como um repositório, pois esse repositório é apenas um exemplo e, portanto, não foi adicionado ao Packagist. No seu caso, é mais provável que esta etapa seja ignorada.
Para resumir o uso de garfos.
As vantagens desta abordagem:
- Não é necessário instalar plugins para o Composer.
Contras desta abordagem:
- se você usar
roave/security-advisories
, não verá informações de que a versão da dependência que você bifurcou e modificou contém uma vulnerabilidade; - quando uma nova versão da dependência sair, a história da bifurcação terá que ser repetida novamente;
- se você deseja corrigir uma dependência de dependência, como no exemplo considerado, o
dev-*
não funcionará para isso e você deverá shamanize com versões ou bifurcação para dependências conflitantes; - se houver projetos que dependem da sua biblioteca, você não precisará instalar a biblioteca no projeto da maneira mais óbvia e conveniente;
- se houver projetos que dependem da sua biblioteca, para eles a
phpunit/php-code-coverage
será estritamente corrigida, o que nem sempre é aceitável; - Além disso, se os projetos dos pontos acima já bifurcaram a Cobertura de código PHP por algum outro motivo, tudo se tornará ainda mais complicado.
Eu acho que você já percebeu que bifurcar o vício não é uma boa idéia.
Usando cweagans / compositor-patches
Mais uma vez, cweagans/composer-patches
dores e sofrendo com o uso de garfos, me deparei com cweagans/composer-patches
no PHP Digest No. 101 (a propósito, pronskiy tem um blog útil, recomendo assinar). Este é um plugin para o omposer, que permite aplicar patches às dependências. Depois de ler a descrição, pensei que era exatamente isso que você precisava.
Como usar cweagans / compositor-patches:
- Clone a cobertura do código PHP:
cd ../ rm -rf php-code-coverage git clone git@github.com:sebastianbergmann/php-code-coverage.git cd php-code-coverage
- Vá para a versão que queremos corrigir:
git checkout 7.0.8
- Nós fazemos as alterações necessárias.
- Crie um 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
- Em nosso projeto, conectamos
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'
- Para configurar
cweagans/composer-patches
adicione o seguinte a composer.json
(você pode especificar vários patches para um pacote):
{ "config": { "preferred-install": "source" }, "extra": { "patches": { "phpunit/php-code-coverage": { "My fix description": "patches/phpunit/php-code-coverage/myFix.patch" } }, "enable-patching": true } }
- Atualize dependências:
composer.phar update
- Se algo der errado, isso pode ser visto na saída do comando anterior, mas por precaução, você pode verificar se nossas alterações foram aplicadas:
$ grep example vendor/phpunit/php-code-coverage/src/CodeCoverage.php // for example some changes here
- Confirme e envie o resultado:
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
Garantimos que, ao instalar nossa biblioteca no projeto, o patch também se aplique.
Crie um projeto:
cd ../ rm -rf php-project mkdir php-project cd php-project composer.phar require phpunit/phpunit '^8.4.2'
Adicione as seguintes linhas ao composer.json
:
{ "extra": { "enable-patching": true } }
Instale 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
Parece que, ao conectar o pacote, deveria ter havido uma tentativa de aplicar o patch, mas não.
Atualizamos os pacotes para que o patch se aplique, mas isso não acontece:
$ 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
Depois de vasculhar um rastreador de erros, descobri que um patch baseado em arquivo não é resolvido no bug de dependências . Acontece que você deve especificar o URL antes do patch (o que significa baixá-lo de algum lugar) ou especificar o caminho para o patch manualmente em cada projeto em que você instala uma dependência que requer patches.
Para resumir o uso de cweagans/composer-patches
.
As vantagens desta abordagem:
- o plugin tem uma comunidade;
roave/security-advisories
não param de funcionar;- quando uma nova versão da dependência é lançada, se o patch for aplicado com êxito, será suficiente garantir que tudo funcione com a nova versão (para versões menores, com uma alta probabilidade de que funcione por si só, para versões principais também é provável que nada precise ser feito);
- se existem projetos que dependem da sua biblioteca, para eles a versão da
phpunit/php-code-coverage
do phpunit/php-code-coverage
não será estritamente corrigida; - Além disso, no caso do parágrafo acima, esse projeto poderá aplicar seus patches na Cobertura de Código PHP.
Contras:
- Este é um plugin para o Composer, o que significa que, ao atualizar o Composer, ele pode ser quebrado;
enable-patching=true
deve ser especificado para que os patches sejam aplicados a partir de dependências;- o mantenedor do projeto principal não tem muito tempo para lidar com isso; portanto, como regra geral, ele aceita solicitações de recebimento, mas não desenvolve o projeto particularmente (por exemplo, ele tinha idéias para a segunda versão da tarefa , mas pouco mudou depois de três anos);
- existe um erro de patches baseados em arquivo que não é resolvido em dependências , o que é inconveniente e está pendurado na lista de pendências há três anos;
- Você não pode usar patches diferentes para diferentes versões de dependências.
O último ponto se tornou uma barreira para mim. Primeiro, fiz uma solicitação de recurso . O mantenedor escreveu que não queria adicionar esse recurso ao código principal, mas na segunda versão seria possível escrever um plug-in (sim, um plug-in para o plug-in do Composer). As perspectivas para a segunda versão eram vagas, então decidi procurar alternativas. Entre a pequena lista, não encontrei um plugin compatível.
Como não queria entrar no código do plug-in, decidi fazer uma bifurcação - com certeza alguém já encontrou o problema e o resolveu.
Usando patches do Vaimo Composer
Na maioria dos garfos, não houve diferenças em relação ao original (por que eles ainda bifurcam?). Parte dos garfos foi feita para solicitações pull, que já foram mescladas com a biblioteca principal. No entanto, havia um candidato interessante que estava resolvendo meu problema - o Vaimo Composer Patches . Naquela época, ainda estava emoldurado como garfo, mas seu mantenedor, ao que parecia, não faria solicitações de recebimento. Entre outras coisas, por exemplo, ele já mudou o nome do pacote para vaimo/composer-patches
. Mas havia um problema: os problemas eram desativados, ou seja, não havia nenhum feedback do autor. Além disso, o plugin não estava hospedado no Packagist .
Um garfo tão bom não deve ser perdido em uma pilha de outros garfos inúteis. Portanto, entrei em contato com o autor com uma solicitação para ativar problemas e adicionar um pacote ao Packagist. Depois de quase um mês, o autor respondeu e fez tudo isso. :)
O uso do vaimo/composer-patches
não vaimo/composer-patches
diferente do uso do plug-in anterior, mas você pode especificar patches diferentes para versões diferentes.
- Estamos
cweagans/composer-patches
nossa biblioteca (é necessário excluir a pasta do vendor
, pois os plugins cweagans/composer-patches
e vaimo/composer-patches
não vaimo/composer-patches
muito compatíveis entre si):
cd ../php-composer-patches-example git checkout master rm -rf vendor/ composer.phar update
- Realizamos os pontos 1-4 da seção anterior.
- Em nosso projeto, conectamos o
vaimo/composer-patches
:
cd ../php-composer-patches-example git checkout -b vaimoComposerPatches composer.phar require vaimo/composer-patches '^4.20.2'
- Para configurar o
vaimo/composer-patches
adicione o seguinte a composer.json
(a documentação pode ser vista aqui ):
{ "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" } } } } }
- Atualize dependências:
composer.phar update
- Se algo der errado, isso pode ser visto na saída do comando anterior, mas por precaução, você pode garantir que nossas alterações sejam aplicadas:
$ grep example vendor/phpunit/php-code-coverage/src/CodeCoverage.php // for example some changes here
- Confirme e envie o resultado:
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
Garantimos que, ao instalar nossa biblioteca no projeto, o patch também se aplique.
Crie um projeto e instale o 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
Por precaução, você pode garantir que nossas alterações foram aplicadas:
$ grep example vendor/phpunit/php-code-coverage/src/CodeCoverage.php // for example some changes here
Para resumir o uso de vaimo/composer-patches
.
As vantagens deste plugin são quase as mesmas que as anteriores, mas também incluem o seguinte:
- o mantenedor está desenvolvendo ativamente o plug-in e já lançou a quarta versão principal;
- não há necessidade de prescrever nada adicionalmente para que os patches das dependências sejam aplicados;
- Você pode usar patches diferentes para diferentes versões de dependências;
- o plug-in tem muitas configurações; portanto, se a funcionalidade descrita no artigo não for suficiente para você, consulte a documentação - talvez o recurso necessário já esteja implementado.
Contras:
- como o anterior, este é um plugin para o Composer, o que significa que, ao atualizar o Composer, ele pode se quebrar;
- ao contrário do plug-in anterior, esta comunidade tem menos.
Conclusões
Para resumir:
- usar garfos de pacotes para pequenas correções é inconveniente;
cweagans/composer-patches
é um bom plugin, mas se desenvolve mal, por isso não o recomendo;- O Vaimo Composer Patches é um excelente plug-in que resolve bem o problema de corrigir dependências e também possui várias configurações;
- O Vaimo Composer Patches possui uma pequena comunidade, mas espero que este artigo a aumente;
- se forem necessárias muitas alterações na dependência, pode ser mais fácil recorrer a um fork forçado (mantenha o fork independente da dependência original).
Também tirei uma conclusão indireta: se algum tipo de dependência não fornecer a funcionalidade necessária, poderá haver garfos que implementaram essa funcionalidade e muito mais.
No Badoo, usamos o Vaimo Composer Patches em dois casos:
- no SoftMocks para corrigir a cobertura do PHPUnit e do código PHP;
- no repositório interno da correção Webmozart Assert para compatibilidade com o SoftMocks como uma correção temporária (enquanto o SoftMocks não suporta construções
array_map(array('static', 'valueToString')
).
Rinat Akhmadeev, Sr. Desenvolvedor PHP
UPD1 : Obrigado BoShurik pelo link para os aliases . Foi adicionado um artigo sobre aliases ao artigo.