AAAbel Aguiar
← Voltar para o blog
phplegadomodernizaçãoarquitetura

Modernização de sistemas legados em PHP sem parar o produto

Estratégias práticas para evoluir sistemas PHP legados com menos risco, mantendo o produto funcionando durante a transição

Abel Aguiar·
Modernização de sistemas legados em PHP sem parar o produto

Sistema legado não é sinônimo de sistema ruim. Muitas vezes ele é justamente o sistema que pagou as contas por anos, acumulou regra de negócio real e resistiu a trocas de equipe, fornecedores e prioridade.

O problema é que esse histórico cobra juros. Um projeto PHP antigo pode rodar bem o bastante para a operação continuar, mas ao mesmo tempo esconder deploy manual, regra duplicada, dependências vencidas, banco cheio de convenções implícitas e telas que todo mundo evita tocar.

Modernizar não é declarar guerra ao passado. É criar margem para continuar evoluindo sem tratar cada mudança como cirurgia de alto risco.

Comece pelo fluxo que dói

Antes de discutir se o destino é Laravel, Symfony, monólito modular ou serviços separados, vale responder uma pergunta mais simples: onde o sistema mais machuca o negócio hoje?

Imagine um ERP interno em PHP 7.2. O cadastro geral funciona, relatórios simples saem e a equipe aprendeu a conviver com algumas telas lentas. Mas o fechamento financeiro falha duas vezes por mês, depende de uma rotina noturna opaca e exige correção manual no banco. Esse é um candidato melhor para modernização do que uma tela antiga apenas porque o HTML parece feio.

Eu começaria listando fluxos críticos, integrações que mais geram incidente, rotinas que têm prazo legal ou financeiro e áreas que ninguém consegue alterar sem medo. A modernização precisa ter alvo. Sem isso, o time troca tecnologia e mantém o mesmo risco.

Teste o comportamento antes de melhorar o desenho

Em legado, nem sempre existe separação boa o bastante para começar com testes unitários elegantes. O primeiro passo costuma ser teste de caracterização: você registra o comportamento atual para saber se uma mudança alterou algo sem querer.

Se uma rotina calcula multa, juros e desconto a partir de dez condições históricas, talvez ninguém goste da implementação. Ainda assim, antes de refatorar, eu congelaria alguns cenários reais:

public function testMantemValorDoBoletoEmAtrasoComDescontoParcial(): void
{
    $resultado = $this->calculator->recalcular(
        valorOriginal: 1000_00,
        diasAtraso: 12,
        descontoManual: 150_00
    );

    self::assertSame(914_40, $resultado->valorFinal());
}

O objetivo inicial não é provar que o código está bonito. É criar uma cerca de proteção em torno da regra que sustenta o negócio. PHPUnit continua sendo a escolha natural para esse tipo de cobertura em PHP, inclusive quando o sistema ainda está longe de ter uma suíte madura.

Ambiente previsível vem antes de arquitetura sofisticada

Muita modernização trava porque cada máquina roda um PHP diferente, a homologação tem extensões que produção não tem e o deploy depende de memória tribal. Nessa situação, discutir arquitetura avançada antes de estabilizar o ambiente costuma ser inverter a ordem das coisas.

Padronizar execução local, versão de PHP, banco, cache e serviços auxiliares já reduz bastante o atrito. Docker Compose ajuda justamente nisso: a aplicação, o banco e dependências de apoio passam a subir de forma reproduzível.

Não precisa transformar tudo em infraestrutura complexa. Às vezes, sair de "só funciona na máquina do João" para "qualquer pessoa sobe com um comando" já muda a velocidade do time.

Ferramentas ajudam a ganhar terreno

Modernizar legado não é só mexer em arquitetura. Há ganhos importantes em tornar o código mais legível, mais tipado e menos perigoso antes mesmo de grandes extrações.

PHPStan funciona bem como adoção gradual porque permite começar em níveis baixos e subir conforme o código melhora. Em projetos muito antigos, a baseline evita que o time precise corrigir centenas de problemas de uma vez. Rector ajuda em outra frente: upgrades e refactors mecânicos que seriam cansativos e arriscados na mão.

Um roteiro comum é:

  1. Rodar análise estática em um escopo pequeno.
  2. Corrigir problemas óbvios de tipos, retornos e caminhos mortos.
  3. Automatizar refactors repetitivos.
  4. Só então atacar mudanças mais profundas de desenho.

Esse trabalho não aparece tanto em apresentações de arquitetura, mas costuma reduzir defeito real.

Modernize por bordas claras

É tentador abrir a pasta mais antiga do projeto e começar a reorganizar classes. O risco é passar semanas mexendo em tudo e entregar pouco. Eu prefiro escolher bordas que tenham entrada e saída reconhecíveis.

