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:

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 :

Symfony Monorepo Estrutura Multinível
Estrutura monorepo do 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/):

Monorepo Estrutura Código Distribuir Níveis Camadas Pacotes
Estrutura monorepo PoP

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 monorepo-builder init para configurá-lo e também criar um novo repositório para cada um de seus novos pacotes. Caso contrário, se você estiver desenvolvendo seus pacotes em seus próprios repositórios, precisará portá-los para o monorepo.

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:

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 os composer.json não entram em conflito
  • monorepo-builder merge extrai todas as dependências (e outras informações) de todo o composer.json e as mescla no próprio composer.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:

Arquivo de configuração Monorepo Builder Stringified JSON Output
Recuperando a lista de 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 tag 0.7

E então, ele reverte o código para desenvolvimento:

  • Atualize o alias do branch para dev-master em todos os pacotes para 0.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:

Monorepo Builder Command Run Tag Packages

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 .

Source link