Dans des circonstances idéales, nous devrions utiliser PHP 8.0 (la dernière version au moment d’écrire ces lignes) pour tous nos sites et le mettre à jour dès qu’une nouvelle version est publiée. Cependant, les développeurs devront souvent travailler avec des versions antérieures de PHP, par exemple lorsqu’ils créent une extension publique pour WordPress ou qu’ils travaillent avec du code hérité qui empêche la mise à jour de l’environnement du serveur web.

Dans ces situations, nous pourrions abandonner l’espoir d’utiliser le dernier code PHP. Mais il existe une meilleure alternative : nous pouvons toujours écrire notre code source avec PHP 8.0 et le transpiler vers une version antérieure de PHP – même vers PHP 7.1.

Dans ce guide, nous allons vous apprendre tout ce que vous devez savoir sur la transpilation du code PHP.

Qu’est-ce que la transpilation ?

La transpilation convertit le code source d’un langage de programmation en un code source équivalent du même langage de programmation ou d’un langage différent.

La transpilation n’est pas un concept nouveau dans le développement web : les développeurs côté client connaissent probablement Babel, un transpileur pour le code JavaScript.

Babel convertit le code JavaScript de la version moderne ECMAScript 2015+ en une version héritée compatible avec les anciens navigateurs. Par exemple, étant donné une fonction flèche ES2015 :

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

…Babel la convertira en sa version ES5 :

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

Qu’est-ce que la transpilation PHP ?

Ce qui est potentiellement nouveau dans le développement web, c’est la possibilité de transpiler le code côté serveur, en particulier PHP.

La transpilation PHP fonctionne de la même manière que la transpilation JavaScript : le code source d’une version moderne de PHP est converti en un code équivalent pour une version plus ancienne de PHP.

En suivant le même exemple que précédemment, une fonction flèche de PHP 7.4 :

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

…peut être transpilée dans sa version équivalente de PHP 7.3 :

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

Les fonctions fléchées peuvent être transpilées car elles sont du syntaxic sugar, c’est-à-dire une nouvelle syntaxe pour produire un comportement existant.

Cependant, il existe aussi de nouvelles fonctions qui créent un nouveau comportement, et à ce titre, il n’y aura pas de code équivalent pour les versions précédentes de PHP. C’est le cas des types d’union, introduits dans PHP 8.0 :

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

Dans ces situations, la transpilation peut toujours être effectuée tant que la nouvelle fonctionnalité est nécessaire pour le développement mais pas pour la production. Dans ce cas, nous pouvons simplement supprimer complètement la fonctionnalité du code transpilé sans conséquences graves.

Un exemple est celui des types d’union. Cette fonctionnalité est utilisée pour vérifier qu’il n’y a pas d’incompatibilité entre le type d’entrée et sa valeur fournie, ce qui permet d’éviter les bogues. S’il y a un conflit avec les types, il y aura une erreur déjà dans le développement, et nous devrions l’attraper et la corriger avant que le code n’atteigne la production.

Nous pouvons donc nous permettre de supprimer la fonctionnalité du code pour la production :

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

Si l’erreur se produit quand même en production, le message d’erreur sera moins précis que si nous avions des types d’union. Cependant, cet inconvénient potentiel est compensé par la possibilité d’utiliser les types d’union en premier lieu.

Avantages de la transpilation du code PHP

La transpilation permet de coder une application en utilisant la dernière version de PHP et de produire une version qui fonctionne également dans des environnements exécutant des versions plus anciennes de PHP.

Cela peut être particulièrement utile pour les développeurs qui créent des produits pour les anciens systèmes de gestion de contenu (CMS). WordPress, par exemple, prend toujours officiellement en charge PHP 5.6 (même s’il recommande PHP 7.4+). Le pourcentage de sites WordPress exécutant les versions 5.6 à 7.2 de PHP – qui sont toutes en fin de vie (End Of Life ou EOL), ce qui signifie qu’elles ne reçoivent plus de mises à jour de sécurité – s’élève à 34,8 %, et ceux fonctionnant avec une version de PHP autre que 8.0 à 99,5 % :

Statistiques d'utilisation de WordPress par version.
Statistiques d’utilisation de WordPress par version. Source de l’image : WordPress

Par conséquent, les thèmes et extensions WordPress destinés à un public mondial seront très probablement codés avec une ancienne version de PHP pour augmenter leur portée possible. Grâce à la transpilation, ils pourraient être codés avec PHP 8.0, et être tout de même publiés pour une ancienne version de PHP, ciblant ainsi le plus grand nombre d’utilisateurs possible.

