En circunstancias ideales, deberíamos utilizar PHP 8.0 (la última versión en el momento de escribir este artículo) para todos nuestros sitios y actualizarlo tan pronto como se publique una nueva versión. Sin embargo, los desarrolladores a menudo necesitarán trabajar con versiones anteriores de PHP, como cuando se crea un plugin público para WordPress o se trabaja con código heredado que impide actualizar el entorno del servidor web.

En estas situaciones, podríamos renunciar a la esperanza de utilizar el último código PHP. Pero hay una alternativa mejor: podemos seguir escribiendo nuestro código fuente con PHP 8.0 y transpilarlo a una versión anterior de PHP – incluso a PHP 7.1.

En esta guía, te enseñaremos todo lo que necesita saber sobre la transpilación de código PHP.

¿Qué es la transpilación?

La transpilación convierte el código fuente de un lenguaje de programación en un código fuente equivalente del mismo o de otro lenguaje de programación.

La transpilación no es un concepto nuevo en el desarrollo web: los desarrolladores del lado del cliente probablemente estén familiarizados con Babel, un transpilador de código JavaScript.

Babel convierte el código JavaScript de la versión moderna de ECMAScript 2015+ en una versión heredada compatible con los navegadores más antiguos. Por ejemplo, dada una función de flecha de ES2015:

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

…Babel lo convertirá en su versión ES5:

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

¿Qué es la transpilación de PHP?

Lo que es potencialmente nuevo dentro del desarrollo web es la posibilidad de transpilar el código del lado del servidor, en particular PHP.

La transpilación de PHP funciona de la misma manera que la transpilación de JavaScript: el código fuente de una versión moderna de PHP se convierte en un código equivalente para una versión antigua de PHP.

Siguiendo el mismo ejemplo que antes, una función de flecha de PHP 7.4:

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

… se puede transpilar a su versión equivalente de PHP 7.3:

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

Las funciones flecha pueden transpilarse porque son azúcar sintáctico, es decir, una nueva sintaxis para producir un comportamiento existente. Esta es la fruta más fácil de conseguir.

Sin embargo, también hay nuevas características que crean un nuevo comportamiento, y como tal, no habrá código equivalente para las versiones anteriores de PHP. Ese es el caso de los tipos de unión, introducidos en PHP 8.0:

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

En estas situaciones, se puede transpilar siempre que la nueva función sea necesaria para el desarrollo pero no para la producción. En ese caso, podemos simplemente eliminar la función del código transpilado sin consecuencias graves.

Un ejemplo de ello son los tipos de unión. Esta característica se utiliza para comprobar que no hay un desajuste entre el tipo de entrada y su valor proporcionado, lo que ayuda a prevenir errores. Si hay un conflicto con los tipos, habrá un error ya en el desarrollo, y debemos atraparlo y arreglarlo antes de que el código llegue a producción.

Por lo tanto, podemos permitirnos eliminar la función del código para la producción:

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

Si el error sigue ocurriendo en producción, el mensaje de error lanzado será menos preciso que si tuviéramos tipos de unión. Sin embargo, esta posible desventaja se ve compensada por el hecho de poder utilizar tipos de unión en primer lugar.

Ventajas de transpilar el código PHP

La transpilación permite codificar una aplicación utilizando la última versión de PHP y producir una versión que también funciona en entornos que ejecutan versiones anteriores de PHP.

Esto puede ser especialmente útil para los desarrolladores que crean productos para sistemas de gestión de contenidos (CMS) heredados. WordPress, por ejemplo, todavía soporta oficialmente PHP 5.6 (aunque recomienda PHP 7.4+). El porcentaje de sitios de WordPress que ejecutan las versiones de PHP 5.6 a 7.2 -que son todas End-of-Life (EOL), lo que significa que ya no reciben actualizaciones de seguridad- se sitúa en un considerable 34,8%, y los que se ejecutan en cualquier versión de PHP distinta de la 8.0 se sitúan en un enorme 99,5%:

Estadísticas de uso de WordPress por versión. Fuente de la imagen: WordPress
Estadísticas de uso de WordPress por versión. Fuente de la imagen: WordPress

En consecuencia, los temas y plugins de WordPress dirigidos a un público global probablemente se codificarán con una versión antigua de PHP para aumentar su posible alcance. Gracias a la transpilación, estos podrían codificarse con PHP 8.0 y seguir siendo lanzados para una versión de PHP más antigua, dirigiéndose así al mayor número de usuarios posible.