Uma borda clara pode ser uma integração externa, um endpoint específico, uma rotina de importação, um módulo administrativo ou um gerador de documentos. Se você consegue medir o comportamento antes e depois, comparar resultado e reverter com segurança, encontrou um bom candidato.

Suponha que o sistema envie notas fiscais por uma integração antiga diretamente espalhada em controllers e scripts cron. Uma modernização pragmática seria encapsular esse acesso em um serviço novo, manter o contrato atual para o restante da aplicação e, aos poucos, mover chamadas para a nova camada. O sistema segue produzindo nota. O risco fica contido.

O padrão strangler funciona porque evita o grande salto

Martin Fowler descreveu o padrão strangler como uma forma de substituir partes de um sistema de maneira gradual, criando a solução nova ao redor da antiga em vez de apostar tudo em uma grande virada. No contexto de aplicações PHP, isso costuma funcionar melhor do que prometer uma reescrita total "em paralelo".

Na prática, você pode colocar uma camada de entrada capaz de decidir se determinado fluxo ainda vai para o legado ou já segue para uma implementação nova. Um módulo de autenticação, um endpoint de consulta, um processamento assíncrono ou um painel administrativo podem ser migrados sem exigir que o restante da aplicação seja refeito no mesmo ciclo.

O ganho não é apenas técnico. Você entrega valor antes, mede comportamento em produção e reduz a chance de criar dois sistemas grandes e incompletos.

Banco de dados pede mais cautela que o código

Em muitos legados, o banco virou o verdadeiro centro do sistema. Há tabelas lidas por relatórios, jobs, integrações externas e telas antigas que já nem estão bem documentadas. Alterar schema sem entender esses vínculos é abrir espaço para incidente.

Antes de mexer em uma tabela importante, eu confirmaria quem lê, quem escreve, qual volume está envolvido, se há trigger ou procedure e se alguma integração consulta aquilo diretamente. Mudança de banco precisa ser preferencialmente aditiva e reversível.

Adicionar uma coluna nova, preencher dados em background e migrar leitura aos poucos costuma ser menos arriscado do que renomear algo crítico em um único deploy. O mesmo vale para índices e constraints: benefício técnico sem plano de rollout pode virar indisponibilidade.

Reescrita total costuma esconder uma aposta grande demais

Reescrever tudo é sedutor porque promete limpeza. O problema é que o sistema antigo continua recebendo bugfix, regra nova e pressão operacional enquanto o novo tenta alcançar paridade. Quando isso se arrasta, o time passa a manter dois mundos e nenhum deles fica realmente confortável.

Uma modernização saudável entrega progresso em ciclos curtos. Reduz o tempo de deploy. Remove uma integração instável. Coloca teste em um cálculo importante. Separa uma borda útil. Melhora observabilidade de um fluxo crítico. Cada etapa deixa o produto um pouco menos frágil.

O sinal de que a estratégia está funcionando não é a quantidade de código reescrito. É a quantidade de risco removido.

Feature flag é controle de exposição

Em legado, ativar uma mudança para todo mundo de uma vez nem sempre é razoável. Feature flag permite liberar um fluxo novo para um grupo pequeno, comparar resultado e desligar sem novo deploy se algo escapar.

Se a aplicação estiver em Laravel, Pennant é uma opção oficial para esse tipo de rollout. Mesmo fora de Laravel, o princípio continua útil: a mudança precisa ter uma chave de controle clara, comportamento previsível e remoção planejada depois que a transição termina.

Flag não substitui teste nem telemetria. Ela serve para reduzir blast radius enquanto você aprende em produção.

Um roteiro realista

Se eu pegasse hoje um sistema PHP legado em produção, começaria assim:

  1. Mapear os fluxos que mais afetam receita, operação ou obrigação legal.
  2. Criar testes de caracterização nos comportamentos de maior risco.
  3. Padronizar ambiente e pipeline básico.
  4. Introduzir análise estática em escopo controlado.
  5. Escolher uma borda clara para a primeira modernização.
  6. Fazer mudanças de banco de forma aditiva e observável.
  7. Liberar o novo fluxo com flag quando o impacto justificar.

Não é um plano glamouroso. É um plano que tende a chegar ao fim.

Conclusão

Modernizar sistemas legados em PHP é menos sobre apagar o passado e mais sobre recuperar capacidade de mudança. O sistema antigo carrega conhecimento de negócio. A engenharia precisa preservar esse conhecimento enquanto reduz surpresa, improviso e medo de deploy.

Quando a modernização começa pelo fluxo certo, ganha testes antes de refactor, estabiliza o ambiente, usa ferramentas a favor do time e avança por bordas claras, o produto continua rodando enquanto o software melhora.

Esse costuma ser o caminho mais sólido: evolução contínua, com risco controlado e resultado visível.

Posts relacionados