En effet, toute application qui doit prendre en charge une version de PHP autre que la plus récente (même dans la gamme des versions PHP actuellement prises en charge) peut en bénéficier.

C’est le cas de Drupal, qui nécessite PHP 7.3. Grâce à la transpilation, les développeurs peuvent créer des modules Drupal accessibles au public en utilisant PHP 8.0, et les publier avec PHP 7.3.

Un autre exemple est la création de code personnalisé pour les clients qui ne peuvent pas exécuter PHP 8.0 dans leur environnement pour une raison ou une autre. Néanmoins, grâce à la transpilation, les développeurs peuvent toujours coder leurs produits livrables en utilisant PHP 8.0 et les exécuter sur ces anciens environnements.

Quand transpiler PHP

Le code PHP peut toujours être transpilé, sauf s’il contient une fonctionnalité PHP qui n’a pas d’équivalent dans la version précédente de PHP.

C’est peut-être le cas des attributs, introduits dans PHP 8.0 :

#[SomeAttr]
function someFunc() {}

#[AnotherAttr]
class SomeClass {}

Dans l’exemple précédent utilisant les fonctions flèches, le code pouvait être transpilé car les fonctions flèches sont du syntaxic sugar. Les attributs, en revanche, créent un comportement complètement nouveau. Ce comportement pourrait aussi être reproduit avec PHP 7.4 et inférieur, mais seulement en le codant manuellement, c’est-à-dire pas automatiquement sur la base d’un outil ou d’un processus (l’IA pourrait fournir une solution, mais nous n’en sommes pas encore là).

Les attributs destinés à être utilisés pour le développement, tels que #[Deprecated]peuvent être supprimés de la même manière que les types d’union. Mais les attributs qui modifient le comportement de l’application en production ne peuvent pas être supprimés, et ils ne peuvent pas non plus être directement transpilés.

À ce jour, aucun transpilateur ne peut prendre du code avec des attributs PHP 8.0 et produire automatiquement son code équivalent en PHP 7.4. Par conséquent, si votre code PHP doit utiliser des attributs, il sera difficile, voire impossible, de le transpiler.

Fonctionnalités PHP qui peuvent être transpilées

Ce sont les fonctionnalités de PHP 7.1 et plus qui peuvent actuellement être transpilées. Si votre code n’utilise que ces fonctionnalités, vous pouvez avoir la certitude que votre application transpilée fonctionnera. Sinon, vous devrez évaluer si le code transpilé produira des défaillances.

