La guía definitiva para transpilar código PHP

Publicado: 2021-09-22

En circunstancias ideales, deberíamos usar PHP 8.0 (la última versión al momento de escribir esto) 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 crean un complemento público para WordPress o trabajan con código heredado que impide la actualización del entorno del servidor web.

En estas situaciones, podríamos perder la esperanza de usar el último código PHP. Pero hay una mejor alternativa: aún podemos escribir 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, le enseñaremos todo lo que necesita saber sobre la transpilación de código PHP.

¿Qué es transpilar?

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

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

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

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

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

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

¿Qué es transpilar PHP?

Lo que es potencialmente nuevo dentro del desarrollo web es la posibilidad de transpilar 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 anterior 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 PHP 7.3 equivalente:

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

Las funciones de flecha se pueden transpilar porque son azúcar sintáctica, es decir, una nueva sintaxis para producir un comportamiento existente. Esta es la fruta madura.

Sin embargo, también hay nuevas funciones que crean un nuevo comportamiento y, como tal, no habrá un 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, aún se puede realizar la transpilación siempre que la nueva característica sea necesaria para el desarrollo pero no para la producción. Luego, podemos simplemente eliminar la función por completo del código transpilado sin consecuencias graves.

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

Por lo tanto, podemos darnos el lujo de eliminar la función del código para la producción:

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

Si el error aún ocurre en producción, el mensaje de error arrojado será menos preciso que si tuviéramos tipos de unión. Sin embargo, esta desventaja potencial se compensa al poder usar tipos de unión en primer lugar.

En un mundo perfecto, deberíamos poder usar PHP 8.0 en todos nuestros sitios y actualizarlo tan pronto como se publique una nueva versión. Pero ese no es siempre el caso. Aprenda todo lo que necesita saber sobre la transpilación de código PHP aquí Haga clic para twittear

Ventajas de transpilar 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 particularmente útil para los desarrolladores que crean productos para sistemas de administración de contenido (CMS) heredados. WordPress, por ejemplo, todavía admite 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 todos End-of-Life (EOL), lo que significa que ya no reciben actualizaciones de seguridad, se ubica en un considerable 34.8%, y aquellos que se ejecutan en cualquier versión de PHP que no sea 8.0 se sitúa en la friolera de 99,5%:

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

En consecuencia, es muy probable que los temas y complementos de WordPress dirigidos a una audiencia global se codifiquen con una versión anterior de PHP para aumentar su posible alcance. Gracias a la transpilación, estos podrían codificarse con PHP 8.0 y aún así publicarse para una versión anterior de PHP, por lo que podrían dirigirse a la mayor cantidad de usuarios posible.

De hecho, cualquier aplicación que necesite admitir cualquier versión de PHP que no sea la más reciente (incluso dentro del rango de las versiones de PHP actualmente admitidas) 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 usando PHP 8.0 y lanzarlos 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 razón u otra. Sin embargo, gracias a la transpilación, los desarrolladores aún pueden codificar sus entregables usando 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 con los atributos, introducidos en PHP 8.0:

 #[SomeAttr] function someFunc() {} #[AnotherAttr] class SomeClass {}

En el ejemplo anterior usando funciones de flecha, el código podría transpilarse porque las funciones de flecha son azúcar sintáctica. Los atributos, por el contrario, crean un comportamiento completamente nuevo. Este comportamiento también podría reproducirse con PHP 7.4 e inferior, pero solo codificándolo manualmente, es decir, no automáticamente en función de una herramienta o proceso (la IA podría proporcionar una solución, pero aún no hemos llegado allí).

Los atributos destinados al uso de desarrollo, como #[Deprecated] , se pueden eliminar de la misma manera que se eliminan 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 partir de hoy, ningún transpilador puede tomar código con atributos PHP 8.0 y producir automáticamente su código PHP 7.4 equivalente. En consecuencia, si su código PHP necesita usar atributos, transpilarlo será difícil o inviable.

Características de PHP que se pueden transpilar

Estas son las características de PHP 7.1 y superiores que actualmente se pueden transpilar. Si su código solo usa estas funciones, puede disfrutar de la certeza de que su aplicación transpilada funcionará. De lo contrario, deberá evaluar si el código transpilado producirá fallas.

