Em circunstâncias ideais, nós devemos usar o PHP 8.0 (a última versão a partir do momento em que escrevemos este artigo) para todos os nossos sites e atualizá-lo assim que uma nova versão for lançada. No entanto, os desenvolvedores freqüentemente precisarão trabalhar com versões anteriores do PHP, como ao criar um plugin público para WordPress ou trabalhar com código antigo que impede a atualização do ambiente do webserver.

Nessas situações, nós poderíamos perder a esperança de usar o último código do PHP. Mas há uma alternativa melhor: nós ainda podemos escrever nosso código fonte com o PHP 8.0 e transpilar para uma versão anterior do PHP – até mesmo para o PHP 7.1.

Neste guia, nós lhe ensinaremos tudo o que você precisa saber sobre a transpilação de código PHP.

O que é transpilação?

Transpilação ou “Transpiling em Inglês” converte o código fonte de uma linguagem de programação em um código fonte equivalente da mesma ou de uma linguagem de programação diferente.

Transpilação não é um conceito novo dentro do desenvolvimento web: desenvolvedores do lado do cliente provavelmente estarão familiarizados com o Babel, um transpilador de código JavaScript.

Babel converte o código JavaScript da moderna versão ECMAScript 2015+ em uma versão antiga e compatível com os navegadores inferior . Por exemplo, dada uma função arrow ES2015:

[2, 4, 6].map((n) => n * 2);

…Babel irá convertê-lo em sua versão ES5:

[2, 4, 6].map(function(n) {
  return n * 2;
});

O que é transpilação PHP?

O que é potencialmente novo dentro do desenvolvimento web é a possibilidade de transpilar o código do lado do servidor, em particular o PHP.

A transpilação do PHP funciona da mesma forma que a transpilação JavaScript: o código fonte de uma versão moderna do PHP é convertido em um código equivalente para uma versão PHP mais antiga.

Seguindo o mesmo exemplo de antes, uma função arrow do PHP 7.4:

$nums = array_map(fn($n) => $n * 2, [2, 4, 6]);

pode ser transpiladas para sua versão equivalente do PHP 7.3:

$nums = array_map(
  function ($n) {
    return $n * 2;
  },
  [2, 4, 6]
);

As funções arrows podem ser transpiladas porque são syntactic sugar, ou seja, uma nova sintaxe para produzir um comportamento existente. Este é o fruto de baixa suspensão.

Entretanto, há também novas funcionalidades que criam um novo comportamento e, como tal, não haverá código equivalente para as versões anteriores do PHP. Esse é o caso dos tipos de união, introduzidos no PHP 8.0:

function someFunction(float|int $param): string|float|int|null
{
  // ...
}

Nessas situações, a transpilação ainda pode ser feita desde que o novo recurso seja necessário para o desenvolvimento, mas não para a produção. Então, nós podemos simplesmente remover o recurso completamente do código transpilado sem conseqüências sérias.

Um desses exemplos são os tipos de união. Este recurso é usado para verificar se não há uma incompatibilidade entre o tipo de entrada e o valor fornecido, o que ajuda a prevenir bugs. Se houver um conflito com os tipos, haverá um erro já em desenvolvimento e nós devemos pegá-lo e corrigi-lo antes que o código chegue à produção.

Portanto, podemos nos dar ao luxo de remover o recurso do código para produção:

function someFunction($param)
{
  // ...
}

Se o erro ainda acontecer na produção, a mensagem de erro lançada será menos precisa do que se tivéssemos tipos de união. No entanto, esta desvantagem potencial é compensada pela capacidade de usar tipos de união em primeiro lugar.

Vantagens da transpilação do Código PHP

A transpilação permite codificar um aplicativo usando a última versão do PHP e produzir uma versão que também funciona em ambientes que executam versões mais antigas do PHP.

Isto pode ser particularmente útil para desenvolvedores que criam produtos antigo para o content management systems (CMS). O WordPress, por exemplo, ainda suporta oficialmente o PHP 5.6 (apesar de recomendar o PHP 7.4+). A porcentagem de sites WordPress rodando versões PHP 5.6 a 7.2 – que todos estão no Fim de vida “End-of-Life (EOL)”, significando que eles não estão mais recebendo atualizações de segurança – é de 34,8%, e aqueles rodando em qualquer versão PHP que não seja a 8.0 é de 99,5%:

Estatísticas de uso do WordPress por versão. Fonte da imagem
Estatísticas de uso do WordPress por versão. Fonte da imagem: WordPress

Consequentemente, os temas e plugins do WordPress direcionados a uma audiência global provavelmente serão codificados com uma versão antiga do PHP para aumentar seu possível alcance. Graças à transpilação, estes poderiam ser codificados usando PHP 8.0, e ainda ser lançados para uma versão mais antiga do PHP, visando assim o maior número possível de usuários.

De fato, qualquer aplicativo que precise suportar qualquer versão PHP que não seja a mais recente (mesmo dentro da faixa das versões PHP atualmente suportadas) pode se beneficiar.

Este é o caso do Drupal, que requer o PHP 7.3. Graças à transpilação, os desenvolvedores podem criar módulos Drupal disponíveis publicamente usando o PHP 8.0, e liberá-los com o PHP 7.3.

Outro exemplo é quando se cria código personalizado para clientes que não podem executar o PHP 8.0 em seus ambientes devido a uma razão ou outra. No entanto, graças à transpilação, os desenvolvedores ainda podem codificar seus produtos usando PHP 8.0 e executá-los nesses ambientes mais antigos.

Quando Transpilar o PHP

O código PHP sempre pode ser transpilado a menos que contenha algum recurso PHP que não tenha equivalente na versão anterior do PHP.

Esse é possivelmente o caso dos atributos, introduzidos no PHP 8.0:

#[SomeAttr]
function someFunc() {}

#[AnotherAttr]
class SomeClass {}

No exemplo anterior, usando funções arrow, o código podia ser transpilado porque as funções arrow são syntactic sugar. Atributos, em contraste, criam um comportamento completamente novo. Este comportamento também poderia ser reproduzido com o PHP 7.4 ou abaixo, mas apenas codificando manualmente, ou seja, não automaticamente baseado em uma ferramenta ou processo (IA poderia fornecer uma solução, mas nós ainda não estamos lá).

Atributos destinados ao uso em desenvolvimento, tais como #[Deprecated] , podem ser removidos da mesma forma que os tipos de união são removidos. Mas os atributos que modificam o comportamento do aplicativo na produção não podem ser removidos, e também não podem ser transpiladas diretamente.

A partir de hoje, nenhum transpilador “transpiler em Inglês” pode pegar código com atributos no PHP 8.0 e produzir automaticamente seu código equivalente no PHP 7.4. Consequentemente, se o seu código PHP precisa usar atributos, então a sua transpilação será difícil ou inviável.

Funcionalidades PHP que podem ser transpiladas

Estas são as funcionalidades do PHP 7.1 ou acima que podem ser transpiladas atualmente. Se o seu código só usa estes recursos, você pode ter a certeza de que seu aplicativo transposta irá funcionar. Caso contrário, você precisará avaliar se o código transpilado irá produzir falhas.