De hecho, cualquier aplicación que necesite soportar cualquier versión de PHP que no sea la más reciente (incluso dentro del rango de las versiones de PHP actualmente soportadas) puede beneficiarse.

Este es el caso de Drupal, que requiere PHP 7.3. Gracias a la transpilación, los desarrolladores pueden crear módulos de Drupal disponibles públicamente utilizando PHP 8.0, y publicarlos con PHP 7.3.

Otro ejemplo es cuando se crea código personalizado para clientes que no pueden ejecutar PHP 8.0 en sus entornos por una u otra razón. Sin embargo, gracias a la transpilación, los desarrolladores pueden seguir codificando sus productos con PHP 8.0 y ejecutarlos en esos entornos heredados.

Cuándo transpilar PHP

El código PHP siempre se puede transpilar a menos que contenga alguna característica de PHP que no tenga equivalente en la versión anterior de PHP.

Ese es posiblemente el caso de los atributos, introducidos en PHP 8.0:

#[SomeAttr]
function someFunc() {}

#[AnotherAttr]
class SomeClass {}

En el ejemplo anterior en el que se utilizaban funciones de flecha, el código podía transpilarse porque las funciones de flecha son azúcar sintáctico. Los atributos, en cambio, crean un comportamiento completamente nuevo. Este comportamiento también podría ser reproducido con PHP 7.4 e inferior, pero solo codificándolo manualmente, es decir, no automáticamente basado en una herramienta o proceso (la IA podría proporcionar una solución, pero aún no estamos allí).

Los atributos destinados al uso en desarrollo, como #[Deprecated], pueden eliminarse del mismo modo que los tipos de unión. Pero los atributos que modifican el comportamiento de la aplicación en producción no se pueden eliminar, y tampoco se pueden transpilar directamente.

A día de hoy, ningún transpilador puede tomar código con atributos de PHP 8.0 y producir automáticamente su código equivalente de PHP 7.4. En consecuencia, si tu código PHP necesita usar atributos, entonces transpilarlo será difícil o inviable.

Funciones de PHP que se pueden transpilar

Estas son las características de PHP 7.1 y superiores que actualmente pueden ser transpiladas. Si tu código solo utiliza estas características, puedes disfrutar de la certeza de que tu aplicación transpilada funcionará. De lo contrario, tendrás que evaluar si el código transpilado producirá fallos.

Versión PHP Características
7.1 Todo
7.2 objecttipo
ampliación del tipo de parámetro
PREG_UNMATCHED_AS_NULL bandera en preg_match
7.3 Asignaciones de referencia en list() / desestructuración de arrays (excepto dentro de foreach#4376)
Sintaxis flexible de Heredoc y Nowdoc
Comas al final de las llamadas a funciones
set(raw)cookie acepta el argumento $option 7.4
Propiedades escritas
Funciones de las flechas
Operador de asignación coalescente nulo
Desembalaje dentro de las matrices
Separador literal numérico
strip_tags()con una matriz de nombres de etiquetas
tipos de retorno covariantes y tipos param contravariantes
8.0 Tipos de unión
mixed pseudotipo
static tipo de retorno
::classconstante mágica en los objetos
match expresiones
catch excepciones sólo por tipo
Operador a prueba de nulidades
Promoción de la propiedad del constructor de la clase
Comas al final de las listas de parámetros y listas de use de cierres lists 

 

Transpiladores PHP

Actualmente, existe una herramienta para transpilar código PHP: Rector.

Rector es una herramienta de reconstrucción de PHP, que convierte el código PHP basado en reglas programables. Introducimos el código fuente y el conjunto de reglas a ejecutar, y Rector transformará el código.

Rector se maneja a través de la línea de comandos, instalada en el proyecto a través de Composer. Cuando se ejecuta, Rector mostrará un «diff» (adiciones en verde, eliminaciones en rojo) del código antes y después de la conversión:

Salida
Salida «diff» de Rector

Qué versiones de PHP hay que transpilar

Para transpilar el código a través de las versiones de PHP, se deben crear las reglas correspondientes.

Hoy en día, la biblioteca Rector incluye la mayoría de las reglas para transpilar código dentro del rango de PHP 8.0 a 7.1. Por lo tanto, podemos transpilar de forma fiable nuestro código PHP hasta la versión 7.1.

También hay reglas para transpilar de PHP 7.1a7.0 y de 7.0 a 5.6, pero no son exhaustivas. Se está trabajando para completarlas, por lo que eventualmente podremos transpilar código PHP hasta la versión 5.6.

Transpilaje vs. Backporting

El backporting es similar al transpiling, pero más sencillo. La backporting de código no depende necesariamente de las nuevas características de un lenguaje. En su lugar, se puede proporcionar la misma funcionalidad a una versión anterior del lenguaje simplemente copiando/pegando/adaptando el código correspondiente de la nueva versión del lenguaje.

Por ejemplo, la función str_contains se introdujo en PHP 8.0. La misma función para PHP 7.4 e inferior se puede implementar fácilmente así:

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;
    }
  }
}