Versión PHP Características
7.1 Todo
7.2 – tipo de object
– ampliación del tipo de parámetro
PREG_UNMATCHED_AS_NULL en preg_match
7.3 – Asignaciones de referencia en list() / desestructuración de matrices ( Excepto dentro de foreach — #4376)
– Sintaxis flexible de Heredoc y Nowdoc
– Comas finales en llamadas a funciones
set(raw)cookie acepta el argumento $option
7.4 – Propiedades tipeadas
– Funciones de flecha
– Operador de asignación coalescente nulo
– Desempaquetado dentro de arreglos
– Separador literal numérico
strip_tags() con una matriz de nombres de etiquetas
– tipos de retorno covariantes y tipos de parámetros contravariantes
8.0 – Tipos de unión
– pseudotipo mixed
– tipo de retorno static
::class en los objetos
– Expresiones de match
catch excepciones solo por tipo
– Operador de seguridad nula
– Promoción de propiedad de constructor de clase
– Comas finales en listas de parámetros y listas de use de cierre

Transpiladores de 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 en función de reglas programables. Ingresamos el código fuente y el conjunto de reglas para ejecutar, y Rector transformará el código.

Rector se opera a través de la línea de comandos, instalado en el proyecto a través de Composer. Cuando se ejecuta, Rector generará una "diferencia" (adiciones en verde, eliminaciones en rojo) del código antes y después de la conversión:

salida "diff" de Rector
Salida "diff" de Rector

A qué versión de PHP transpilar

Para transpilar código entre 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 manera confiable nuestro código PHP hasta la versión 7.1.

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

Transpiling vs Backporting

La retroportación es similar a la transpilación, pero más sencilla. El código de backporting no depende necesariamente de las nuevas funciones de un lenguaje. En cambio, 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 y versiones anteriores se puede implementar fácilmente de esta manera:

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

Debido a que el backporting es más simple que la transpilación, deberíamos optar por esta solución siempre que el backporting funcione.

Con respecto al rango entre PHP 8.0 a 7.1, podemos usar las bibliotecas polyfill de Symfony:

  • Polyfill PHP 7.1
  • Polyfill PHP 7.2
  • Polyfill PHP 7.3
  • Polyfill PHP 7.4
  • Polyfill PHP 8.0

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

Versión PHP Características
7.2 Funciones:
  • spl_object_id
  • utf8_encode
  • utf8_decode

Constantes:

  • PHP_FLOAT_*
  • PHP_OS_FAMILY
7.3 Funciones:
  • array_key_first
  • array_key_last
  • hrtime
  • is_countable

Excepciones:

  • JsonException
7.4 Funciones:
  • get_mangled_object_vars
  • mb_str_split
  • password_algos
8.0 Interfaces:
  • Stringable

Clases:

  • ValueError
  • UnhandledMatchError

Constantes:

  • FILTER_VALIDATE_BOOL

Funciones:

  • fdiv
  • get_debug_type
  • preg_last_error_msg
  • str_contains
  • str_starts_with
  • str_ends_with
  • get_resource_id

Ejemplos de PHP Transpilado

Inspeccionemos algunos ejemplos de código PHP transpilado y algunos paquetes que se están transpilando por completo.

Código PHP

La expresión de match se introdujo 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 PHP 7.4 equivalente, utilizando el operador de 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 se introdujo en PHP 8.0:

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

El código transpilado necesita 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 función 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 escritas, que se introdujeron en PHP 7.4. Transpilar ese código a PHP 7.3 los reemplaza con docblocks:

 class QueryResolver { /** * @var QueryFormatter */ protected $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }

Paquetes PHP

Las siguientes bibliotecas se están transpilando 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
Estándares de codificación fáciles
Herramienta para que el código PHP se adhiera a un conjunto de reglas
- Código fuente
– Código transpilado
– Notas
API de GraphQL para WordPress
Complemento que proporciona un servidor GraphQL para WordPress
- Código fuente
– Código transpilado
– Notas

Pros y contras de transpilar PHP

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

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

Con estas características, podemos detectar mejor los errores durante el desarrollo y escribir código que sea más fácil de leer.

Ahora, echemos un vistazo a los inconvenientes.

Debe codificarse y mantenerse

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

Las bibliotecas de terceros también deben transpilarse

Esto se convierte en un problema cada vez que transpilarlos produce errores, ya que luego 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, necesitaremos enviar una solicitud de incorporación de cambios. Si la biblioteca no es de código abierto, podemos encontrarnos con un obstáculo.

Rector no nos informa cuando no se puede transpilar el código

Si el código fuente contiene atributos de PHP 8.0 o cualquier otra característica que no se pueda transpilar, no podemos continuar. Sin embargo, Rector no verificará esta condición, por lo que debemos hacerlo manualmente. Puede que esto no sea un gran problema con respecto a nuestro propio código fuente, ya que ya lo conocemos, pero podría convertirse en un obstáculo con respecto a las dependencias de terceros.

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

Cuando la aplicación genera un mensaje de error con un seguimiento de pila en producción, el número de línea apuntará al código transpilado. Necesitamos 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 tener un prefijo

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

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

Debido a que el código transpilado anulará naturalmente el código fuente, no debemos ejecutar el proceso de transpilación en nuestras computadoras 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 a continuación).