Version de PHP Caractéristiques
7.1 Tout
7.2 type objet
élargissement du type de paramètre
Drapeau PREG_UNMATCHED_AS_NULL dans preg_match
7.3 Affectations de référence dans la déstructuration des list() (sauf à l’intérieur de foreach#4376)
Syntaxe Heredoc et Nowdoc flexible
Virgules de fin dans les appels de fonctions
set(raw)cookie accepte l’argument $option
7.4 Propriétés typées
Fonctions fléchées
Opérateur d’affectation à coalescence nulle
Déballage à l’intérieur des tableaux
Séparateur de littéral numérique
strip_tags()avec un tableau de noms de balises
types de retour covariants et types de paramètres contravariants
8.0 Types d’union
pseudo-type mixed
type de retour static
::class constante magique sur les objets
match expressions
catch exceptions uniquement par type
Opérateur à sécurité nulle
Promotion des propriétés du constructeur de classe
Virgules de fin dans les listes de paramètres et les listes d’use fermetures

Transpilateurs PHP

Actuellement, il existe un outil pour transpiler le code PHP : Rector.

Rector est un outil de reconstruction de PHP, qui convertit le code PHP sur la base de règles programmables. On entre le code source et l’ensemble des règles à exécuter, et Rector transformera le code.

Rector s’utilise en ligne de commande et est installé dans le projet via Composer. Une fois exécuté, Rector produira un « diff » (ajouts en vert, suppressions en rouge) du code avant et après la conversion :

Sortie « diff » de Rector
Sortie « diff » de Rector

Quelle version de PHP transpiler

Pour transpiler du code entre plusieurs versions de PHP, il faut créer les règles correspondantes.

Aujourd’hui, la bibliothèque Rector comprend la plupart des règles pour transpiler du code dans la plage de PHP 8.0 à 7.1. Nous pouvons donc transpiler notre code PHP de manière fiable jusqu’à la version 7.1.

Il existe également des règles pour transpiler de PHP 7.1 à 7.0 et de 7.0 à 5.6, mais elles ne sont pas exhaustives. Des travaux sont en cours pour les compléter, afin que nous puissions éventuellement transpiler du code PHP jusqu’à la version 5.6.

Transpiler vs rétroporter

Le rétroportage (backporting) est similaire à la transpilation (transpiling), mais plus simple. Le rétroportage de code ne repose pas nécessairement sur les nouvelles fonctionnalités d’un langage. Au lieu de cela, la même fonctionnalité peut être fournie à une ancienne version du langage simplement en copiant/collant/adaptant le code correspondant de la nouvelle version du langage.

Par exemple, la fonction str_contains a été introduite dans PHP 8.0. La même fonction pour PHP 7.4 et les versions inférieures peut être facilement mise en œuvre comme ceci :

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

Comme le rétroportage est plus simple que la transpilation, nous devrions opter pour cette solution chaque fois que le rétroportage fait l’affaire.

En ce qui concerne la gamme entre PHP 8.0 et 7.1, nous pouvons utiliser les bibliothèques polyfill de Symfony:

Ces bibliothèques rétroportent les fonctions, classes, constantes et interfaces suivantes :

Version de PHP Caractéristiques
7.2 Fonctions :

Constantes :

7.3 Fonctions :

Exceptions :

7.4 Fonctions :
8.0 Interfaces :
  • Stringable

Classes :

  • ValueError
  • UnhandledMatchError

Constantes :

  • FILTER_VALIDATE_BOOL

Fonctions :

Exemples de PHP transpilé

Inspectons quelques exemples de code PHP transpilé, et quelques paquets qui sont entièrement transpilés.

Code PHP

L’expression match a été introduite dans PHP 8.0. Ce code source :

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

…sera transpilé dans sa version équivalente de PHP 7.4, en utilisant l’opérateur switch:

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

L’opérateur nullsafe a également été introduit dans PHP 8.0 :

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

Le code transpilé doit d’abord affecter la valeur de l’opération à une nouvelle variable, afin d’éviter d’exécuter l’opération deux fois :

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

La fonction de promotion des propriétés des constructeurs, également introduite dans PHP 8.0, permet aux développeurs d’écrire moins de code :

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

En le transpilant pour PHP 7.4, le morceau de code complet est produit :

 class QueryResolver
 {
  protected QueryFormatter $queryFormatter;

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

Le code transpilé ci-dessus contient des propriétés typées, qui ont été introduites dans PHP 7.4. En transpilant ce code en PHP 7.3, vous les remplacez par des commentaires (docblocks) :

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

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

Paquets PHP

Les bibliothèques suivantes sont en cours de transpilation pour la production :

Bibliothèque/description Code/notes
Rector
Outil de reconstruction de PHP qui rend la transpilation possible
Code source
Code transpilé
Notes
Normes de codage faciles
Outil pour que le code PHP adhère à un ensemble de règles
Code source
Code transpilé
Notes
API GraphQL pour WordPress
Extension fournissant un serveur GraphQL pour WordPress
Code source
Code transpilé
Notes

Avantages et inconvénients de la transpilation PHP

L’avantage de transpiler PHP a déjà été décrit : cela permet au code source d’utiliser PHP 8.0 (c’est-à-dire la dernière version de PHP), qui sera transformé en une version inférieure de PHP pour la production afin de fonctionner dans une application ou un environnement hérité.

Cela nous permet effectivement de devenir de meilleurs développeurs, en produisant un code de meilleure qualité. En effet, notre code source peut utiliser les types d’union de PHP 8.0, les propriétés typées de PHP 7.4 et les différents types et pseudo-types ajoutés à chaque nouvelle version de PHP(mixed de PHP 8.0, object de PHP 7.2), entre autres fonctionnalités modernes de PHP.

Grâce à ces fonctionnalités, nous pouvons mieux attraper les bogues pendant le développement et écrire du code plus facile à lire.

Maintenant, voyons les inconvénients.

Il doit être codé et entretenu

Rector peut transpiler du code automatiquement, mais le processus nécessitera probablement une saisie manuelle pour le faire fonctionner avec notre configuration spécifique.

Les bibliothèques tierces doivent aussi être transpilées

Cela devient un problème lorsque leur transpilation produit des erreurs car nous devons alors nous plonger dans leur code source pour trouver la raison possible. Si le problème peut être résolu et que le projet est open source, nous devrons envoyer une « pull request ». Si la bibliothèque n’est pas open source, nous risquons de nous heurter à un obstacle.

Rector ne nous informe pas lorsque le code ne peut pas être transpilé

Si le code source contient des attributs PHP 8.0 ou toute autre fonctionnalité qui ne peut pas être transpilée, nous ne pouvons pas continuer. Cependant, Rector ne vérifie pas cette condition, nous devons donc le faire manuellement. Ce n’est peut-être pas un gros problème concernant notre propre code source puisque nous le connaissons déjà, mais cela pourrait devenir un obstacle concernant les dépendances tierces.

Les informations de débogage utilisent le code transpilé, pas le code source

Lorsque l’application produit un message d’erreur avec une trace de pile en production, le numéro de ligne pointe vers le code transpilé. Nous devons reconvertir le code transpilé en code original pour trouver le numéro de ligne correspondant dans le code source.

Le code transpilé doit aussi être préfixé

Notre projet transpilé et une autre bibliothèque également installée dans l’environnement de production pourraient utiliser la même dépendance tierce. Cette dépendance tierce sera transpilée pour notre projet et conservera son code source original pour l’autre bibliothèque. Par conséquent, la version transpilée doit être préfixée via PHP-Scoper, Strauss ou un autre outil pour éviter les conflits potentiels.

La transpilation doit avoir lieu pendant l’intégration continue (IC)

Comme le code transpilé remplacera naturellement le code source, nous ne devons pas exécuter le processus de transpilation sur nos ordinateurs de développement, car nous risquerions de créer des effets secondaires. Exécuter le processus pendant une exécution de l’IC est plus approprié (plus d’informations à ce sujet ci-dessous).

Comment transpiler PHP

Tout d’abord, nous devons installer Rector dans notre projet de développement :

composer require rector/rector --dev

Nous créons ensuite un fichier de configuration rector.php dans le répertoire racine du projet contenant les ensembles de règles nécessaires. Pour rétrograder le code de PHP 8.0 à 7.1, nous utilisons cette configuration :

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

Pour nous assurer que le processus s’exécute comme prévu, nous pouvons exécuter la commande process de Rector en mode sec, en passant le ou les emplacements à traiter (dans ce cas, tous les fichiers du dossier src/) :

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

Pour effectuer la transpilation, nous exécutons la commande process de Rector, qui modifiera les fichiers dans leur emplacement actuel :

vendor/bin/rector process src

Remarque : si nous exécutons rector process sur nos ordinateurs de développement, le code source sera converti sur place, sous src/. Cependant, nous voulons produire le code converti dans un emplacement différent pour ne pas écraser le code source lors de la rétrogradation du code. C’est pour cette raison que l’exécution du processus convient le mieux pendant l’intégration continue.

Optimiser le processus de transpilation

Pour générer un livrable transpilé pour la production, seul le code destiné à la production doit être converti ; le code nécessaire uniquement au développement peut être ignoré. Cela signifie que nous pouvons éviter de transpiler tous les tests (pour notre projet et ses dépendances) et toutes les dépendances pour le développement.

Concernant les tests, nous savons déjà où se trouvent ceux de notre projet – par exemple, dans le répertoire tests/. Nous devons également découvrir où se trouvent ceux des dépendances – par exemple, dans leurs sous-répertoires tests/, test/ et Test/ (pour différentes bibliothèques). Ensuite, nous disons à Rector de ne pas traiter ces répertoires :

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

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

Concernant les dépendances, Composer sait lesquelles sont pour le développement (celles sous l’entrée require-dev dans composer.json) et lesquelles sont pour la production (celles sous l’entrée require). Pour récupérer auprès de Composer les chemins de toutes les dépendances pour la production, nous exécutons :

composer info --path --no-dev

Cette commande produira une liste de dépendances avec leur nom et leur chemin, comme ceci :

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

Nous pouvons extraire tous les chemins et les introduire dans la commande Rector, qui traitera ensuite le répertoire src/ de notre projet plus les répertoires contenant toutes les dépendances pour la production :

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

Une autre amélioration peut empêcher Rector de traiter les dépendances qui utilisent déjà la version PHP cible. Si une bibliothèque a été codée avec PHP 7.1 (ou toute version inférieure), il n’est pas nécessaire de la transpiler vers PHP 7.1.

Pour cela, nous pouvons obtenir la liste des bibliothèques nécessitant PHP 7.2 et plus et ne traiter que celles-là. Nous obtiendrons les noms de toutes ces bibliothèques via la commande why-not de Composer, comme ceci :

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

Comme cette commande ne fonctionne pas avec le drapeau --no-dev, pour inclure uniquement les dépendances pour la production, nous devons d’abord supprimer les dépendances pour le développement et régénérer l’autoloader, exécuter la commande, puis les ajouter à nouveau :

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

La commande info --path de Composer récupère le chemin d’accès d’un paquet, avec ce format :

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

Nous exécutons cette commande pour tous les éléments de notre liste afin d’obtenir tous les chemins d’accès à transpiler :

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

Enfin, nous fournissons cette liste à Rector (ainsi que le répertoire src/ du projet) :

vendor/bin/rector process src $paths

Pièges à éviter lorsque vous transpilez du code

Transpiler du code peut être considéré comme un art, nécessitant souvent des ajustements spécifiques au projet. Voyons quelques problèmes que nous pouvons rencontrer.

Les règles enchaînées ne sont pas toujours traitées

Une règle enchaînée, c’est lorsqu’une règle doit convertir le code produit par une règle précédente. Par exemple, la bibliothèque symfony/cache contient ce code:

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

Lors de la transpilation de PHP 7.4 à 7.3, la fonction tag doit subir deux modifications :

Le résultat final devrait être celui-ci :

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

Cependant, Rector ne produit que l’étape intermédiaire :

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

Le problème est que Rector ne peut pas toujours contrôler l’ordre dans lequel les règles sont appliquées.

La solution consiste à identifier les règles enchaînées qui n’ont pas été traitées et à lancer une nouvelle exécution de Rector pour les appliquer.

Pour identifier les règles enchaînées, nous exécutons Rector deux fois sur le code source, comme ceci :

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

La première fois, nous exécutons Rector comme prévu, pour exécuter la transpilation. La deuxième fois, nous utilisons le drapeau --dry-run pour découvrir s’il y a encore des modifications à apporter. S’il y en a, la commande sortira avec un code d’erreur et la sortie « diff » indiquera quelle(s) règle(s) peut (peuvent) encore être appliquée(s). Cela signifierait que la première exécution n’était pas complète, certaines règles enchaînées n’ayant pas été traitées.

Exécution de Rector avec le drapeau --dry-run
Exécution de Rector avec le drapeau –dry-run

Une fois que nous avons identifié la (ou les) règle(s) chaînée(s) non appliquée(s), nous pouvons alors créer un autre fichier de configuration Rector – par exemple, rector-chained-rule.php exécutera la règle manquante. Au lieu de traiter un ensemble complet de règles pour tous les fichiers sous src/, cette fois, nous pouvons exécuter la règle spécifique manquante sur le fichier spécifique où elle doit être appliquée :

// 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',
  ]);
};