Dado que el backporting es más sencillo que la transpilación, deberíamos optar por esta solución siempre que el backporting haga el trabajo.

En cuanto al rango entre PHP 8.0 y 7.1, podemos utilizar las librerías polyfill de Symfony:

Estas bibliotecas soportan las siguientes funciones, clases, constantes e interfaces:

Versión PHP Características
7.2 Funciones:

Constantes:

7.3 Funciones:

Excepciones:

7.4 Funciones:
8.0 Interfaces:
  • Stringable

Clases:

  • ValueError
  • UnhandledMatchError

Constantes:

  • FILTER_VALIDATE_BOOL

Funciones:

Ejemplos de PHP transpilado

Vamos a inspeccionar algunos ejemplos de código PHP transpilado, y algunos paquetes que están siendo totalmente transpilados.

Código PHP

La expresión de match fue introducida en PHP 8.0. Este código fuente:

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

… se transpilará a su versión equivalente de PHP 7.4, utilizando el operador switch:

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

El operador nullsafe también fue introducido en PHP 8.0:

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

El código transpilado debe asignar primero el valor de la operación a una nueva variable, para evitar ejecutar la operación dos veces:

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

La característica de promoción de propiedades del constructor, también introducida en PHP 8.0, permite a los desarrolladores escribir menos código:

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

