← Voltar ao blog

PHPStan no Magento 2: guia de setup de análise estática

Rodrigo Biazzi

O código Magento está cheio de lugares onde um tipo pode estar errado sem que nada falhe até a produção. Um método de repository que devolve uma interface, um item de collection que pode ser null, um valor de config que é string quando você espera um inteiro. O próprio PHP só percebe quando a linha roda. O PHPStan lê o código sem executá-lo e reporta esses problemas antes de irem para o ar.

Rodar o PHPStan no Magento costumava ser frustrante, principalmente por causa das classes geradas e dos métodos mágicos. Com a extensão certa e um ponto de partida sensato, hoje faz parte normal de um pipeline Magento. Este artigo mostra como configuro em projetos 2.4.8.

O que o PHPStan encontra

Achados típicos em código customizado do Magento:

  • Chamar um método em um valor que pode ser null, por exemplo o resultado de uma busca que devolve um item ou null
  • Chamar um método que não existe no tipo declarado, muitas vezes depois de um refactor
  • Passar uma string onde um int foi declarado, por exemplo um store ID vindo de um parâmetro do request
  • Tipos de retorno que não batem com o tipo de retorno declarado
  • Propriedades e métodos privados sem uso, branches mortos e condições sempre verdadeiras
  • Argumentos errados em métodos do core depois de um upgrade ter mudado a assinatura

Nada disso é questão de estilo. São bugs ou bugs futuros.

Instalação

Instale o PHPStan e a extensão do Magento como dependências de dev:

composer require --dev phpstan/phpstan bitexpert/phpstan-magento phpstan/extension-installer

O phpstan/extension-installer registra automaticamente as extensões instaladas, então você não precisa incluir os arquivos de config delas na mão. O Composer vai pedir para você permiti-lo como plugin.

A extensão bitexpert/phpstan-magento é o que torna o PHPStan utilizável no Magento. Entre outras coisas, ela sabe resolver as classes que o Magento gera em runtime, como factories e proxies, então o PHPStan não as reporta como classes inexistentes. Ela também entende as interfaces de extension attributes. Consulte o README da extensão para a lista completa de recursos na versão que você instalar.

Configuração

Crie o phpstan.neon na raiz do projeto:

parameters:
    level: 5
    paths:
        - app/code
    excludePaths:
        - app/code/*/*/Test/*
    tmpDir: var/phpstan

Analise só o seu código. O PHPStan ainda vai ler classes do core e do vendor para saber os tipos delas, só não reporta erros nelas. Analisar ou não os testes depende da sua equipe. Testes com muitos mocks geram muito ruído nos levels mais altos, então muitos projetos os excluem no início e os incluem depois.

Execute:

vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=2G

O Magento é um codebase grande de ler, então o limite de memória padrão costuma ser baixo demais. Rode dentro do container PHP do seu ambiente local, para ele enxergar a mesma versão do PHP e as mesmas extensões da aplicação.

Escolhendo um level

Os levels do PHPStan vão de 0, que checa só coisas básicas como classes desconhecidas, até os levels mais altos, que checam tratamento estrito de null, tipos mixed e mais. Os levels em resumo:

  • Levels baixos encontram classes e métodos desconhecidos, número errado de argumentos e variáveis indefinidas. Todo projeto deveria passar neles.
  • Levels intermediários adicionam tipos de retorno, tipos de argumento, código morto e variáveis possivelmente indefinidas. É um bom alvo para a maioria dos projetos Magento.
  • Levels altos adicionam checagens estritas de valores nullable, union types e mixed. Muito úteis em código novo, mas o core do Magento devolve valores com tipagem frouxa em muitos lugares, então espere atrito.

A abordagem prática: rode cada level do baixo para o alto e pare quando o número de erros passar do que a equipe consegue corrigir em um tempo razoável. Defina esse como o level, adicione um baseline e suba o level depois.

Baselines para código existente

Em um projeto com anos de código customizado, a primeira execução pode reportar centenas de erros. Um baseline registra os erros atuais para o PHPStan ignorá-los e reportar só os novos:

vendor/bin/phpstan analyse -c phpstan.neon --generate-baseline

Isso grava o phpstan-baseline.neon. Inclua-o a partir da config principal:

includes:
    - phpstan-baseline.neon

parameters:
    level: 6
    paths:
        - app/code
    tmpDir: var/phpstan

Faça commit do baseline. A partir daí, o CI falha só em erros que não estão nele. Quando alguém corrige um erro do baseline, o PHPStan avisa que o erro ignorado não ocorre mais, então o baseline precisa ser regenerado. Isso é bom, porque mantém o arquivo encolhendo com o tempo.

Problemas comuns no Magento e como corrigir

Resultados nullable

public function getBrandLabel(int $productId): string
{
    $product = $this->findProduct($productId);

    return (string) $product->getCustomAttribute('brand')?->getValue();
}

Se o findProduct() é declarado como retornando ?ProductInterface, o PHPStan reporta a chamada de método em um valor possivelmente null nos levels que checam nullability. A correção é tratar o caso null explicitamente, em geral com um return antecipado ou uma exceção, em vez de torcer para que nunca aconteça.

Getters mágicos em models

Models que estendem DataObject respondem a getAnything() pelo __call. O PHPStan não tem como saber qual deles devolve o quê. A correção mais limpa é usar métodos da interface, como getSku() ou getData('field') com um cast explícito, e adicionar getters de verdade com tipos de retorno aos seus próprios models e data interfaces.

Valores de config

O ScopeConfigInterface::getValue() devolve mixed. Faça o cast do resultado para o tipo de que precisa no ponto onde lê, e faça isso em uma classe pequena por módulo que exponha métodos tipados como isEnabled(): bool. Isso tira os casts do resto do código e dá ao PHPStan tipos exatos.

Arrays sem formato

Nos levels mais altos o PHPStan reclama de arrays sem tipo de valor. Use docblocks como @return array<int, string> ou @param array{sku: string, qty: float} $row. Eles documentam a intenção tanto quanto satisfazem a ferramenta.

Código gerado e di:compile

A extensão do Magento resolve factories e proxies mesmo com o generated/ vazio. Se aparecerem erros sobre interceptors ausentes ou outras classes geradas, confira se a extensão está instalada e registrada. Rodar o setup:di:compile antes do PHPStan também funciona, mas é lento e não deveria ser requisito de um job de análise estática.

Rodando no CI

O PHPStan se encaixa ao lado do PHPCS e dos testes unitários no job de checks de um pipeline, como no artigo sobre GitHub Actions. Duas dicas:

  • Faça cache do tmpDir entre as execuções. O cache de resultados do PHPStan deixa as execuções repetidas bem mais rápidas quando poucos arquivos mudaram.
  • Use --error-format=github no GitHub Actions para os achados aparecerem como anotações no diff do pull request.
vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=2G --no-progress --error-format=github

PHPStan e upgrades

A análise estática é especialmente útil durante um upgrade de Magento ou de PHP. Quando um método do core muda de assinatura ou uma classe é removida, o PHPStan encontra toda chamada no código customizado em segundos, sem navegar pela loja. Rode na branch de upgrade depois do composer update e trate os erros novos como a primeira parte do trabalho de upgrade. O artigo de compatibilidade com PHP 8 cobre o lado do PHP.

Onde o PHPStan para

O PHPStan não conhece XML. Um plugin declarado para um método que não existe, uma classe errada no di.xml ou um erro de digitação no nome de um evento não vão aparecer. O setup:di:compile pega alguns desses, testes de integração pegam mais. Ele também não avalia design nem performance. Combine com o coding standard do Magento e um code review de verdade.

FAQ

Por que o PHPStan diz que uma classe Factory não existe?

Factories, proxies e classes parecidas são geradas pelo Magento e podem não existir no disco quando o PHPStan roda. A extensão bitexpert/phpstan-magento ensina o PHPStan sobre elas, então instale-a em vez de excluir esses erros.

Qual level do PHPStan um projeto Magento deve usar?

Comece no level mais alto que o seu código passa com um baseline administrável, muitas vezes em algum ponto do meio. Suba um level por vez. Módulos novos normalmente aguentam um level mais estrito que os antigos.

O PHPStan deve analisar o vendor/magento?

Não. Analise o seu próprio código em app/code e seus próprios pacotes. O PHPStan ainda lê as classes do core para entender os tipos, só não reporta erros nelas.

Qual a diferença entre PHPStan e PHPCS?

O PHPCS verifica como o código é escrito: formatação, nomes e padrões proibidos. O PHPStan verifica se o código está correto: tipos, existência de métodos, tratamento de null e código morto. Um complementa o outro.

Atendo como freelancer em Magento e Adobe Commerce, incluindo setups de qualidade de código para projetos existentes. Se precisar de ajuda com isso, fale comigo.

Artigos relacionados

Erros comuns do Magento 2 e como corrigir

Correções para erros clássicos do Magento 2: Area code is not set, Class does not exist, Invalid Form Key, indexers travados, unserialize e 404 em estáticos.