Versão PHP Funcionalidades
7.1 Tudo
7.2 tipoobject
Parameter type widening
PREG_UNMATCHED_AS_NULL flag em preg_match
7.3 Atribuições de referência em list() / desestruturação de array (Exceto dentro do foreach#4376)
Sintaxe flexível de Heredoc e Nowdoc
Chamadas de funções com vírgulas de rastreamento
set(raw)cookie aceita argumento $opção
7.4 Propriedades datilografadas
Funções arrow
Operador de coalescência nula
Desembalando dentro de arrays
Separador numérico literal
strip_tags() com array de nomes de tags
tipos de retorno covariantes e param tipos contravariantes
8.0 Tipo União
Pseudo-tiposmixed
Tipo retorno static
Constante mágica nos objetos ::class
expressões match
Exceções somente por tipo catch
Operador seguro para todos
Promoção de propriedade de construtores de classe
Vírgulas de rastreamento em listas de parâmetros e listas use de fechamento

Transpiladores PHP

Atualmente, existe uma ferramenta para a transpilação de código PHP: Rector.

Rector é uma ferramenta reconstrutora de PHP, que converte código PHP baseado em regras programáveis. Nós inserimos o código fonte e o conjunto de regras a serem executadas, e Rector transformará o código.

O Rector é operado via linha de comando, instalado no projeto via Composer. Quando executado, o Rector irá emitir um “diff” (adições em verde, remoções em vermelho) do código antes e depois da conversão:

Saída "diff" do Rector
Saída “diff” do Rector

Qual versão do PHP que pode ser transpilada

Para transpilar o código através das versões PHP, as regras correspondentes devem ser criadas.

Hoje, a biblioteca do Rector inclui a maioria das regras para a transpilação de código dentro da faixa do PHP 8.0 a 7.1. Portanto, nós podemos transpilar de forma confiável nosso código PHP até a versão 7.1.

Há também regras para a transpilação do PHP 7.1 para 7.0 e de 7.0 para 5.6, mas estas não são exaustivas. O trabalho está em andamento para completá-las, então nós podemos eventualmente transpilar o código PHP para a versão 5.6.

Transpilação vs. Backporting

O backporting é similar ao transpilação “transpiling em Inglês”, mas mais simples. O código de backporting não necessariamente depende de novas funcionalidades de uma linguagem. Ao invés disso, a mesma funcionalidade pode ser fornecida para uma versão antiga da linguagem simplesmente copiando/colando/adaptando o código correspondente da nova versão da linguagem.

Por exemplo, a função str_contains foi introduzida no PHP 8.0. A mesma função para o PHP 7.4 e abaixo pode ser facilmente implementada desta forma:

if (!defined('PHP_VERSION_ID') || (defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80000)) {
  if (!function_exists('str_contains')) {
    /**
     * Checks if a string contains another
     *
     * @param string $haystack The string to search in
     * @param string $needle The string to search
     * @return boolean Returns TRUE if the needle was found in haystack, FALSE otherwise.
     */
    function str_contains(string $haystack, string $needle): bool
    {
      return strpos($haystack, $needle) !== false;
    }
  }
}

Porque backporting é mais simples do que transpilar, nós devemos optar por esta solução sempre que o backporting fizer o trabalho.

Com relação à faixa entre PHP 8.0 e 7.1, nós podemos usar as bibliotecas polifill da Symfony:

Estas bibliotecas suportam as seguintes funções, classes, constantes e interfaces:

Versão PHP Características
7.2 Funções:

Constantes:

7.3 Funções:

Exceções:

7.4 Funções:
8.0 Interfaces:
  • Stringable

Classes:

  • ValueError
  • UnhandledMatchError

Constantes:

  • FILTER_VALIDATE_BOOL

Funções:

Exemplos de PHP transpilado

Vamos inspecionar alguns exemplos de código PHP transpilado, e alguns pacotes que estão sendo totalmente transpilados.

Código PHP

A expressão  match foi introduzida no PHP 8.0. Este código fonte:

function getFieldValue(string $fieldName): ?string
{
  return match($fieldName) {
    'foo' => 'foofoo',
    'bar' => 'barbar',
    'baz' => 'bazbaz',
    default => null,
  };
}

…será transpilada para sua versão equivalente do PHP 7.4, usando o operador switch:

function getFieldValue(string $fieldName): ?string
{
  switch ($fieldName) {
    case 'foo':
      return 'foofoo';
    case 'bar':
      return 'barbar';
    case 'baz':
      return 'bazbaz';
    default:
      return null;
  }
}

O operador nullsafe também foi introduzido no PHP 8.0:

public function getValue(TypeResolverInterface $typeResolver): ?string
{
  return $this->getResolver($typeResolver)?->getValue();
}

O código transpilado precisa primeiro atribuir o valor da operação a uma nova variável, para evitar executar a operação duas vezes:

public function getValue(TypeResolverInterface $typeResolver): ?string
{
  return ($val = $this->getResolver($typeResolver)) ? $val->getValue() : null;
}

O recurso de promoção de propriedade do construtor, também introduzido no PHP 8.0, permite que os desenvolvedores escrevam menos código:

class QueryResolver
{
  function __construct(protected QueryFormatter $queryFormatter)
  {
  }
}

Ao transpilar para o PHP 7.4, o código completo é produzido:

 class QueryResolver
 {
  protected QueryFormatter $queryFormatter;

  function __construct(QueryFormatter $queryFormatter)
  {
    $this->queryFormatter = $queryFormatter;
  }
}

O código transpilado acima contém propriedades digitadas, que foram introduzidas no PHP 7.4. A transpilação desse código para o PHP 7.3 os substitui por blocos de documentos:

 class QueryResolver
 {
  /**
   * @var QueryFormatter
   */
  protected $queryFormatter;

  function __construct(QueryFormatter $queryFormatter)
  {
    $this->queryFormatter = $queryFormatter;
  }
}

Pacotes PHP

As seguintes bibliotecas estão sendo transpiladas para a produção:

Biblioteca/descrição Código/notas
Rector
Ferramenta reconstrutora de PHP que torna a transpilação possível
Código fonte
Código transpilado
Notas
Easy Coding Standards
Ferramenta para que o código PHP adere a um conjunto de regras
Código fonte
Código transpilado
Notas
API GraphQL para WordPress
Plugin fornecendo um servidor GraphQL para WordPress
Código fonte
Código transpilado
Notas

Prós e Contras da transpilação do PHP

O benefício de transpilar o PHP já foi descrito: ele permite que o código fonte utilize o PHP 8.0 (ou seja, a última versão do PHP), que será transformado em uma versão inferior para que o PHP seja executado em um aplicativo ou ambiente inferior.

Isto nos permite efetivamente nos tornarmos melhores desenvolvedores, produzindo código com maior qualidade. Isto porque nosso código fonte pode usar os tipos de união (Union Type) do PHP 8.0, as propriedades digitadas do PHP 7.4, e os diferentes tipos e pseudo-tipos adicionados a cada nova versão do PHP (mixed a partir do PHP 8.0, object do PHP 7.2), entre outras funcionalidades modernas do PHP.

Usando estas funcionalidades, podemos capturar melhor os bugs durante o desenvolvimento e escrever código que é mais fácil de ler.

Agora, vamos dar uma olhada nos inconvenientes.

Deve ser codificado e atualizado

O rector pode transportar o código automaticamente, mas o processo provavelmente exigirá alguma entrada manual para que ele funcione com nossa configuração específica.

Bibliotecas de terceiros também precisam ser transpiladas

Isto se torna um problema sempre que transpilados produzem erros, já que devemos então mergulhar no código fonte deles para descobrir a razão possível. Se o problema puder ser resolvido e o projeto for de código aberto, nós precisaremos enviar um pedido pull. Se a biblioteca não for de código aberto, nós podemos chegar a um roadblock.

O Rector não nos informa quando o código não pode ser transpilado

Se o código fonte contém atributos PHP 8.0 ou qualquer outra funcionalidade que não pode ser transpilada, nós não podemos prosseguir. Entretanto, o Rector não irá verificar esta condição, então precisamos fazer isso manualmente. Isto pode não ser um grande problema em relação ao nosso próprio código fonte, uma vez que já estamos familiarizados com ele, mas pode se tornar um obstáculo em relação às dependências de terceiros.

A informação de depuração utiliza o código transpilado, e não o código fonte

Quando o aplicativo produz uma mensagem de erro com um rastreamento de pilha na produção, o número da linha irá apontar para o código transpilado. Nós precisamos converter de volta do código transpilado para o código original para encontrar o número de linha correspondente no código fonte.

O código transpilado também deve ser prefixado

Nosso projeto transpilado e algumas outras bibliotecas também instaladas no ambiente de produção poderiam usar a mesma dependência de terceiros. Esta dependência de terceiros será transpilada para o nosso projeto e manterá seu código fonte original para a outra biblioteca. Portanto, a versão transpilada deve ser prefixada via PHP-Scoper, Strauss, ou alguma outra ferramenta para evitar possíveis conflitos.

A transpilação deve ser feita durante a integração contínua (CI – Continuous Integration)

Como o código transpilado irá naturalmente substituir o código fonte, não devemos executar o processo de transpilação em nossos computadores de desenvolvimento, ou corremos o risco de criar efeitos colaterais. Executar o processo durante a execução de um CI é mais adequado (mais sobre isso abaixo).

Como transpilar o PHP

Primeiro, nós precisamos instalar o Rector em nosso projeto de desenvolvimento:

composer require rector/rector --dev

Então criamos um arquivo de configuração rector.php no diretório raiz do projeto contendo os conjuntos de regras necessários. Para baixar o código do PHP 8.0 para 7.1, nós usamos esta configuração:

use Rector\Set\ValueObject\DowngradeSetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $containerConfigurator->import(DowngradeSetList::PHP_80);
    $containerConfigurator->import(DowngradeSetList::PHP_74);
    $containerConfigurator->import(DowngradeSetList::PHP_73);
    $containerConfigurator->import(DowngradeSetList::PHP_72);
};

