Idealmente, devemos sempre instalar a versão mais recente do PHP em nossos servidores web. No momento, isso é PHP 8.0.

Em muitas circunstâncias, no entanto, isso não é possível. Considere situações em que nossos clientes estão executando software legado que é incompatível com a versão mais recente do PHP. Ou talvez não controlemos o ambiente, como ao construir um plug-in para WordPress para o público em geral.

Nessas situações, transpilando o código PHP faz sentido porque nos permite usar os recursos PHP mais recentes para desenvolvimento e, ao mesmo tempo, lançar o software com seu código convertido em uma versão PHP mais antiga para produção.

Um meme tornando a codificação leve com PHP 8.0 e implantando como PHP 7.1

Neste artigo, aprenderemos várias dicas para transpilar do PHP 8.0 para 7.1.

O PHP 7.1 é bom o suficiente?

O downgrade é realizado por meio do Rector , a ferramenta de reconstrução do PHP. O PHP 7.1 é o alvo para o downgrade porque essa é atualmente a versão mais baixa do PHP que o Rector pode suportar para downgrades. (No futuro, possivelmente seremos capazes de fazer downgrade para 7.0 e 5.6 .)

Como PHP 7.1 já é EOL , isso deve ser suficiente para a maioria dos cenários. Afinal, devemos sempre executar apenas uma versão do PHP mantida ativamente, o que significa PHP 7.3 e superior. Caso contrário, corremos o risco de usar o PHP contendo vulnerabilidades não corrigidas.

Infelizmente, nem sempre é esse o caso. WordPress, por exemplo, ainda suporta PHP 5.6 e, portanto, um plug-in usando PHP 7.1 não estará disponível para usuários que executam WordPress em PHP 5.6 e 7.0, que atualmente fica em torno de 16,4 por cento de todos os usuários do WordPress .

Se seus usuários dependem de software legado e você está desenvolvendo com uma versão muito antiga do PHP, como 5.6, você deve considerar se vale a pena pular para o PHP 7.1. Se for, você pode pular diretamente para o uso do PHP 8.0 graças à transpilação.

Na minha situação, uma vez que apenas aplicativos modernos executarão GraphQL, meu plug-in API GraphQL para WordPress não deve ser muito afetado por omitir usuários executando WordPress 5.6 e 7.0, então vale a pena.

No caso de Yoast , porém, o impacto será grande: porque tem mais de 5 milhões de instalações ativas , excluindo 16,4 por cento pode significar cerca de 1 milhão de usuários. Não vale a pena.

O que podemos alcançar transpilando o código PHP?

Depois de introduzir a transpilação em meu plug-in, consegui aumentar sua versão mínima exigida do PHP para 8.0 (para desenvolvimento).

A recompensa é grande: tendo acesso aos tipos de união do PHP 8.0, além das propriedades digitadas do PHP 7.4, fui capaz de adicionar tipos estritos em todos os lugares da base de código do plugin (incluindo todos os parâmetros de função, declarações de retorno e propriedades de classe), o que se traduz em menos bugs e código mais compreensível.

Estou emocionado em este trecho de código que agora posso produzir:

 interface CustomPostTypeAPIInterface
{ public function createCustomPost (array $ data): string | int | null | Erro;
} 

O tipo de retorno desta função expressa que uma das seguintes situações ocorreu:

  • O novo objeto de postagem personalizado foi criado com sucesso, retornando seu ID, que é do tipo string ou int
  • O novo objeto não foi criado devido à falha na validação, retornando null
  • O novo objeto não foi criado devido a algo errado no processo (por exemplo, a conexão com uma API de terceiros necessária falhou) retornando um objeto personalizado do tipo Erro , que também contém uma mensagem de erro

Assim, transpilar me dá a chance de me tornar um desenvolvedor melhor, produzindo código com maior qualidade.

Como o código transpilado se comporta na produção

Depois de transpilar o código acima para PHP 7.1, o tipo de retorno será removido:

 interface CustomPostTypeAPIInterface
{ public function createCustomPost (array $ data);
} 

Agora, se houver uma incompatibilidade de tipo entre o tipo de retorno desta função e onde ela está sendo chamada, já estarei ciente disso durante o desenvolvimento e corrigirei o problema.

Portanto, remover o tipo de retorno para produção não produz nenhuma consequência.

Quais novos recursos estão disponíveis?

Ser capaz de codificar com o PHP 8.0 não significa que todos os recursos do PHP versões 8.0, 7.4, 7.3 e 7.2 podem ser usados. Em vez disso, apenas aqueles recursos para os quais há uma regra de downgrade no Rector podem ser usados, mais aqueles sendo backported pelos pacotes polyfill do Symfony ( polyfill-php80 , polyfill-php74 , polyfill-php73 e polyfill-php72 ).

