PHP moderno não precisa de defesa — precisa de gente usando direito

Depois de dez anos entregando sistemas em PHP, a discussão que ainda me cansa não é 'PHP é ruim?'. É ver time novo escrevendo Laravel 12 com a cabeça do PHP 5.6. Tipos, enums, readonly, filas e testes mudam mais o dia a dia do que qualquer troca de linguagem.

Alessandro Melo//4 min de leitura

Toda vez que aparece uma thread sobre "PHP morreu", eu abro o terminal de algum projeto em produção e olho o que está rodando ali: Laravel 12, PHP 8.4, filas em Redis, painel em Filament, testes em Pest, deploy em Docker atrás de Nginx. Nada disso lembra o PHP que virou meme. E, sinceramente, a defesa da linguagem já foi ganha faz tempo — o problema hoje é outro.

O problema é que muito time escreve código de 2013 dentro de um framework de 2026. A stack evoluiu; o hábito, não.

O sintoma: array como contrato

O primeiro cheiro é sempre o mesmo. Um método de service que recebe array $data, mexe em dez chaves e devolve outro array. Ninguém sabe o que entra, ninguém sabe o que sai, e o autocomplete da IDE vira decoração. Aí o time compensa isso com documentação em Notion, que envelhece em duas sprints.

PHP tem tipagem forte o suficiente para acabar com isso desde a 7.4, e desde a 8.x ficou confortável de verdade. Um objeto de valor com readonly resolve mais do que um bloco de PHPDoc:

final readonly class NovaAssinatura
{
    public function __construct(
        public int $clienteId,
        public PlanoAssinatura $plano,
        public Carbon $inicioEm,
        public ?string $cupom = null,
    ) {}
}

Agora o contrato existe no código, não na cabeça de quem escreveu. Se alguém tentar passar um cupom como int, o erro aparece no CI, não no suporte.

Enum é a ferramenta mais subaproveitada da linguagem

Máquina de estados em string solta é a causa de bug mais chata que já depurei. 'pending', 'PENDING', 'pendente', 'aguardando' — todos no mesmo banco, todos escritos por pessoas diferentes ao longo de dois anos. Enums nativos, que chegaram no PHP 8.1, matam essa categoria inteira de problema.

enum StatusDemanda: string
{
    case Aberta = 'aberta';
    case EmVisita = 'em_visita';
    case Orcada = 'orcada';
    case Concluida = 'concluida';

    public function podeAvancarPara(self $proximo): bool
    {
        return in_array($proximo, $this->proximosPassos(), strict: true);
    }
}

O Eloquent faz o cast direto no model, o Filament monta o select sozinho a partir do enum, e a regra de transição fica no mesmo arquivo que define os estados. Num marketplace de serviços que eu mantenho, esse único movimento cortou a maior fonte de chamado do suporte: pedido que "pulava" etapa porque alguém escreveu a string errada numa migration de correção.

Fila não é otimização, é design

A outra herança do PHP antigo é achar que tudo tem que acontecer dentro do request. Envio de e-mail, geração de PDF, chamada de gateway, processamento de imagem — tudo empurrado no ciclo do HTTP porque "é rápido". Até o dia em que o gateway demora oito segundos e o cliente clica duas vezes no botão de pagar.

Filas mudam a forma de modelar a operação, não só o tempo de resposta. Quando o job é a unidade de trabalho, você ganha de graça três coisas que dificilmente ia implementar na mão: retentativa com backoff, isolamento de falha e visibilidade. Eu tenho pipeline de vídeo em produção onde cada etapa — baixar, transcrever, selecionar trecho, cortar, publicar — é um job separado. Quando a transcrição falha por limite de API, só aquela etapa volta para a fila. O resto do pipeline não sabe e não precisa saber.

A regra prática que uso: se a operação depende de um sistema que eu não controlo, ela não roda no request.

Teste que você escreve porque quer

A objeção clássica contra teste em PHP era a ergonomia. PHPUnit funciona, mas escrever suíte nele nunca foi prazeroso. Pest resolveu isso de um jeito quase injusto — a barreira caiu tanto que hoje é mais rápido escrever o teste do que abrir o Postman e reproduzir o cenário na mão.

it('bloqueia checkout quando a loja está fechada', function () {
    $loja = Loja::factory()->fechada()->create();

    postJson('/api/checkout', payloadValido($loja))
        ->assertStatus(422)
        ->assertJsonPath('erro', 'loja_fechada');
});

Não é sobre cobertura de 100%. É sobre ter uma rede embaixo das regras que dão dinheiro: pagamento, cupom, zona de entrega, transição de status. Essas eu testo sempre. Layout de e-mail, não.

O que eu não faço mais

Duas coisas que já defendi e abandonei.

A primeira é encher o projeto de camada. Repository que só faz proxy pro Eloquent, DTO para toda ação, interface com uma implementação só. Isso não é arquitetura limpa, é imposto de indireção. O Eloquent já é a camada de dados; brigar com ele custa caro e não devolve nada.

A segunda é perseguir a última versão de tudo no dia do lançamento. Laravel tem ciclo previsível e política de suporte clara justamente para você planejar a atualização. Subir versão major numa sexta porque saiu no Twitter é escolha, não obrigação.

O ponto

PHP moderno é uma linguagem tipada, com enums, com atributos, com um ecossistema que resolve fila, painel, teste e deploy sem você escrever nada disso do zero. A linguagem cumpriu a parte dela. Falta o hábito acompanhar — e isso não se resolve trocando de stack, se resolve lendo a documentação da versão que você está usando e parando de escrever array $data.

Fontes

  1. Laravel 12 — Release Notes
  2. PHP Manual — Enumerations
  3. PHP Manual — Readonly properties
  4. Laravel Docs — Queues
  5. Laravel Docs — Eloquent: Relationships
  6. Pest — The elegant PHP testing framework
Voltar para o blog