Al transpilarlo para PHP 7.4, se produce el código completo:

 class QueryResolver
 {
  protected QueryFormatter $queryFormatter;

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

El código transpilado arriba contiene propiedades tipadas, que fueron introducidas en PHP 7.4. La transpilación de ese código a PHP 7.3 las sustituye por docblocks:

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

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

Paquetes PHP

Las siguientes bibliotecas están siendo transpiladas para producción:

Biblioteca/descripción Código/notas
Rector
Herramienta de reconstrucción de PHP que hace posible la transpilación
Código fuente
Código transpilado
Notas
Normas de codificación sencillas
Herramienta para que el código PHP se adhiera a un conjunto de reglas
Código fuente
Código transpliado
Notas
API GraphQL para WordPress
Plugin que proporciona un servidor GraphQL para WordPress
Código fuente
Código transpilado
Notes

Ventajas y desventajas de transpilar PHP

La ventaja de transpilar PHP ya se ha descrito: permite que el código fuente utilice PHP 8.0 (es decir, la última versión de PHP), que se transformará en una versión inferior para que PHP se ejecute en producción en una aplicación o entorno heredado.

Esto nos permite efectivamente ser mejores desarrolladores, produciendo código de mayor calidad. Esto se debe a que nuestro código fuente puede utilizar los tipos de unión de PHP 8.0, las propiedades tipadas de PHP 7.4 y los diferentes tipos y pseudotipos añadidos a cada nueva versión de PHP (mixed de PHP 8.0, objetc de PHP 7.2), entre otras características modernas de PHP.

Gracias a estas funciones, podemos detectar mejor los errores durante el desarrollo y escribir un código más fácil de leer.

Ahora, veamos los inconvenientes.

Debe ser codificado y mantenido

Rector puede transpilar el código automáticamente, pero el proceso probablemente requerirá alguna entrada manual para que funcione con nuestra configuración específica.

Las bibliotecas de terceros también deben ser transpiladas

Esto se convierte en un problema cuando al transpilarlos se producen errores, ya que entonces debemos profundizar en su código fuente para averiguar la posible razón. Si el problema se puede solucionar y el proyecto es de código abierto, tendremos que enviar una solicitud de extracción. Si la biblioteca no es de código abierto, podemos encontrarnos con un obstáculo.

El rector no nos informa cuando el código no puede ser transpuesto

Si el código fuente contiene atributos de PHP 8.0 o cualquier otra característica que no pueda ser transpilada, no podemos proceder. Sin embargo, Rector no comprobará esta condición, por lo que tendremos que hacerlo manualmente. Esto puede no ser un gran problema en relación con nuestro propio código fuente, ya que estamos familiarizados con él, pero podría convertirse en un obstáculo en relación con las dependencias de terceros.

La información de depuración utiliza el código transpilado, no el código fuente

Cuando la aplicación produce un mensaje de error con un seguimiento de pila en producción, el número de línea apuntará al código transpilado. Tenemos que volver a convertir el código transpilado al original para encontrar el número de línea correspondiente en el código fuente.

El código transpilado también debe llevar un prefijo

Nuestro proyecto transpilado y alguna otra biblioteca también instalada en el entorno de producción podrían utilizar la misma dependencia de terceros. Esta dependencia de terceros será transpilada para nuestro proyecto y mantendrá su código fuente original para la otra biblioteca. Por lo tanto, la versión transpilada debe ser prefijada a través de PHP-Scoper, Strauss, o alguna otra herramienta para evitar potenciales conflictos.

La transpilación debe realizarse durante la integración continua (CI)

Debido a que el código transpilado naturalmente anulará el código fuente, no debemos ejecutar el proceso de transpilación en nuestros ordenadores de desarrollo, o nos arriesgaremos a crear efectos secundarios. Ejecutar el proceso durante una ejecución de CI es más adecuado (más sobre esto más adelante).

Cómo transpilar PHP

En primer lugar, tenemos que instalar Rector en nuestro proyecto para el desarrollo:

composer require rector/rector --dev

A continuación, creamos un archivo de configuración rector.php en el directorio raíz del proyecto que contiene los conjuntos de reglas necesarios. Para bajar el código de PHP 8.0 a 7.1, usamos esta configuración:

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 asegurarnos de que el proceso se ejecuta como se espera, podemos ejecutar el comando de process de Rector en modo seco, pasando la(s) ubicación(es) a procesar (en este caso, todos los archivos bajo la carpeta src/):

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

Para realizar la transpilación, ejecutamos el comando de process de Rector, que modificará los archivos en su ubicación actual:

vendor/bin/rector process src

Ten en cuenta: si ejecutamos el  rector process en nuestros ordenadores de desarrollo, el código fuente se convertirá en su lugar, bajo src/. Sin embargo, queremos producir el código convertido en una ubicación diferente para no sobrepasar el código fuente cuando se degrade el código. Por esta razón, ejecutar el proceso es más adecuado durante la integración continua.

Optimización del proceso de transpilación

Para generar un entregable transpilado para producción, solo hay que convertir el código para producción; se puede omitir el código necesario solo para desarrollo. Esto significa que podemos evitar transpilar todas las pruebas (tanto para nuestro proyecto como para sus dependencias) y todas las dependencias para el desarrollo.

En cuanto a las pruebas, ya sabremos dónde se encuentran las de nuestro proyecto, por ejemplo, en la carpeta tests/. También debemos averiguar dónde están los de las dependencias – por ejemplo, en sus subcarpetas tests/, test/ y Test/ (para diferentes librerías). Entonces, le decimos a Rector que omita el procesamiento de estas carpetas:

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

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

Con respecto a las dependencias, Composer sabe cuáles son para el desarrollo (las que están bajo la entrada require-dev en composer.json) y cuáles son para producción (las que están bajo la entrada require).

Para recuperar de Composer las rutas de todas las dependencias para la producción, ejecutamos:

composer info --path --no-dev

Este comando producirá una lista de dependencias con su nombre y ruta de acceso, así:

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

Podemos extraer todas las rutas e introducirlas en el comando Rector, que procesará la carpeta src/ de nuestro proyecto más aquellas carpetas que contengan todas las dependencias para producción:

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

Una mejora adicional puede evitar que Rector procese aquellas dependencias que ya utilizan la versión de PHP de destino. Si una biblioteca ha sido codificada con PHP 7.1 (o cualquier versión inferior), entonces no hay necesidad de transpilarla a PHP 7.1.

Para lograr esto, podemos obtener la lista de bibliotecas que requieren PHP 7.2 y superior y procesar solo esas. Obtendremos los nombres de todas estas bibliotecas a través del comando why-not de Composer, así:

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

Debido a que este comando no funciona con la bandera --no-dev, para incluir solo las dependencias para producción, primero tenemos que eliminar las dependencias para desarrollo y regenerar el autoloader, ejecutar el comando, y luego añadirlas de nuevo:

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

El comando info --path de Composer recupera la ruta de un paquete, con este formato:

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

Ejecutamos este comando para todos los elementos de nuestra lista para obtener todas las rutas a transpilar:

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

Finalmente, proporcionamos esta lista al Rector (además de la carpeta src/ del proyecto):

vendor/bin/rector process src $paths

Errores que debemos evitar al transpilar código

Transpilar código podría considerarse un arte, que a menudo requiere ajustes específicos para el proyecto. Veamos algunos problemas con los que nos podemos encontrar.

Las reglas encadenadas no siempre se procesan

Una regla encadenada es cuando una regla necesita convertir el código producido por una regla anterior.

Por ejemplo, la biblioteca symfony/cache contiene este código:

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

l transpilar de PHP 7.4 a 7.3,  tag de la función debe sufrir dos modificaciones:

El resultado final debería ser éste:

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

Sin embargo, el rector solo da salida a la etapa intermedia:

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

La cuestión es que el rector no siempre puede controlar el orden de aplicación de las normas.

La solución es identificar qué reglas encadenadas quedaron sin procesar y ejecutar una nueva ejecución de Rector para aplicarlas.

Para identificar las reglas encadenadas, ejecutamos Rector dos veces en el código fuente, así:

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

La primera vez, ejecutamos Rector como se espera, para ejecutar la transpilación. La segunda vez, usamos la bandera --dry-run para descubrir si todavía hay cambios que hacer. Si los hay, el comando saldrá con un código de error, y la salida «diff» indicará qué regla(s) puede(n) aplicarse todavía. Esto significaría que la primera ejecución no se ha completado, con alguna regla encadenada que no se ha procesado.

Ejecución de Rector con la bandera --dry-run
Ejecución de Rector con la bandera –dry-run

Una vez que hayamos identificado la regla (o reglas) encadenada sin aplicar, podemos crear otro archivo de configuración de Rector – por ejemplo, rector-chained-rule.php ejecutará la regla que falta. En lugar de procesar un conjunto completo de reglas para todos los archivos bajo src/, esta vez, podemos ejecutar la regla específica que falta en el archivo específico donde debe aplicarse:

// 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, le decimos a Rector en su segunda pasada que utilice el nuevo archivo de configuración mediante la entrada --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

Las dependencias de Composer pueden ser inconsistentes

Las librerías pueden declarar una dependencia para ser destinada al desarrollo (es decir, bajo require-dev en composer.json), y aún así, referenciar algún código de ellas para producción (como en algunos archivos bajo src/, no tests/).

Normalmente, esto no es un problema porque ese código puede no cargarse en producción, por lo que nunca habrá un error en la aplicación. Sin embargo, cuando Rector procesa el código fuente y sus dependencias, valida que todo el código referenciado pueda ser cargado. Rector arrojará un error si algún archivo hace referencia a alguna pieza de código de una biblioteca no instalada (porque fue declarada como necesaria sólo para el desarrollo).

Por ejemplo, la clase EarlyExpirationHandler del componente Cache de Symfony implementa la interfaz MessageHandlerInterface del componente Messenger:

class EarlyExpirationHandler implements MessageHandlerInterface
{
    //...
}

Sin embargo, symfony/cache declara que symfony/messenger es una dependencia para el desarrollo. Entonces, al ejecutar Rector en un proyecto que depende de symfony/cache, arrojará un error:

[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".   

Hay tres soluciones para esta cuestión:

  1. En la configuración del rector, omita el procesamiento del archivo que hace referencia a ese fragmento de código:
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::SKIP, [
    __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php',
  ]);
};
  1. Descarga la biblioteca que falta y añada tu ruta para que sea autocargada por Rector:
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/vendor/symfony/messenger',
  ]);
};
  1. Haz que tu proyecto dependa de la biblioteca que falta para la producción:
composer require symfony/messenger

Transpilación e integración continua

Como se mencionó anteriormente, en nuestros equipos de desarrollo debemos utilizar la bandera --dry-run cuando ejecutamos Rector, o de lo contrario, el código fuente será anulado con el código transpilado. Por esta razón, es más adecuado ejecutar el proceso de transpilación real durante la integración continua (CI), donde podemos girar corredores temporales para ejecutar el proceso.

Un momento ideal para ejecutar el proceso de transpilación es cuando se genera la liberación de nuestro proyecto. Por ejemplo, el código siguiente es un flujo de trabajo para GitHub Actions, que crea la liberación de un plugin de 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 flujo de trabajo contiene un procedimiento estándar para liberar un plugin de WordPressa través de GitHub Actions. La nueva adición, para transpilar el código del plugin de PHP 7.4 a 7.1, ocurre en este paso:

      - 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

En conjunto, este flujo de trabajo realiza ahora los siguientes pasos:

  1. Comprueba el código fuente de un plugin de WordPress desde su repositorio, escrito con PHP 7.4
  2. Instala sus dependencias de Composer
  3. Transpila su código de PHP 7.4 a 7.1
  4. Modifica la entrada «Requiere PHP» en la cabecera del archivo principal del plugin de "7.4" a "7.1"
  5. Elimina las dependencias necesarias para el desarrollo
  6. Crea el archivo . zip del plugin, excluyendo todos los archivos innecesarios
  7. Sube el archivo . zip como un activo de lanzamiento (y, además, como un artefacto a la Acción GitHub)