Enfin, nous indiquons à Rector lors de son deuxième passage d’utiliser le nouveau fichier de configuration via l’entrée --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

Les dépendances de Composer peuvent être incohérentes

Les bibliothèques peuvent déclarer qu’une dépendance est destinée au développement (c’est-à-dire sous require-dev dans composer.json), et pourtant, référencer du code provenant d’elles pour la production (comme sur certains fichiers sous src/, et non tests/).

En général, ce n’est pas un problème car ce code ne sera pas chargé en production et il n’y aura donc jamais d’erreur sur l’application. Cependant, lorsque Rector traite le code source et ses dépendances, il valide que tout le code référencé peut être chargé. Rector lancera une erreur si un fichier fait référence à un morceau de code d’une bibliothèque non installée (parce qu’il a été déclaré nécessaire pour le développement uniquement).

Par exemple, la classe EarlyExpirationHandler du composant Cache de Symfony implémente l’interface MessageHandlerInterface du composant Messenger :

class EarlyExpirationHandler implements MessageHandlerInterface
{
    //...
}

Cependant, symfony/cache déclare que symfony/messenger est une dépendance pour le développement. Ensuite, lorsque vous exécutez Rector sur un projet qui dépend de symfony/cache, il lancera une erreur :

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

Il existe trois solutions à ce problème :

  1. Dans la configuration de Rector, ignorez le traitement du fichier qui fait référence à ce morceau de code :
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::SKIP, [
    __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php',
  ]);
};
  1. Téléchargez la bibliothèque manquante et ajoutez son chemin pour qu’elle soit chargée automatiquement par Rector :
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/vendor/symfony/messenger',
  ]);
};
  1. Faîtes en sorte que votre projet dépende de la bibliothèque manquante pour la production :