Para garantir que o processo seja executado como esperado, podemos executar o comando process do Rector em modo seco, passando o(s) local(is) a processar (neste caso, todos os arquivos sob a pasta src/):

vendor/bin/rector process src --dry-run

Para realizar a transpilação, nós executamos o comando process do Rector, que modificará os arquivos dentro de sua localização existente:

vendor/bin/retor process src

Por favor note: se rodarmos o rector process em nossos computadores de desenvolvimento, o código fonte será convertido no lugar, sob src/. Entretanto, nós queremos produzir o código convertido em um local diferente para não sobrepor o código fonte quando o código for baixado. Por esta razão, rodar o processo é mais adequado durante a integração contínua.

Otimizando o processo de transpilação

Para gerar um deliverable transpiled para produção, apenas o código para produção deve ser convertido; o código necessário apenas para desenvolvimento pode ser pulado. Isso significa que podemos evitar a transpilação de todos os testes (tanto para nosso projeto quanto para suas dependências) e todas as dependências para desenvolvimento.

Com relação aos testes, nós já saberemos onde estão localizados os testes para o nosso projeto – por exemplo, sob a pasta tests/. Também devemos saber onde estão os tests das dependências – por exemplo, sob suas subpastas tests/, test/ e Test/ (para diferentes bibliotecas). Então, nós dizemos ao Rector para pular o processamento dessas pastas:

return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::SKIP, [
    // Skip tests
    '*/tests/*',
    '*/test/*',
    '*/Test/*',
  ]);
};

Com relação às dependências, Composer sabe quais são para desenvolvimento (aqueles sob entrada require-dev no composer.json) e quais são para produção (aqueles sob entrada require).

Para recuperar do Composer os caminhos de todas as dependências para a produção, nós executamos:
Este comando produzirá uma lista de dependências com seu nome e caminho, como esta:

brain/cortex                     /Users/leo/GitHub/leoloso/PoP/vendor/brain/cortex
composer/installers              /Users/leo/GitHub/leoloso/PoP/vendor/composer/installers
composer/semver                  /Users/leo/GitHub/leoloso/PoP/vendor/composer/semver
guzzlehttp/guzzle                /Users/leo/GitHub/leoloso/PoP/vendor/guzzlehttp/guzzle
league/pipeline                  /Users/leo/GitHub/leoloso/PoP/vendor/league/pipeline

Nós podemos extrair todos os caminhos e alimentá-los com o comando do Rector, que então processará a pasta src/ do nosso projeto mais aquelas pastas contendo todas as dependências para produção:

$ paths="$(composer info --path --no-dev | cut -d' ' -f2- | sed 's/ //g' | tr '\n' ' ')"
$ vendor/bin/rector process src $paths

Uma melhoria adicional pode impedir que o Rector processe aquelas dependências que já utilizam a versão alvo do PHP. Se uma biblioteca foi codificada com o PHP 7.1 (ou qualquer versão abaixo), então não há necessidade de transpilar para o PHP 7.1.

Para conseguir isso, nós podemos obter a lista de bibliotecas que requerem PHP 7.2 ou superior e processar apenas aquelas. Nós obteremos os nomes de todas essas bibliotecas através do comando why-not do Composer, como este:

composer why-not php "7.1.*" | grep -o "\S*\/\S*"

Como este comando não funciona com a flag  --no-dev, para incluir apenas dependências para produção, precisamos primeiro remover as dependências para desenvolvimento e regenerar o autoloader, executar o comando, e então adicioná-las novamente:

$ composer install --no-dev
$ packages=$(composer why-not php "7.1.*" | grep -o "\S*\/\S*")
$ composer install

O comando info --path do Composer recupera o caminho para um pacote, com este formato:

# Executing this command
$ composer info psr/cache --path   
# Produces this response:
psr/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cache

Nós executamos este comando para todos os itens da nossa lista para obter todos os caminhos a serem transpilados:

for package in $packages
do
  path=$(composer info $package --path | cut -d' ' -f2-)
  paths="$paths $path"
done

Finalmente, nós fornecemos esta lista para o Rector (mais a pasta src/ do projeto):

vendor/bin/rector process src $paths

Armadilhas a serem evitadas durante a transpilação do código

A transpilação do código pode ser considerado uma arte, muitas vezes exigindo ajustes específicos para o projeto. Vamos ver alguns problemas que podemos ter.

Regra da cadeia nem sempre é processada

Uma regra da cadeia é quando uma regra precisa converter o código produzido por uma regra anterior.

Por exemplo, a biblioteca symfony/cache contém este código:

final class CacheItem implements ItemInterface
{
  public function tag($tags): ItemInterface
  {
    // ...
    return $this;
  }
}

Ao transpilar do PHP 7.4 para 7.3, a tag de função deve sofrer duas modificações:

O resultado final deve ser este aqui:

final class CacheItem implements ItemInterface
{
  public function tag($tags)
  {
    // ...
    return $this;
  }
}

No entanto, o Rector só produz o estágio intermediário:

final class CacheItem implements ItemInterface
{
  public function tag($tags): self
  {
    // ...
    return $this;
  }
}

A questão é que o Rector nem sempre pode controlar a ordem na qual as regras são aplicadas.

A solução é identificar quais regra da cadeia foram deixadas sem processamento e executar uma nova corrida do Rector para aplicá-las.

Para identificar a regra de cadeia, nós executamos o Rector duas vezes no código fonte:

$ vendor/bin/rector process src
$ vendor/bin/rector process src --dry-run

A primeira vez, nós executamos o Rector, como esperado, para executar a transpilação. Na segunda vez, nós usamos a flag --dry-run para descobrir se ainda há mudanças a serem feitas. Se houver, o comando sairá com um código de erro, e a saída “diff” indicará qual regra(s) ainda pode(m) ser aplicada(s). Isso significaria que a primeira execução não foi completa, com alguma regra de cadeia não sendo processada.

Rector de Corrida com bandeira --dry-run
Rector de Corrida com flag –dry-run

Uma vez que tenhamos identificado a regra da cadeia (ou chained rule) não aplicada, podemos então criar outro arquivo de configuração do Rector – por exemplo, o rector-chained-rule.php executará a regra que falta. Ao invés de processar um conjunto completo de regras para todos os arquivos sob src/, desta vez, podemos executar a regra específica que está faltando no arquivo específico onde ela precisa ser aplicada:

// rector-chained-rule.php
use Rector\Core\Configuration\Option;
use Rector\DowngradePhp74\Rector\ClassMethod\DowngradeSelfTypeDeclarationRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
  $services = $containerConfigurator->services();
  $services->set(DowngradeSelfTypeDeclarationRector::class);

  $parameters = $containerConfigurator->parameters();
  $parameters->set(Option::PATHS, [
    __DIR__ . '/vendor/symfony/cache/CacheItem.php',
  ]);
};

Finalmente, nós dizemos ao Rector em seu segundo passe para usar o novo arquivo de configuração via input --config:

# First pass with all modifications
$ vendor/bin/rector process src

# Second pass to fix a specific problem
$ vendor/bin/rector process --config=rector-chained-rule.php

Dependências do Composer podem ser inconsistentes

As bibliotecas poderiam declarar uma dependência para desenvolvimento (ou seja, sob require-dev no composer.json), ainda assim, referenciar algum código delas para produção (como em alguns arquivos sob src/, não tests/).

Normalmente, isso não é um problema porque esse código pode não ser carregado na produção, então nunca haverá um erro no aplicativo. Entretanto, quando o Rector processa o código fonte e suas dependências, ele valida que todo o código referenciado pode ser carregado. Rector irá lançar um erro se algum arquivo fizer referência a algum trecho de código de uma biblioteca não instalada (porque foi declarado como sendo necessário apenas para desenvolvimento).

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