Por exemplo, atualmente não há como fazer o downgrade dos atributos do PHP 8.0 , portanto, não podemos usar esse recurso. No momento em que este livro foi escrito, a lista de recursos PHP disponíveis para um aplicativo codificado com PHP 8.0 a ser rebaixado para 7.1 é a seguinte:

Versão PHP Recursos
7.1 Tudo
7,2 ✅ tipo de objeto
✅ Ampliação do tipo de parâmetro
✅ sinalização PREG_UNMATCHED_AS_NULL em preg_match
✅ Funções:

✅ Constantes:

7.3 ✅ Atribuições de referência na lista ( ) /array destructuring => [& $ a, [$ b, & $ c]]=$ d exceto dentro de foreach ( # 4376 )
✅ Sintaxe flexível Heredoc e Nowdoc
✅ Comandos à direita em chamadas de função
✅ conjunto (bruto) de cookie aceita o argumento $ option
✅ Funções:

Exceções:

7,4 ✅ Propriedades digitadas
✅ Funções de seta
✅ Operador de atribuição de coalescência nula => ??=
✅ Descompactando dentro de arrays => $ nums=[3, 4]; $ merged=[1, 2,... $ nums, 5];
✅ Separador literal numérico => 1_000_000
✅ strip_tags () com matriz de nomes de tag => strip_tags ($ str, ['a','p'])
✅ Tipos de retorno covariante e tipos de parâmetro contravariantes
✅ Funções:

8.0 ✅ Tipos de união
✅ misto pseudo tipo
✅ tipo de retorno static
✅ :: class constante mágica em objetos
✅ expressões match
✅ catch exceções apenas por tipo
✅ Operador seguro para nulo
✅ Promoção da propriedade do construtor de classe
✅ Vírgulas finais em listas de parâmetros e listas de uso de encerramento
✅ Interfaces:

  • Stringable

✅ Aulas:

  • ValueError
  • UnhandledMatchError

✅ Constantes:

  • FILTER_VALIDATE_BOOL

✅ Funções:

Realizando a transpilação

A configuração do Rector para converter o código do PHP 8.0 até o PHP 7.1 é este :

 função estática de retorno (ContainerConfigurator $ containerConfigurator): void { //obter parâmetros $ parameters=$ containerConfigurator-> parameters (); //aqui podemos definir quais conjuntos de regras serão aplicadas $ parâmetros-> definir (Opção:: SETS, [ DowngradeSetList:: PHP_80, DowngradeSetList:: PHP_74, DowngradeSetList:: PHP_73, DowngradeSetList:: PHP_72, ]);
} 

Transpilar o código apenas para produção

Precisamos transpilar todo o código que compõe nosso projeto, o que inclui nosso código-fonte e todos os pacotes de terceiros dos quais ele depende.

Quanto aos pacotes, não precisamos transpilar todos eles; apenas aqueles que farão parte da entrega. Em outras palavras, apenas pacotes para PROD, não DEV.

Essa é uma boa notícia, porque:

  • A execução do Rector na base de código levará algum tempo, portanto, a remoção de todos os pacotes desnecessários (como PHPUnit, PHPStan, o próprio Rector e outros) diminuirá o tempo de execução
  • O processo provavelmente não será completamente tranquilo (alguns arquivos podem produzir erros e precisar de alguma solução personalizada). Assim, quanto menos arquivos para transpilar, menos esforço é necessário

Podemos descobrir quais são as dependências do PROD no Composer assim:

 informações do compositor-somente nome--no-dev 

O seguinte script Bash calcula a lista de todos os caminhos para fazer o downgrade (ou seja, o código-fonte do projeto e suas dependências PROD) e aplica Rector a eles:

 # Obtenha os caminhos para todas as dependências do PROD
# 1. `composer`: obtém a lista de caminhos, no formato"packageName packagePath"
# 2. `cut`: Remova os nomes dos pacotes
# 3. `sed`: Remova todos os espaços vazios
# 4. `tr`: Substituir novas linhas por espaços
caminhos="$ (informações do compositor--caminho--no-dev | cut-d''-f2-| sed's///g'| tr'\ n''')" # Execute o downgrade
# 1. Pasta de origem do projeto como"src"
# 2. Todos os caminhos de dependência
vendedor/bin/rector processo src $ caminhos--ansi 

A configuração deve excluir a execução do Rector em todos os casos de teste. Caso contrário, o Rector gerará um erro porque PHPUnit \ Framework \ TestCase está faltando no PROD. Dependências diferentes podem colocá-los em locais diferentes, que é como precisamos ajustar nossa configuração Rector. Para descobrir, podemos inspecionar o código-fonte ou executar o Rector e ver se/como ele falha.

Para meu plug-in, as pastas a serem ignoradas (incluindo aquelas do código-fonte do plug-in e suas dependências) são these :

 $ parameters-> set (Option:: SKIP, [ //Pular testes '*/tests/*', '*/teste/*', '*/Teste/*',
]); 

Cuidado com as inconsistências de dependência

Às vezes, as dependências podem fazer referência a alguma classe externa carregada para DEV. Quando Rector analisa a dependência, ele gera um erro porque o código referenciado não existe para PROD.

Por exemplo, classe EarlyExpirationHandler do componente Cache do Symfony implementa a interface MessageHandlerInterface do componente Messenger:

 classe EarlyExpirationHandler implementa MessageHandlerInterface
{ //...
} 

No entanto, a dependência do symfony/cache do symfony/messenger está em require-dev , não em require . Portanto, se nosso projeto depender de symfony/cache e o analisarmos com Rector, ele gerará um erro:

 [ERROR] Não foi possível processar o arquivo"vendor/symfony/cache/Messenger/EarlyExpirationHandler.php"devido a: "Erro de análise:"Classe Symfony \ Component \ Messenger \ Handler \ MessageHandlerInterface não encontrada.". Inclua seus arquivos em"$ parameters-> set (Option:: AUTOLOAD_PATHS, [...]);"em"rector.php"config. Consulte https://github.com/rectorphp/rector#configuration". 

Para resolver isso, primeiro verifique se isso é um bug no repositório da dependência. Neste caso, deve symfony/messenger ser adicionado à seção require do symfony/cache ? Se você não souber a resposta, pode perguntar por meio de um problema no repo.

Se for um bug, esperamos que seja corrigido e você pode esperar que a mudança aconteça (ou até mesmo contribuir diretamente). Caso contrário, você precisa considerar se seu projeto de produção usa a classe que produz o erro ou não.

Se usar, você pode carregar a dependência ausente na configuração do Rector por meio de sua configuração Option:: AUTOLOAD_PATHS :

 $ parameters-> set (Option:: AUTOLOAD_PATHS, [ __DIR__.'/vendor/symfony/messenger',
]); 

Se não o usar, você pode pular o arquivo diretamente para que o Rector não o processe:

 $ parameters-> set (Option:: SKIP, [ __DIR__.'/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php',
]); 

Otimizando o processo de transpilação

O script Bash que vimos anteriormente era simples porque está fazendo o downgrade de todas as dependências do PROD do PHP 8.0 para 7.1.

Agora, o que acontece se alguma dependência já estiver no PHP 7.1 ou anterior? Executar Rector em seu código não produzirá efeitos colaterais, mas é uma perda de tempo. Se houver muito código, o tempo desperdiçado se tornará significativo, fazendo-nos esperar mais para que o processo de CI seja concluído ao testar/mesclar um PR.

Sempre que isso acontecer, preferimos executar o Rector apenas nos pacotes que contêm o código que deve ser desatualizado, não em todos eles. Podemos descobrir quais pacotes são através do Composer. Como as dependências normalmente especificam qual versão do PHP elas requerem, podemos deduzir quais são os pacotes que requerem PHP 7.2 e superior desta forma:

 compositor porque não php"7.1. *"| grep-o"\ S * \/\ S *"

Por alguma razão, composer why-not não funciona com o sinalizador --no-dev , então precisamos instalar apenas dependências do PROD para obter essas informações:

 # Mude para a produção, para calcular os pacotes
composer install--no-dev--no-progress--ansi
# Obtenha a lista de pacotes que precisam de PHP 7.2 e superior
pacotes=$ (composer why-not php"7.1. *"| grep-o"\ S * \/\ S *")
# Mude para dev novamente
instalação do compositor--no-progress--ansi 

Com a lista de nomes de pacotes, calculamos seus caminhos assim:

 para pacote em $ packages
Faz # Obtenha o caminho do pacote do Composer # O formato é"caminho do pacote", então extraia tudo após a 1ª palavra com cut para obter o caminho path=$ (composer info $ package--path | cut-d''-f2-) caminhos="$ caminhos $ caminho"
feito 

Finalmente, executamos Rector em todos os caminhos (e na pasta de origem do projeto):

 vendor/bin/rector processo src $ path--ansi 

Cuidado com as regras encadeadas

Em algumas situações, podemos encontrar regras encadeadas: o próprio código produzido a partir da aplicação de uma regra de downgrade precisará ser modificado por outra regra de downgrade.

Podemos esperar que definir as regras em sua ordem de execução esperada lidará com regras encadeadas. Infelizmente, isso nem sempre funciona porque não controlamos como o PHP-Parser atravessa os nós .

Esta situação aconteceu no meu projeto : symfony/cache tem o arquivo vendor/symfony/cache/CacheItem.php com a função tag retornando ItemInterface :

 classe final CacheItem implementa ItemInterface
{ tag de função pública ($ tags): ItemInterface { //... return $ this; }
} 

A interface implementada ItemInterface , em vez disso, retorna self na função tag :

 interface ItemInterface estende CacheItemInterface
{ tag de função pública ($ tags): self;
} 

O conjunto de downgrade para PHP 7.4 contém as duas regras a seguir, definidas nesta ordem:

 $ services=$ containerConfigurator-> services ();
$ services-> set (DowngradeCovariantReturnTypeRector:: class);
$ services-> set (DowngradeSelfTypeDeclarationRector:: class); 

Ao fazer o downgrade da classe CacheItem , a função tag deve ser modificada duas vezes:

  1. DowngradeCovariantReturnTypeRector deve primeiro transformar o tipo de retorno de ItemInterface para self
  2. DowngradeSelfTypeDeclarationRector deve então remover o tipo de retorno self

Mas a segunda etapa não está acontecendo. Como consequência, após executar o downgrade, a função tag retorna self , que não funcionará para PHP 7.3 e abaixo .

A solução que encontrei para resolver esse problema envolve duas etapas:

  1. Descobrir sempre que tais problemas ocorrerem (será excepcional)
  2. Resolvendo o problema “manualmente” executando um segundo processo Rector, com sua própria configuração, especificamente para resolver o problema

Vamos ver como eles funcionam.

1. Descobrir sempre que tais problemas ocorrem

Normalmente, esperamos executar o Rector uma vez e fazer com que ele execute todas as modificações necessárias. Então, se executarmos o Rector uma segunda vez (na saída da primeira execução), não esperamos que nenhum código seja modificado. Se algum código for modificado na segunda passagem, isso significa que algo não foi bem na primeira passagem. Provavelmente, foi uma regra encadeada que não foi aplicada.

O Rector aceita o sinalizador --dry-run , o que significa que ele irá imprimir as modificações na tela, mas sem realmente aplicá-las no código. Convenientemente, executar Rector com este sinalizador retornará um erro sempre que houver uma modificação.

Então, podemos executar rector process--dry-run como segunda passagem em nosso CI . Sempre que o processo de CI falhar, a saída no console mostrará qual regra foi aplicada nesta segunda passagem, indicando qual é a regra encadeada que não foi aplicada na primeira passagem.

Executar a segunda passagem tem um benefício adicional: se o código PHP produzido apresentar bugs (o que pode acontecer ocasionalmente, como isso exemplo ), a segunda passagem de Rector falhará. Em outras palavras, estamos usando o Rector para testar a saída do próprio Rector.

2. Resolvendo o problema ‘manualmente’

Assim que descobrirmos que uma regra não foi executada em algum nó, devemos introduzir uma maneira de aplicá-la imediatamente após a primeira passagem do Rector. Poderíamos executar o mesmo processo Rector novamente, mas isso é ineficiente porque esse processo envolve dezenas de regras aplicadas em milhares de arquivos, levando vários minutos para ser concluído.

Mas o problema provavelmente envolverá uma única regra e uma única classe. Portanto, preferimos criar uma segunda configuração Rector , que exigirá apenas alguns segundos para ser executado:

 função estática de retorno (ContainerConfigurator $ containerConfigurator): void { $ parameters=$ containerConfigurator-> parameters (); $ parâmetros-> definir (Opção:: PATHS, [ __DIR__.'/vendor/symfony/cache/CacheItem.php', ]); $ services=$ containerConfigurator-> services (); $ services-> set (DowngradeSelfTypeDeclarationRector:: class);
}; 

Para suportar o processamento de mais de uma configuração Rector adicional, podemos passar uma lista de configs Rector para um script Bash :

 # Execute configurações adicionais do reitor
# Eles devem ser independentes, já incluindo todas as pastas src/para fazer o downgrade
if [-n"$ additional_rector_configs"]; então para rector_config em $ additional_rector_configs Faz vendor/bin/rector process--config=$ rector_config--ansi feito
fi 

Conclusão

Transpilar o código PHP é uma arte em si, que requer um pouco de esforço para ser configurado. Muito provavelmente, precisaremos ajustar a configuração do Rector para que funcione perfeitamente com nosso projeto, dadas as dependências de que ele precisa e quais recursos do PHP eles usam.

No entanto, transpilar o código é uma experiência incrivelmente poderosa que recomendo vivamente. In my own case, I’m able to use PHP 8.0 features for my publicly available WordPress plugin (something that is quite unheard of otherwise), allowing me to add strict typing on its codebase, thus lowering the likelihood of bugs and improving its documentation.

The post Tips for transpiling code from PHP 8.0 down to 7.1 appeared first on LogRocket Blog.

Source link