Uma alteração aparentemente menor numa biblioteca PHP partilhada pode interromper entregas independentes. Renomear um parâmetro, alterar um valor predefinido ou substituir uma exceção pode quebrar um consumidor que não é colocado em produção hoje, que está noutro repositório ou que invoca o componente de forma indireta. A falha pode surgir em tempo de execução, numa tarefa assíncrona ou ao desserializar dados gerados antes da alteração.
A compatibilidade retroativa em PHP não consiste em preservar toda a interface histórica. É uma disciplina para permitir que produtores e consumidores evoluam a ritmos distintos, com uma janela de migração explícita e uma remoção verificável. O objetivo é evitar tanto deployments coordenados forçados como a acumulação permanente de APIs obsoletas.
Identificar o que faz parte do contrato interno

Um contrato interno é qualquer comportamento do qual outro módulo depende, mesmo que não esteja publicado como API externa. As dependências do Composer e as interfaces PHP são uma parte visível, mas não esgotam o âmbito. Antes de alterar código partilhado, reveja pelo menos estes elementos:
- Assinaturas públicas: nomes de métodos, parâmetros, ordem, tipos, nulabilidade, valores predefinidos e tipo de retorno.
- Semântica: o que significa cada argumento, que campos são obrigatórios e que resultado é esperado perante uma condição específica.
- Erros: exceções lançadas, códigos de erro, mensagens processadas por clientes e resultados nulos ou vazios.
- Dados: chaves de arrays, estruturas JSON, mensagens de fila, eventos de domínio, ficheiros serializados e dados persistidos.
- Efeitos secundários: envio de eventos, escrita na base de dados, invalidação de cache, chamadas HTTP e ordem de execução.
- Comportamento operacional: tentativas de repetição, idempotência, limites de tempo e tratamento de falhas transitórias.
Por exemplo, adicionar um campo a uma resposta JSON costuma ser aditivo, mas deixa de o ser se um consumidor validar uma lista fechada de propriedades. Do mesmo modo, uma exceção mais específica pode estar tecnicamente correta, mas ser incompatível se o consumidor capturar a exceção anterior para ativar uma recuperação.
Classificar a alteração antes de escrever a implementação
A classificação evita que uma decisão de design se transforme num incidente de produção. Convém documentá-la na proposta da alteração, juntamente com os consumidores conhecidos e a estratégia de saída.
Alterações aditivas
Incorporam uma nova capacidade sem alterar o caminho existente: um novo método, um parâmetro opcional com semântica neutra, um evento adicional ou uma nova versão de uma mensagem. São a opção preferível quando os consumidores têm deployments separados. O novo caminho deve poder coexistir com o anterior e o comportamento prévio deve ser preservado de forma verificável.
Alterações compatíveis com adaptação
Permitem manter o resultado anterior através de uma camada de tradução. Por exemplo, uma interface antiga pode delegar num novo serviço, convertendo argumentos e resultados. A adaptação faz sentido se estiver localizada, tiver uma data de remoção e não ocultar uma diferença de negócio que o consumidor deva decidir conscientemente.
Alterações incompatíveis ou incertas
Remover um método, restringir um tipo, alterar o significado de um estado ou modificar um formato persistido costuma ser incompatível. Qualquer alteração sem um inventário fiável de consumidores também deve ser tratada como incerta. Em ambos os casos, não basta publicar uma nova versão do pacote: é necessária uma transição, uma migração planeada ou uma versão de contrato separada.
Construir um inventário verificável de consumidores
Não baseie a decisão apenas em pesquisas de texto. Um componente pode chegar a outro através de um contentor de dependências, uma configuração, reflexão, eventos, filas ou uma integração HTTP. O inventário deve combinar evidência estática e execução representativa.
- Reveja as dependências declaradas no Composer, as restrições de versões e os repositórios que instalam o pacote.
- Procure utilizações diretas de classes, interfaces, métodos, eventos, chaves de configuração e formatos de mensagem.
- Inspecione fábricas, definições do contentor, listeners, comandos, cron, workers e adaptadores de infraestrutura.
- Identifique caminhos críticos: cobrança, autenticação, encomendas, sincronização, notificações e processos de recuperação.
- Registe para cada consumidor o responsável, a versão utilizada, o caminho de migração e a evidência de que concluiu a alteração.
A publicação de uma biblioteca e o deployment de uma aplicação são ações distintas. Publicar uma versão compatível permite que cada consumidor atualize quando estiver preparado; fazer o deployment simultaneamente de todos os consumidores transforma uma evolução normal numa dependência organizacional frágil.
Aplicar evolução aditiva e adaptadores no limite correto
Quando um novo requisito altera o modelo, introduza primeiro uma nova capacidade e preserve temporariamente a anterior. Uma interface legada pode delegar na nova implementação, desde que a conversão seja inequívoca. Assim, os consumidores migram sem terem de coordenar uma janela única.
interface LegacyPriceCalculator
{
public function calculate(int $amount): int;
}
final class LegacyPriceCalculatorAdapter implements LegacyPriceCalculator
{
public function __construct(private PriceCalculator $calculator) {}
public function calculate(int $amount): int
{
return $this->calculator->calculate(new Money($amount, 'EUR'))->amount();
}
}O adaptador pertence normalmente ao limite entre contratos, não ao núcleo do domínio. O domínio deve expressar o modelo atual; a tradução de argumentos antigos, valores sentinela ou formatos históricos deve ficar numa camada dedicada. Se o domínio preservar condições para cada geração de clientes, a complexidade histórica propaga-se a cada alteração futura.
Não force um adaptador quando houver perda de informação ou uma nova decisão de negócio. Se o contrato antigo não contiver os dados necessários para o novo comportamento, mantenha ambos os contratos durante a transição ou peça explicitamente ao consumidor a informação adicional.
Transformar a depreciação numa remoção gerida
Uma API marcada como obsoleta sem alternativa, prazo nem responsável não é uma depreciação: é dívida sem acompanhamento. Uma remoção útil deve incluir um sinal no código, instruções de migração, uma condição de eliminação e observação da utilização quando possível.
- Marque o método ou a classe legada com documentação clara e, se aplicável, emita um aviso controlado com
trigger_error(..., E_USER_DEPRECATED). - Indique a alternativa exata, incluindo diferenças de semântica, erros e valores predefinidos.
- Defina uma condição de saída verificável: todos os repositórios inventariados migrados, ausência de chamadas observadas ou fim do suporte de uma versão específica.
- Atribua um responsável que reveja o progresso e remova a camada quando a condição for cumprida.
Evite emitir avisos indiscriminados em caminhos de elevado volume sem uma estratégia de agregação: o ruído pode ocultar sinais relevantes e aumentar o custo operacional. A observabilidade deve responder a uma pergunta concreta: que consumidores continuam a usar o contrato anterior e em que caminho.
Testar a transição e executar a sequência de entrega
Os testes unitários do componente não demonstram, por si só, que os consumidores continuam a funcionar. Adicione testes de contrato para as entradas, saídas e erros de que cada consumidor necessita. Mantenha casos de regressão para a interface antiga enquanto estiver suportada e teste explicitamente valores em falta, cargas serializadas anteriores e exceções esperadas.
A sequência segura costuma seguir esta ordem:
- Publicar o novo contrato ou a implementação aditiva mantendo o caminho anterior.
- Atualizar e fazer o deployment dos consumidores de forma independente, usando testes de integração quando o risco o justificar.
- Observar erros, avisos de depreciação e utilização da interface legada.
- Confirmar o inventário de migração e resolver consumidores indiretos detetados.
- Remover o adaptador ou o contrato antigo numa entrega separada, com testes que confirmem a sua ausência.
Lista de verificação para aprovar a alteração

- Está definido o contrato afetado para além da assinatura PHP?
- A alteração está classificada como aditiva, adaptável, incompatível ou incerta?
- Existe um inventário de consumidores, incluindo eventos, dados e caminhos indiretos?
- A solução evita exigir deployments simultâneos?
- O adaptador, se existir, está fora do domínio e tem remoção prevista?
- Foram testados o comportamento prévio, a nova capacidade e os erros esperados?
- A depreciação indica alternativa, condição de remoção e responsável?
- Existe um sinal para detetar dependências ocultas antes de eliminar a API?
A decisão correta não é manter compatibilidade indefinidamente nem impor coordenação total. É desenhar uma transição com limites: preservar o necessário, migrar com evidência e eliminar a compatibilidade histórica quando deixar de proporcionar segurança.