class EarlyExpirationHandler implements MessageHandlerInterface
{
    //...
}

No entanto, symfony/cache declara symfony/messenger como uma dependência para o desenvolvimento. Então, ao executar o Rector em um projeto que depende de symfony/cache, ele irá lançar um erro:

[ERROR] Could not process "vendor/symfony/cache/Messenger/EarlyExpirationHandler.php" file, due to:             
  "Analyze error: "Class Symfony\Component\Messenger\Handler\MessageHandlerInterface not found.". Include your files in "$parameters->set(Option::AUTOLOAD_PATHS, [...]);" in "rector.php" config.
  See https://github.com/rectorphp/rector#configuration".   

Há três soluções para esta questão:

  1. Na configuração do Rector, pular o processamento do arquivo que faz referência àquele pedaço de código:
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::SKIP, [
    __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php',
  ]);
};
  1. Faça o download da biblioteca que falta e adicione seu caminho para ser carregado automaticamente pelo Rector:
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/vendor/symfony/messenger',
  ]);
};
  1. Faça seu projeto depender da biblioteca que falta para a produção:
composer require symfony/messenger

Transpilação e integração contínua (CI)

Como mencionado anteriormente, em nossos computadores de desenvolvimento nós devemos usar a flag --dry-run ao rodar o Rector, ou então, o código fonte será substituído pelo código transpilado. Por esta razão, é mais adequado executar o processo de transpilação real durante a integração contínua (CI – Continuous Integration), onde podemos rodar os runners temporários para executar o processo.

Um momento ideal para executar o processo de transpilação é quando se gera a liberação para o nosso projeto. Por exemplo, o código abaixo é um workflow para o GitHub Actions, que cria o lançamento de um plugin WordPress:

name: Generate Installable Plugin and Upload as Release Asset
on:
  release:
    types: [published]
jobs:
  build:
    name: Build, Downgrade and Upload Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Downgrade code for production (to PHP 7.1)
        run: |
          composer install
          vendor/bin/rector process
          sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php
      - name: Build project for production
        run: |
          composer install --no-dev --optimize-autoloader
          mkdir build
      - name: Create artifact
        uses: montudor/[email protected]
        with:
          args: zip -X -r build/graphql-api.zip . -x *.git* node_modules/\* .* "*/\.*" CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md rector.php *.dist composer.* dev-helpers** build**
      - name: Upload artifact
        uses: actions/upload-artifact@v2
        with:
            name: graphql-api
            path: build/graphql-api.zip
      - name: Upload to release
        uses: JasonEtco/upload-to-release@master
        with:
          args: build/graphql-api.zip application/zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Este fluxo de trabalho contém um procedimento padrão para liberar um plugin WordPress via GitHub Actions. A nova adição, para transpor o código do plugin do PHP 7.4 para o 7.1, acontece nesta etapa:

      - name: Downgrade code for production (to PHP 7.1)
        run: |
          vendor/bin/rector process
          sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php

Em conjunto, este fluxo de trabalho agora executa os seguintes passos:

  1. Verifica o código fonte para um plugin WordPress a partir do seu repositório, escrito com PHP 7.4
  2. Instala suas dependências do Composer
  3. Transpila seu código do PHP 7.4 para o 7.1
  4. Modifica a entrada “Requires PHP” no cabeçalho do arquivo principal do plugin do "7.4" para "7.1"
  5. Remove as dependências necessárias para o desenvolvimento
  6. Cria o arquivo .zip do plugin, excluindo todos os arquivos desnecessários
  7. Carrega o arquivo .zip como um ativo de lançamento (e, além disso, como um artefato para a Ação GitHub)

Testando o código transpilado

Uma vez que o código foi transpilado para o PHP 7.1, como sabemos que ele funciona bem? Ou, em outras palavras, como sabemos que ele foi completamente convertido, e que nenhum remanescente de versões superiores do código PHP foi deixado para trás?