Cómo transpilar PHP

Primero, necesitamos instalar Rector en nuestro proyecto de desarrollo:

 composer require rector/rector --dev

Luego creamos un archivo de configuración rector.php en el directorio raíz del proyecto que contiene los conjuntos de reglas requeridos. Para degradar 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 ejecute como se esperaba, podemos ejecutar el comando de process de Rector en modo seco, pasando las ubicaciones para procesar (en este caso, todos los archivos en 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 dentro de su ubicación actual:

 vendor/bin/rector process src

Tenga en cuenta: si ejecutamos el rector process en nuestras computadoras de desarrollo, el código fuente se convertirá en su lugar, en src/ . Sin embargo, queremos producir el código convertido en una ubicación diferente para no anular el código fuente al degradar 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 se debe convertir el código para producción; el código necesario solo para el desarrollo se puede omitir. Eso 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 bibliotecas). Luego, le decimos a Rector que se salte 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 desarrollo (aquellas bajo la entrada require-dev en composer.json ) y cuáles son para producción (aquellas 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, 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

Podemos extraer todas las rutas e introducirlas en el comando Rector, que luego procesará la carpeta src/ de nuestro proyecto más las carpetas que contienen todas las dependencias para la 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 esas dependencias que ya utilizan la versión PHP de destino. Si una biblioteca ha sido codificada con PHP 7.1 (o cualquier versión anterior), 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 mediante el comando why-not de Composer, así:

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

Debido a que este comando no funciona con el indicador --no-dev , para incluir solo dependencias para producción, primero debemos eliminar las dependencias para desarrollo y regenerar el cargador automático, ejecutar el comando y luego agregarlas nuevamente:

 $ 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 para transpilar:

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

Finalmente, proporcionamos esta lista a Rector (más la carpeta src/ del proyecto):

¿Necesita una solución de hospedaje que le brinde una ventaja competitiva? Kinsta lo tiene cubierto con una velocidad increíble, seguridad de última generación y escalado automático. Consulta nuestros planes

 vendor/bin/rector process src $paths

Trampas a evitar al transpilar código

La transpilación de código podría considerarse un arte y, a menudo, requiere ajustes específicos para el proyecto. Veamos algunos problemas en 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; } }

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

  • El tipo de devolución ItemInterface debe convertirse primero en self , debido a la regla DowngradeCovariantReturnTypeRector
  • El tipo de devolución self debe eliminarse debido a la regla DowngradeSelfTypeDeclarationRector

El resultado final debería ser este:

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

Sin embargo, Rector solo genera la etapa intermedia:

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

El problema es que Rector no siempre puede controlar el orden en que se aplican las reglas.

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 esperaba, para ejecutar la transpilación. La segunda vez, usamos el --dry-run para descubrir si todavía hay cambios por hacer. Si los hay, el comando saldrá con un código de error y la salida "diff" indicará qué regla(s) aún se pueden aplicar. Eso significaría que la primera ejecución no se completó, con alguna regla encadenada que no se procesó.

