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%:
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.
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:
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:
Clases:
Constantes:
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 tipo de retorno
ItemInterface
debe convertirse primero enself
, debido a la reglaDowngradeCovariantReturnTypeRector
- El tipo de retorno
self
debe entonces ser eliminado, debido a la reglaDowngradeSelfTypeDeclarationRector
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.
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:
- 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',
]);
};
- 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',
]);
};
- 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:
- Comprueba el código fuente de un plugin de WordPress desde su repositorio, escrito con PHP 7.4
- Instala sus dependencias de Composer
- Transpila su código de PHP 7.4 a 7.1
- Modifica la entrada «Requiere PHP» en la cabecera del archivo principal del plugin de
"7.4"
a"7.1"
- Elimina las dependencias necesarias para el desarrollo
- Crea el archivo . zip del plugin, excluyendo todos los archivos innecesarios
- 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:
- Consulta el código fuente
- Hacer que el entorno ejecute PHP 8.0, para que Rector pueda interpretar el código fuente
- Transpila el código a PHP 7.1
- Instala la herramienta PHP linter
- Cambia la versión de PHP del entorno a la 7.1
- 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!
Deja una respuesta