composer require symfony/messenger

Transpilation et intégration continue

Comme mentionné précédemment, dans nos ordinateurs de développement, nous devons utiliser le drapeau --dry-run lorsque nous exécutons Rector, sinon le code source sera remplacé par le code transpilé. Pour cette raison, il est plus approprié d’exécuter le processus de transpilation réel pendant l’intégration continue (IC), où nous pouvons faire tourner des lanceurs temporaires pour exécuter le processus.

Un moment idéal pour exécuter le processus de transpilation est la génération de la version de notre projet. Par exemple, le code ci-dessous est un flux de travail pour GitHub Actions, qui crée la version d’une extension 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 }}

Ce flux de travail contient une procédure standard pour publier une extension WordPress via GitHub Actions. Le nouvel ajout, qui consiste à transpiler le code du plugin de PHP 7.4 à 7.1, se produit à cette étape :

      - 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

Pris ensemble, ce flux de travail effectue maintenant les étapes suivantes :

  1. Vérifier le code source d’une extension WordPress dans son dépôt, écrit avec PHP 7.4
  2. Installer ses dépendances Composer
  3. Transpiler son code de PHP 7.4 à 7.1
  4. Modifier l’entrée « Nécessite PHP » dans l’en-tête du fichier principal de l’extension de « 7.4 » à « 7.1 »
  5. Supprimer les dépendances nécessaires au développement
  6. Créer le fichier .zip de l’extension, en excluant tous les fichiers inutiles
  7. Téléversez le fichier .zip en tant que ressource de la version (et, en plus, en tant qu’artefact sur l’action GitHub)

