Quando um projeto PHP fica grande e complexo, torna-se difícil de gerenciar.
Nessa situação, dividiríamos o projeto em pacotes independentes e usaríamos o Composer para importar todos os pacotes para o projeto. Em seguida, diferentes funcionalidades podem ser implementadas e mantidas por diferentes equipes e podem ser reutilizadas por outros projetos também.
O Composer usa o registro Packagist para distribuir pacotes PHP. Packagist exige que forneçamos uma URL de repositório ao publicar um novo pacote.
Como consequência, dividir um projeto em pacotes também afeta como eles são hospedados: de um único repositório hospedando todo o código a uma infinidade de repositórios para hospedar o código de cada pacote.
Então, resolvemos o problema de gerenciamento do código do projeto, mas às custas da criação de um novo problema: agora temos que gerenciar a hospedagem do código.
O problema com hospedagem de pacotes descentralizada
Nossos pacotes serão controlados, e cada versão do pacote dependerá de alguma versão específica de outro pacote, que por sua vez dependerá de alguma outra versão de algum outro pacote e assim por diante.
Isso se torna um problema ao enviar uma solicitação pull para o seu projeto; provavelmente, você também precisará modificar o código em algum pacote, portanto, você precisa criar um novo branch para esse pacote e apontar para ele em seu composer.json
.
Então, se esse pacote depende de algum outro pacote que também deve ser modificado, você precisa criar um novo branch para ele e atualizar o composer.json
do primeiro pacote para apontar para ele.
E se esse pacote depende de algum outro pacote… Você entendeu.
Então, depois de aprovar a solicitação pull, você precisa desfazer todas as modificações em todos os arquivos composer.json
para apontar para a versão recém-publicada do pacote.
Isso tudo se torna tão difícil de conseguir que você provavelmente pode parar completamente de usar branches de recursos e publicar diretamente em master
, então você não será capaz de rastrear uma mudança entre os pacotes. Então, se, no futuro, você precisar reverter a alteração, boa sorte para encontrar todos os pedaços de código, em todos os pacotes, que foram modificados.
O que podemos fazer a respeito?
Introdução ao monorepo
É aqui que o monorepo vem para salvar o dia. Em vez de ter nosso código distribuído em vários repositórios, podemos ter todos os pacotes hospedados em um único repositório.
O monorepo nos permite controlar a versão de todos os nossos pacotes juntos, de forma que a criação de um novo branch e o envio de uma solicitação pull seja feito em um único lugar, incluindo o código de todos os pacotes que podem ser afetados por ele.
No entanto, ainda estamos limitados pelas restrições do Packagist: para fins de distribuição, cada pacote precisa viver em seu próprio repositório.
O que fazemos agora?
Lidando com as restrições do Packagist
A solução é desacoplar o desenvolvimento e a distribuição do código:
- Use um monorepo para desenvolver o código
- Use vários repositórios (um repositório por pacote) para distribuí-lo (os famosos repositórios “[SOMENTE LEIA]”)
Então, devemos manter todos os repositórios de origem e distribuição sincronizados.
Ao desenvolver o código no monorepo, após uma nova solicitação pull ser mesclada, o novo código para cada pacote deve ser copiado para seu próprio repositório, a partir do qual pode ser distribuído.
Isso é chamado de divisão do monorepo.
Como dividir o monorepo
Uma solução simples é criar um script usando git subtree split
e sincronizando o código do pacote em seu próprio repo.
A melhor solução é usar uma ferramenta para fazer exatamente isso, de modo que possamos evitar fazer isso manualmente. Existem várias ferramentas para escolher:
- Divisor de subárvore Git (
splitsh/lite
) - Git Subsplit (
dflydev/git-subsplit
) - Monorepo builder (
symplify/monorepo-builder
)
Destes, escolhi usar o construtor Monorepo porque ele é escrito em PHP, então posso estendê-lo com funcionalidade personalizada. (Em contraste, splitsh/lite
é escrito em Go e dflydev/git-subsplit
é um script Bash.)
N.B. , o construtor Monorepo funciona apenas para pacotes PHP. Se você precisa gerenciar pacotes JavaScript ou qualquer outra coisa, você deve usar outra ferramenta.
Organizando a estrutura do monorepo
Você deve criar uma estrutura para organizar o código no monorepo. No caso mais simples, você pode ter uma pasta raiz packages/
e adicionar cada pacote em sua própria subpasta.
Se o seu código for mais complexo, contendo não apenas pacotes, mas também pacotes, ou contratos ou outros, você pode criar uma estrutura de vários níveis.
Symfony, por exemplo, usa a seguinte estrutura em seu monorepo symfony/symfony
:
No meu caso, só recentemente abri um monorepo para hospedar todos os meus projetos juntos. (A razão é que eu tinha um contribuidor em potencial que não conseguiu configurar o ambiente de desenvolvimento, então ele foi embora .)
Meu projeto geral abrange várias camadas: o plug-in API GraphQL para WordPress fica na parte superior do servidor GraphQL por PoP , que se baseia na estrutura PoP .
E embora estejam relacionados, eles também são independentes: podemos usar PoP para alimentar outros aplicativos, não apenas GraphQL por PoP; e GraphQL by PoP pode alimentar qualquer CMS, não apenas WordPress.
Portanto, minha decisão foi tratá-los como”camadas”, onde cada camada pode ver e usar outra, mas não outros .
Ao criar a estrutura monorepo, repliquei essa ideia distribuindo o código em dois níveis: layers/
primeiro, e só então packages/
(e, para um específico case, também plugins/
):
Em vez de criar um novo repositório, decidi reutilizar o do PoP, em leoloso/PoP
, porque era a base de todo o código (e também porque eu não queria perder as estrelas que ele havia recebido ).
Depois de definir a estrutura monorepo, você pode migrar o código do repositório de cada pacote.
Importando código, incluindo o histórico Git
Se você está começando o monorepo do zero, pode executar
Provavelmente, ao migrar os pacotes, você também desejará portar seus históricos Git e commits de hashes para continuar navegando neles como documentação e manter o controle de quem fez o quê, quando e por quê.
O construtor Monorepo não o ajudará nesta tarefa. Então, você precisa usar outra ferramenta:
- Multi-para mono-repositório (
hraban/tomono
) - Shopsys Monorepo Tools (
shopsys/monorepo-tools
)
Depois de migrar o código, você pode começar a gerenciá-lo com o construtor Monorepo conforme explicado em seu README .
Um único composer.json
para governar todos eles
Cada pacote PHP tem seu próprio arquivo composer.json
definindo quais dependências ele possui.
O monorepo também terá seu próprio arquivo composer.json
, contendo todas as dependências para todos os pacotes PHP. Dessa forma, podemos executar testes PHPUnit, análise estática PHPStan ou qualquer outra coisa para todos os códigos de todos os pacotes, executando um único comando da raiz monorepo.
Para isso, os pacotes PHP devem conter a mesma versão para a mesma dependência! Então, se o pacote A requer PHPUnit 7.5, e o pacote B requer PHPUnit 9.3, ele não funcionará.
O construtor Monorepo fornece os seguintes comandos:
-
monorepo-builder validate
verifica se as dependências em todos oscomposer.json
não entram em conflito -
monorepo-builder merge
extrai todas as dependências (e outras informações) de todo ocomposer.json
e as mescla no própriocomposer.json
O que demorei um pouco para perceber é que, então, você não deve editar manualmente a raiz composer.json
! Como esse arquivo é gerado automaticamente, você pode perder suas alterações personalizadas se elas não forem adicionadas por meio do arquivo de configuração da ferramenta.
Curiosamente, este é o caso de lidar com o próprio construtor Monorepo. Para instalar esta biblioteca em seu projeto, você pode executar composer require symplify/monorepo-builder--dev
na raiz monorepo, como de costume. Mas imediatamente depois, você deve recriar a dependência no arquivo de configuração monorepo-builder.php
:
função estática de retorno (ContainerConfigurator $ containerConfigurator): void { $ parameters=$ containerConfigurator-> parameters (); $ parâmetros-> definir (Opção:: DATA_TO_APPEND, [ 'require-dev'=> [ 'symplify/monorepo-builder'=>'^ 9.0', ] ]); }
Dividindo o monorepo
Então você mesclou uma solicitação pull. Agora é hora de sincronizar o novo código nos repositórios de pacotes. Isso é chamado de divisão.
Se você estiver hospedando seu monorepo no GitHub, pode apenas criar uma ação a ser disparada no evento push
do master
(ou main
) branch para executar o Ação do GitHub para Monorepo Split , indicando qual é o diretório do pacote de origem e para qual repositório copiar o conteúdo:
nome:'Monorepo Split' sobre: Empurre: galhos: -mestre empregos: monorepo_split_test: roda em: ubuntu-mais recente degraus: -usa: ações/checkout @ v2 com: profundidade de busca: 0 -usa:"symplify/monorepo-split-github-action @ master" env: GITHUB_TOKEN: $ {{secrets.ACCESS_TOKEN}} com: # ↓ dividir o diretório"packages/your-package-name" diretório-pacote:'pacotes/nome-do-seu-pacote' # ↓ no repositório https://github.com/your-organization/your-package-name divisão-repositório-organização:'sua-organização' nome-do-repositório dividido:'nome-do-seu-pacote' # ↓ o usuário assinou sob o commit dividido nome de usuário:"seu-nome-de-usuário do github" usuário-e-mail:"[email protected]"
Para que isso funcione, você também precisa criar um novo token de acesso com escopos “repo” e “workflow,” como explicado aqui , e configurar este token sob o segredo ACCESS_TOKEN
, como explicado aqui .
O exemplo acima funciona para dividir um único pacote. Como podemos dividir vários pacotes? Precisamos declarar um fluxo de trabalho para cada um deles?
Claro que não. As ações do GitHub oferecem suporte para definir uma matriz de diferentes configurações de trabalho . Portanto, podemos definir uma matriz para iniciar muitas instâncias de runner em paralelo, com um runner por pacote para dividir:
empregos
: supply_packages_json: roda em: ubuntu-mais recente degraus: -usa: ações/checkout @ v2 -usa: shivammathur/setup-php @ v2 com: versão php: 7.4 cobertura: nenhuma -usa:"ramsey/composer-install @ v1" # get package json list -id: output_data execute: echo":: set-output name=matrix:: $ (vendor/bin/monorepo-builder packages-json)" saídas: matriz: $ {{steps.output_data.outputs.matrix}} split_monorepo: necessidades: fornecer_packages_json roda em: ubuntu-mais recente estratégia: fail-fast: false matriz: pacote: $ {{fromJson (needs.provide_packages_json.outputs.matrix)}} degraus: -usa: ações/checkout @ v2 -nome: Divisão Monorepo de $ {{matrix.package}} usa: symplify/github-action-monorepo-split @ master env: GITHUB_TOKEN: $ {{secrets.ACCESS_TOKEN}} com: diretório-pacote:'packages/$ {{matrix.package}}' divisão-repositório-organização:'sua-organização' nome do repositório dividido:'$ {{matrix.package}}' nome de usuário:"seu-nome-de-usuário do github" usuário-e-mail:"[email protected]"
Agora, o nome do pacote não está mais codificado, mas vem da matriz (“a realidade é que a colher não existe”).
Além disso, como a lista de pacotes é fornecida por meio do arquivo de configuração monorepo-builder.php
, podemos simplesmente extraí-la daí. Isso é feito executando o comando vendor/bin/monorepo-builder packages-json
, que produz uma saída JSON stringificada contendo todos os pacotes:
Lançando uma nova versão (para todos os pacotes)
O monorepo é mantido simples ao criar versões de todos os pacotes juntos, usando a mesma versão para todos eles. Portanto, o pacote A com a versão 0.7 dependerá do pacote B com a versão 0.7 e assim por diante.
Isso significa que marcaremos os pacotes mesmo que nenhum código tenha sido alterado neles. Por exemplo, se o pacote A foi modificado, ele será marcado como 0,7, mas o pacote B também, embora não contenha modificações.
O construtor Monorepo torna muito fácil marcar todos os pacotes. Primeiro precisamos ter um fluxo de trabalho para dividir o monorepo sempre que marcado (é basicamente o mesmo fluxo de trabalho acima, mais passando a tag para symplify/github-action-monorepo-split
).
Em seguida, marcamos o monorepo para a versão 0.7
executando este comando :
vendor/bin/monorepo-builder release"0.7"
Executar este comando faz mágica real. Ele primeiro libera o código para produção:
- Bump dependências mútuas entre pacotes para
0.7
- Marque o monorepo com
0.7
- Faça um
git push
com a tag0.7
E então, ele reverte o código para desenvolvimento:
- Atualize o alias do branch para
dev-master
em todos os pacotes para0.8-dev
- Bump de dependências mútuas para
0.8-dev
- Faça um
git push
Assistir em ação nunca para de me fascinar. Verifique como, ao executar um comando, todo o ambiente parece ganhar vida própria:
Removendo fluxos de trabalho de pacotes
Mesmo que estejamos executando o PHPUnit em nosso monorepo para todos os pacotes, ainda podemos querer executar o PHPUnit em cada pacote em seu próprio repositório após ter sido dividido, apenas para mostrar um emblema de sucesso.
No entanto, não podemos mais fazer isso. Ou, pelo menos, não tão facilmente.
O fato de que todos os pacotes são versionados juntos e lançados ao mesmo tempo, e que a nova versão de cada pacote leva um pouco de tempo para se tornar disponível no Packagist-digamos, cinco minutos-significa que as dependências podem não estar disponíveis quando executando composer install
, fazendo com que o fluxo de trabalho do PHPUnit falhe.
Por exemplo, se o pacote A depende do pacote B, marcá-los com a versão 0.3 significa que a versão 0.3 do pacote A dependerá da versão 0.3 do pacote B. No entanto, como ambos são divididos e marcados ao mesmo tempo, quando o pacote A executa uma ação disparada por push para master
, a versão 0.3 do pacote B ainda não estará disponível e o fluxo de trabalho falhará.
Em conclusão: você precisará remover a execução desses fluxos de trabalho de todos os repositórios de pacote e confiar apenas nos fluxos de trabalho do monorepo.
Ou, se você realmente deseja aquele emblema de sucesso, encontre algum truque para ele (como atrasar 10 minutos na execução do fluxo de trabalho).
Conclusão
Um monorepo ajuda a gerenciar a complexidade de uma grande base de código. Facilita a manutenção de um instantâneo ou estado coerente para todo o projeto, permite o envio de uma solicitação pull que envolve o código de vários pacotes e dá as boas-vindas aos contribuintes iniciantes para configurar o projeto sem soluços.
Todas essas características também podem ser obtidas usando uma grande variedade de repositórios, mas, na prática, são muito difíceis de executar.
Um monorepo deve ser gerenciado. Em relação aos pacotes PHP, podemos fazer isso por meio da biblioteca do construtor Monorepo. Neste artigo, aprendemos como instalar essa ferramenta, configurá-la e lançar nossos pacotes com ela.
A postagem Hospedando todos os seus pacotes PHP juntos em um monorepo apareceram primeiro no LogRocket Blog .