O que é o Composer?
O Composer é um gestor de dependências para PHP, equivalente ao NPM ou ao Yarn em JavaScript e ao pip em Python. Simplifica a gestão dos pacotes de que a tua aplicação depende. Como «serviço» adicional, permite ainda que cada pacote registe um autoloader. É essa combinação que faz dele, na minha opinião, um dos utilitários mais críticos do ecossistema PHP.
O repositório de pacotes mais usado pelo Composer é o Packagist.org. Este repositório só aceita pacotes open source. Se quiseres tirar partido de tudo o que o Composer oferece, podes recorrer à sua oferta privada. É uma versão paga do Packagist, construída pelos criadores do Composer, que permite usar pacotes de código fechado com controlo de acessos granular.
Não vou explicar aqui como instalar e configurar o Composer: os mantenedores fazem-no em detalhe na documentação oficial. Introdução ao Composer no site do projeto. Em todos os exemplos com a CLI, parto do princípio de que o Composer está instalado globalmente e se invoca com composer.
Separar dependências normais e de desenvolvimento
O Composer distingue dois tipos de dependências: as normais e as de desenvolvimento. As normais são aquelas de que o teu código precisa sempre, seja qual for o ambiente em que corre. As de desenvolvimento só servem enquanto se programa: frameworks de teste como o PHPUnit, analisadores estáticos como o Psalm, verificadores de estilo como o PHP Code Sniffer. Não são precisas para executar o código em produção. Quando acrescentas uma dependência com composer require [package], ela fica marcada como «dependência normal». Com a flag --dev, o pacote fica marcado como «dependência de desenvolvimento».
Ao correr composer install, o Composer descarrega e instala as dependências de produção e as de desenvolvimento da tua aplicação. Para instalar apenas as de produção, acrescenta a flag --no-dev. É normalmente o que queres na tua pipeline de deployment, onde as dependências de desenvolvimento não servem para nada.
Nota composer install só instala as dependências de desenvolvimento da tua aplicação ou do teu pacote, e não as das tuas dependências. Se dependes do «Pacote A» e o «Pacote A» tem uma dependência de desenvolvimento do «Pacote B», só o «Pacote A» entra no teu projeto.
As dependências de desenvolvimento acumulam-se depressa até uma lista respeitável e, com ela, uma boa quantidade de ficheiros, todos carregáveis pelo autoloader. Reduzir ao mínimo as dependências de produção poupa espaço em disco, tempo de transferência (durante o deployment) e tempo de execução. À pequena escala a poupança é negligenciável, mas cresce à medida que a aplicação e o número de pessoas que lhe mexem aumentam. Outra vantagem de marcar as dependências de desenvolvimento é que ficam de fora do autoloader gerado pelo Composer quando corres composer install --no-dev.
Carregamento automático ( autoload.php )
Como já foi dito, outra característica do Composer é oferecer utilitários que permitem aos pacotes enxertarem-se no teu projeto PHP através do carregamento automático ( autoload.php ). Com esta funcionalidade, cada pacote indica ao Composer como fazer corresponder um nome de classe a um nome de ficheiro. Deixas de encher o código de chamadas require_once e require: basta incluir vendor/autoload.php.
Tal como nas dependências, o Composer propõe dois tipos de autoloader: as diretivas autoload normais e as diretivas autoload de desenvolvimento. Os pacotes usam-nas para reduzir o seu tamanho e o do autoloader, mas as aplicações também ganham com isso. Num projeto bem testado, os ficheiros usados apenas nos testes podem representar uma boa fatia do código. O mesmo se aplica aos utilitários de desenvolvimento, como as ferramentas de linha de comandos: não precisas deles em produção. Ao dizer ao autoloader que são ficheiros de desenvolvimento, deixam de ser considerados quando executas composer install --no-dev, o que pode melhorar o desempenho da aplicação. A distinção entre autoload normal e autoload de desenvolvimento faz-se no ficheiro de configuração composer.json:
{
"autoload": {
"psr-4": {
"MyApp\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MyApp\Tests": "tests/"
}
}
}
O Composer oferece ainda ferramentas para acelerar o carregamento automático de funções e classes, através da otimização do autoloader.
Restrições de versão
Antes do Composer, descarregava-se uma versão precisa de uma dependência e colocava-se no controlo de versões. Com o Composer, podemos afastar-nos de uma versão fixa e deixar que ele decida exatamente qual instalar. Para o fazer, apoia-se fortemente no conceito de versionamento semântico. E não o aplica só à tua aplicação: estende-o às dependências. Assim, cada pacote pode ter as suas próprias dependências, que por sua vez têm as suas, e assim sucessivamente. O resultado são mais pacotes, mas mais pequenos. Pacotes pequenos concentram-se numa única funcionalidade, o que permite centralizá-la.
Ao contrário do NPM, por exemplo, o Composer só admite uma versão de cada pacote por aplicação. Instalam-se menos pacotes, mas surge um risco adicional de colisões de versões. É uma boa razão para não pedir uma versão exata de uma dependência e pedir antes um intervalo de versões. Faz-se normalmente com o operador caret: ^1.0. Significa que qualquer versão >= 1.0.0 e < 2.0.0 é aceite. Como estamos em versionamento semântico, tens a garantia de dispor das funcionalidades necessárias sem que nenhuma quebra de compatibilidade seja introduzida.
Ao indicar um intervalo em vez de uma versão exata, evitas colisões quando outra dependência usa o mesmo pacote. Imagina a situação seguinte.
O composer.json da tua aplicação:
{
"require": {
"vendor/package": "^1.4",
"another_vendor/package": "^1.0"
}
}
O composer.json de vendor/package:
{
"require": {
"another_vendor/package": "^1.2"
}
}
Ao instalar as dependências, o Composer deteta vários pedidos de another_vendor/package mas, como se trata de restrições, consegue determinar que deve evitar a versão 1.1.5 e instalar antes a 1.2.8.
O Composer tem vários operadores de restrição e permite até combiná-los. Uma restrição como “php”: “^7.2” não inclui o PHP 8. Para declarar que tanto o PHP 7.2 como o PHP 8 são suportados, combinam-se dois operadores: “php”: “^7.2 || ^8.0”.
Nota Outra opção seria seguir a abordagem que a Symfony escolheu, com "php": >= 7.2". Pessoalmente não sou favorável, porque isso anuncia que o código também funciona em PHP 9 e seguintes. Como não há maneira de saber que quebras de compatibilidade o PHP 9 vai trazer, prefiro não fazer essa promessa, e é por isso que uso "php": "^7.2 || ^8.0" nos meus repositórios.
Root ou não root
Depois de analisar um composer.json, podemos separar as dependências exigidas diretamente na raiz do teu projeto das restantes. Na terminologia do Composer, os Root Packages são as dependências de raiz que recorrem, elas próprias, às suas dependências.
Pega neste composer.json:
{
"require": {
"laminas/laminas-diactoros": "^2.0"
}
}
Correr composer install sobre este ficheiro mostra que não só laminas/laminas-diactoros é descarregado, mas também laminas/laminas-zendframework-bridge, psr/http-factory e psr-http-message. Neste exemplo, laminas/laminas-diactoros é um Root Package fundamental no nosso projeto.
A diferença entre um pacote de raiz e um pacote derivado está no número de dependências diretas do teu projeto. Em geral, menos pacotes de raiz ( root package ) significa menos pacotes e menos conflitos na hora de atualizar. Menos pacotes porque uma nova versão de um pacote pode trazer dependências diferentes, geridas automaticamente pelo Composer. E menos conflitos porque há menos restrições na tua árvore de dependências.
Mas o que torna um pacote candidato à raiz?
{
"require": {
"laminas/laminas-diactoros": "^2.0"
}
}
Voltemos ao exemplo em que dependemos apenas de laminas/laminas-diactoros. O diactoros oferece uma implementação das interfaces especificadas em psr-http-message e psr/http-factory. Se no meu código só referir objetos da interface laminas/laminas-diactoros, esta configuração do Composer é válida. Mas se referir as interfaces definidas em psr-http-message e psr/http-factory, o que, na minha opinião, é boa prática, então há razão para tornar psr-http-message e psr/http-factory pacotes de raiz do nosso projeto ( root package ).
{
"require": {
"laminas/laminas-diactoros": "^2.0",
"psr-http-message": "^1.0",
"psr/http-factory": "^1.0"
}
}
Declarar o teu ambiente
Constróis a tua aplicação ou o teu pacote sobre uma dada versão de PHP. E só nessa versão consegues garantir o bom funcionamento do código. Talvez ele exija também uma extensão PHP instalada. Por isso é boa prática indicar que versões de PHP são exigidas ou suportadas e que extensões são necessárias.
Quando desenvolves um pacote, as versões de PHP e as extensões PHP declaram-se como se fossem pacotes normais:
{
"require": {
"php": "^7.2",
"ext-curl": "*"
}
}
Podemos usar o wildcard * para a extensão cURL porque a extensão está ligada ao PHP: não há versão a especificar.
Também podes seguir esta abordagem ao construir uma aplicação, declarando além disso essas restrições como configuração da plataforma:
{
"config": {
"platform": {
"php": "7.4.4",
"ext-curl": "*"
}
}
}
A vantagem é que o Composer passa a usar o PHP 7.4.4 como versão de referência, mesmo que a máquina onde os comandos correm tenha outra instalada. Com a generalização de soluções de virtualização como o Docker e o Vagrant, isto permite correr os comandos do Composer localmente e executar a aplicação no ambiente virtualizado. Evita também que um colega com uma versão de PHP de ponta instale versões de dependências que o ambiente de produção não suporta.
Trabalhar com um sistema de controlo de versões ( Git, Mercurial, Subversion )
No teu projeto, o Composer deixa três entidades no sistema de ficheiros: dois ficheiros chamados composer.json e composer.lock e uma pasta chamada vendor*. Mas não é preciso incluir as três no controlo de versões.
O ficheiro composer.json contém todos os pacotes exigidos com as respetivas restrições de versão, ver a secção «Restrições de versão». Este ficheiro deve sempre ser adicionado ao controlo de versões. Sem ele, quem quiser usar o teu código não faz ideia dos pacotes de que precisa.
O ficheiro composer.lock contém as versões exatas, incluindo as somas de verificação (checksum), de todos os pacotes. Se estiver presente quando corres composer install, são instaladas exatamente as versões especificadas no ficheiro composer.lock. É esta funcionalidade que garante dependências idênticas em todos os ambientes. Ao construir uma aplicação, convém adicioná-lo ao controlo de versões. A razão principal é que as aplicações (ou outros pacotes) que usam o teu pacote não o têm em conta, mas o teu ambiente de desenvolvimento (local) tem. Isso pode levar-te a desenvolver ou testar com versões desatualizadas das tuas dependências, enquanto quem usa o teu pacote corre as mais recentes.
A pasta vendor contém o código dos pacotes. O seu conteúdo não deve ir para o controlo de versões. Se usas o Git, acrescenta-a ao .gitignore para não a versionares.
* Podes mudar o nome e a localização desta pasta através da opção de configuração vendor-dir, mas neste artigo fico-me pelo valor por omissão.
Alterar as dependências
Imagina que queres atualizar uma dependência da tua aplicação. Ou várias. Corres alguns composer update e vês o composer.lock mudar. Fazes um commit. Outra ronda de atualizações, outro commit. Uns dias depois retomas o trabalho onde o deixaste. Como o repositório é muito ativo, antes de continuares fazes rebase para garantir que as tuas alterações são compatíveis com a última versão do código. Conflito de merge! Um conflito no composer.json resolve-se como qualquer outro, mas um conflito no composer.lock é bem mais complicado.
A forma de resolver conflitos de merge num composer.lock é aceitar as alterações que chegam, são as mais recentes, e voltar a correr os comandos do Composer que produziram as tuas alterações. Assim, elas passam a aplicar-se sobre a versão mais recente do código. Faz commit das alterações do composer.lock e continua o rebase. Se tiveres feito mais trabalho com o Composer num commit seguinte, terás de repetir estes passos.
Conclusão
O Composer tornou-se uma peça essencial do desenvolvimento em PHP. Como qualquer gestor de pacotes, precisa de alguns cuidados. Uso-o todos os dias em vários repositórios e, ao longo desse tempo, fixei algumas práticas que considero boas práticas:
- Separa as tuas dependências em dependências de produção e de desenvolvimento, para reduzir o tamanho da aplicação.
- Separa o teu autoloader em autoloader de produção e de desenvolvimento, para o encolher e ganhar desempenho.
- Não dependas de uma versão fixa das dependências, mas de um intervalo, para tornar as atualizações menos dolorosas.
- Depende diretamente apenas das dependências que realmente chamas na tua aplicação, para simplificar a atualização ou a troca por uma alternativa.
- Põe sempre o
composer.jsonno controlo de versões, mas só coloca lá ocomposer.locknas aplicações, para não impedir quem usa o teu pacote de atualizar outros pacotes. - Tenta ter no máximo um commit por pull request/merge request com as alterações de
composer.jsonecomposer.lock, para minimizar os conflitos de merge nestes ficheiros.