Ejecutar Rector con indicador --dry-run
Ejecutando Rector con indicador de ejecución en seco

Una vez que hayamos identificado la regla (o reglas) encadenada no aplicada, podemos crear otro archivo de configuración de Rector; por ejemplo, rector-chained-rule.php ejecutará la regla faltante. En lugar de procesar un conjunto completo de reglas para todos los archivos en src/ , esta vez, podemos ejecutar la regla faltante específica 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 segundo paso que use el nuevo archivo de configuración a través de 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

Las dependencias del compositor pueden ser inconsistentes

Las bibliotecas podrían declarar una dependencia para ser programada para el desarrollo (es decir, bajo require-dev en composer.json ), y aún así, hacer referencia a algún código de ellas para producción (como en algunos archivos bajo src/ , no tests/ ).

Por lo general, esto no es un problema porque es posible que ese código no se cargue 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 se pueda cargar todo el código referenciado. Rector arrojará un error si algún archivo hace referencia a algún fragmento de código de una biblioteca no instalada (porque se declaró necesario solo para 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. Luego, 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 a este problema:

  1. En la configuración de 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. Descargue la biblioteca que falta y agregue su ruta para que Rector la cargue automáticamente:
 return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::AUTOLOAD_PATHS, [ __DIR__ . '/vendor/symfony/messenger', ]); };
  1. Haga que su proyecto dependa de la biblioteca que falta para la producción:
 composer require symfony/messenger

Transpiling e Integración Continua

Como se mencionó anteriormente, en nuestras computadoras de desarrollo debemos usar el --dry-run al ejecutar Rector, o de lo contrario, el código fuente se anulará 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 activar ejecutores temporales para ejecutar el proceso.

Un momento ideal para ejecutar el proceso de transpilación es cuando se genera la versión para nuestro proyecto. Por ejemplo, el siguiente código es un flujo de trabajo para GitHub Actions, que crea el lanzamiento de un complemento 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/[email protected] - 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/[email protected] with: name: graphql-api path: build/graphql-api.zip - name: Upload to release uses: JasonEtco/[email protected] with: args: build/graphql-api.zip application/zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Este flujo de trabajo contiene un procedimiento estándar para lanzar un complemento de WordPress a través de GitHub Actions. La nueva adición, transpilar el código del complemento 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 ahora realiza los siguientes pasos:

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

Prueba del código transpilado

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

Similar a la transpilación del 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 algún fragmento de código no es compatible con PHP 7.1 (como una propiedad escrita de PHP 7.4 que no se convirtió), el linter generará 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 la 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 arrojará un error 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.

Agreguemos el linter al 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. Mira el código fuente
  2. Haga que el entorno ejecute PHP 8.0, para que Rector pueda interpretar el código fuente
  3. Transpilar el código a PHP 7.1
  4. Instale la herramienta PHP linter
  5. Cambie la versión de PHP del entorno a 7.1
  6. Ejecute el linter en el código transpilado

Este flujo de trabajo de GitHub Action 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/[email protected] - name: Set-up PHP uses: shivammathur/[email protected] 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/[email protected] 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 transpilarse) deben excluirse del linter. Estos archivos contienen PHP 8.0, por lo que el linter generaría errores al procesarlos. Sin embargo, excluir estos archivos es seguro ya que se cargarán en producción solo cuando se ejecute PHP 8.0 o superior:

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

Ya sea que esté creando un complemento público para WordPress o esté actualizando el código heredado, hay muchas razones por las que usar la última versión de PHP puede ser imposible Aprenda cómo la transpilación puede ayudar en esta guía Haga clic para tuitear

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 funcione en PHP 7.1. La transpilación se realiza a través de Rector, una herramienta de reconstrucción de PHP.

La transpilación de nuestro código nos convierte en 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 comprender.

La transpilación también nos permite desvincular nuestro código de los requisitos específicos de PHP del CMS. Ahora podemos hacerlo si deseamos usar la última versión de PHP para crear un complemento de WordPress disponible públicamente o un módulo de Drupal sin restringir severamente nuestra base de usuarios.

¿Tiene alguna pregunta sobre la transpilación de PHP? ¡Infórmenos en la sección para comentarios!