Semelhante à transpilação do código, nós podemos implementar a solução dentro de um processo CI. A idéia é configurar o ambiente do runner com PHP 7.1 e executar um linter sobre o código transpilado. Se qualquer pedaço de código não for compatível com o PHP 7.1 (como uma propriedade digitada do PHP 7.4 que não foi convertida), então o linter irá lançar um erro.

Um linter para PHP que funciona bem é o PHP Parallel Lint. Nós podemos instalar esta biblioteca como uma dependência para desenvolvimento em nosso projeto, ou fazer com que o processo de CI a instale como um projeto autônomo do Composer:

composer create-project php-parallel-lint/php-parallel-lint

Sempre que o código contiver PHP 7.2 e superior, o PHP Parallel Lint irá lançar um erro como este:

Run php-parallel-lint/parallel-lint layers/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php
PHP 7.1.33 | 10 parallel jobs
............................................................   60/2870 (2 %)
............................................................  120/2870 (4 %)
...
............................................................  660/2870 (22 %)
.............X..............................................  720/2870 (25 %)
............................................................  780/2870 (27 %)
...
............................................................ 2820/2870 (98 %)
..................................................           2870/2870 (100 %)


Checked 2870 files in 15.4 seconds
Syntax error found in 1 file

------------------------------------------------------------
Parse error: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php:55
    53|     '0.8.0',
    54|     \__('GraphQL API for WordPress', 'graphql-api'),
  > 55| ))) {
    56|     $plugin->setup();
    57| }
Unexpected ')' in layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php on line 55
Error: Process completed with exit code 1.

Vamos adicionar o linter ao fluxo de trabalho do nosso CI. Os passos a serem executados para transpor o código do PHP 8.0 para 7.1 e testá-lo são:

  1. Confira o código fonte
  2. Tenha o ambiente rodando PHP 8.0, para que o Rector possa interpretar o código fonte
  3. Transpile o código para o PHP 7.1
  4. Instale a ferramenta linter do PHP
  5. Mude a versão do ambiente PHP para 7.1
  6. Executar o linter no código transpilado

Este fluxo de trabalho da GitHub Action faz o trabalho:

name: Downgrade PHP tests
jobs:
  main:
    name: Downgrade code to PHP 7.1 via Rector, and execute tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set-up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.0
          coverage: none

      - name: Local packages - Downgrade PHP code via Rector
        run: |
          composer install
          vendor/bin/rector process

      # Prepare for testing on PHP 7.1
      - name: Install PHP Parallel Lint
        run: composer create-project php-parallel-lint/php-parallel-lint --ansi

      - name: Switch to PHP 7.1
        uses: shivammathur/setup-php@v2
        with:
          php-version: 7.1
          coverage: none

      # Lint the transpiled code
      - name: Run PHP Parallel Lint on PHP 7.1
        run: php-parallel-lint/parallel-lint src/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php

Por favor note que vários arquivos bootstrap80.php das bibliotecas polifill da Symfony (que não precisam ser transpilados) devem ser excluídos do linter. Estes arquivos contêm PHP 8.0, então o linter lançaria erros ao processá-los. Entretanto, excluir estes arquivos é seguro, uma vez que eles serão carregados na produção somente quando rodando o PHP 8.0 ou superior:

if (\PHP_VERSION_ID >= 80000) {
  return require __DIR__.'/bootstrap80.php';
}

Resumo

Este artigo nos ensinou como transpilar nosso código PHP, permitindo-nos usar PHP 8.0 no código fonte e criar uma versão que funcione no PHP 7.1. A transpilação é feita através do Rector, uma ferramenta reconstrutora do PHP.

Transpilar nosso código nos torna melhores desenvolvedores, uma vez que podemos capturar melhor os bugs no desenvolvimento e produzir código que é naturalmente mais fácil de ler e entender.

A transpilação também nos permite desacoplar nosso código com requisitos específicos de PHP do CMS. Agora nós podemos fazer isso se quisermos usar a última versão do PHP para criar um plugin WordPress ou módulo Drupal disponível publicamente sem restringir severamente nossa base de usuários.

Você ainda tem alguma pergunta sobre a transpilação do PHP? Nos informe na seção de comentários!

Leonardo Losoviz

Leo writes about innovative web development trends, mostly concerning PHP, WordPress and GraphQL. You can find him at leoloso.com and twitter.com/losoviz.