Tester le code transpilé

Une fois que le code a été transpilé en PHP 7.1, comment savoir s’il fonctionne bien ? Ou, en d’autres termes, comment savons-nous qu’il a été entièrement converti et qu’aucun vestige de versions supérieures du code PHP n’a été laissé derrière lui ?

Comme pour la transpilation du code, nous pouvons mettre en œuvre la solution dans le cadre d’un processus d’intégration continue. L’idée est de configurer l’environnement du lanceur avec PHP 7.1 et d’exécuter un linter sur le code transpilé. Si un morceau de code n’est pas compatible avec PHP 7.1 (comme une propriété typée de PHP 7.4 qui n’a pas été convertie), le linter lancera une erreur.

Un linter pour PHP qui fonctionne bien est PHP Parallel Lint. Nous pouvons installer cette bibliothèque en tant que dépendance pour le développement dans notre projet, ou demander au processus IC de l’installer en tant que projet Composer autonome :

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

Lorsque le code contient PHP 7.2 et plus, PHP Parallel Lint affiche une erreur comme celle-ci:

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.

Ajoutons le linter dans le flux de travail de notre CI. Les étapes à exécuter pour transpiler le code de PHP 8.0 à 7.1 et le tester sont les suivantes :

  1. Vérifier le code source
  2. Fare en sorte que l’environnement exécute PHP 8.0, afin que Rector puisse interpréter le code source
  3. Transpiler le code en PHP 7.1
  4. Installer l’outil PHP linter
  5. Passer la version PHP de l’environnement à 7.1
  6. Exécuter l’outil linter sur le code transpilé

Ce flux de travail GitHub Action fait le travail :

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

Remarquez que plusieurs fichiers bootstrap80.php des bibliothèques polyfill de Symfony (qui n’ont pas besoin d’être transpilés) doivent être exclus du linter. Ces fichiers contiennent PHP 8.0, le linter enverrait donc des erreurs lors de leur traitement. Cependant, l’exclusion de ces fichiers est sûre car ils ne seront chargés en production que si PHP 8.0 ou plus est exécuté:

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

Résumé

Cet article nous a appris à transpiler notre code PHP, ce qui nous permet d’utiliser PHP 8.0 dans le code source et de créer une version qui fonctionne sur PHP 7.1. La transpilation se fait via Rector, un outil de reconstruction de PHP.

Transpiler notre code fait de nous de meilleurs développeurs puisque nous pouvons mieux attraper les bogues en cours de développement et produire un code naturellement plus facile à lire et à comprendre.

Transpiler nous permet aussi de découpler notre code des exigences PHP spécifiques du CMS. Nous pouvons désormais le faire si nous souhaitons utiliser la dernière version de PHP pour créer une extension WordPress ou un module Drupal accessible au public sans restreindre sévèrement notre base d’utilisateurs.

Vous avez encore des questions sur la transpilation PHP ? Faîtes-nous en part dans la section des commentaires !

Leonardo Losoviz

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