Prueba del código transpilado

Una vez que el código ha sido transpilado a PHP 7.1, ¿cómo sabemos que funciona bien? O, en otras palabras, ¿cómo sabemos que se ha convertido a fondo, y que no se han dejado restos de versiones superiores de código PHP?

De forma similar a transpilar el código, podemos implementar la solución dentro de un proceso de CI. La idea es configurar el entorno del corredor con PHP 7.1 y ejecutar un linter en el código transpilado. Si alguna parte del código no es compatible con PHP 7.1 (como una propiedad tipada de PHP 7.4 que no fue convertida), entonces el linter arrojará un error.

Un linter para PHP que funciona bien es PHP Parallel Lint. Podemos instalar esta biblioteca como una dependencia para el desarrollo en nuestro proyecto, o hacer que el proceso de CI lo instale como un proyecto independiente de Composer:

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

Siempre que el código contenga PHP 7.2 y superior, PHP Parallel Lint lanzará un error como éste:

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 a añadir el linter en el flujo de trabajo de nuestro CI. Los pasos a ejecutar para transpilar código de PHP 8.0 a 7.1 y probarlo son:

  1. Consulta el código fuente
  2. Hacer que el entorno ejecute PHP 8.0, para que Rector pueda interpretar el código fuente
  3. Transpila el código a PHP 7.1
  4. Instala la herramienta PHP linter
  5. Cambia la versión de PHP del entorno a la 7.1
  6. Ejecuta el linter en el código transpilado

Este flujo de trabajo de GitHubAction hace el trabajo:

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

Ten en cuenta que varios archivos bootstrap80.php de las bibliotecas polyfill de Symfony (que no necesitan ser transpiladas) deben ser excluidos del linter. Estos archivos contienen PHP 8.0, por lo que el linter arrojaría errores al procesarlos. Sin embargo, excluir estos archivos es seguro ya que se cargarán en producción sólo cuando se ejecute PHP 8.0 o superior:

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

Resumen

Este artículo nos enseñó cómo transpilar nuestro código PHP, permitiéndonos usar PHP 8.0 en el código fuente y crear una versión que funciona en PHP 7.1. La transpilación se realiza a través de Rector, una herramienta de reconstrucción de PHP.

Transpilar nuestro código nos hace mejores desarrolladores, ya que podemos detectar mejor los errores en el desarrollo y producir un código que, naturalmente, es más fácil de leer y entender.

La transpilación también nos permite desacoplar nuestro código con requisitos específicos de PHP del CMS. Ahora podemos hacerlo si deseamos utilizar la última versión de PHP para crear un plugin de WordPress o un módulo de Drupal de acceso público sin restringir gravemente nuestra base de usuarios.

¿Te queda alguna duda sobre la transpilación de PHP? ¡Háznoslo saber en la sección de comentarios!

Leonardo Losoviz

Leo escribe sobre tendencias innovadoras en desarrollo web, principalmente sobre PHP, WordPress y GraphQL. Puedes encontrarle en leoloso.com y X.com/losoviz.