Wordpress

Le guide ultime pour transpiler du code PHP

Dans des circonstances idéales, nous devrions utiliser PHP 8.0 (la dernière version au moment d'écrire ceci) 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 PHP précédentes, par exemple lors de la création d'un plugin public pour WordPress ou de l'utilisation de code hérité qui empêche la mise à niveau 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 précédente de PHP — même vers PHP 7.1.

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

Qu'est-ce que la transpilation ?

Le transpilage convertit le code source d'un langage de programmation en un code source équivalent du même ou d'un langage de programmation différent.

Le transpilage n'est pas un nouveau concept dans le développement Web : les développeurs côté client connaissent très 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 navigateurs plus anciens. Par exemple, étant donné une fonction de flèche ES2015 :

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

… Babel va le convertir dans sa version ES5 :

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

Qu'est-ce que Transpiler PHP ?

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

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

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é dans sa version PHP 7.3 équivalente :

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

Les fonctions fléchées peuvent être transpilées car elles sont du sucre syntaxique, c'est-à-dire une nouvelle syntaxe pour produire un comportement existant. C'est le fruit à portée de main.

Cependant, il existe également de nouvelles fonctionnalités qui créent un nouveau comportement, et en tant que tel, il n'y aura pas de code équivalent pour les versions précédentes de PHP. C'est le cas des types union, introduits en PHP 8.0 :

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

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

Les types d'union en sont un exemple. 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 déjà une erreur en développement, et nous devons l'attraper et la corriger avant que le code n'atteigne la production.

Par conséquent, nous pouvons nous permettre de supprimer la fonctionnalité du code pour la production :

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

Si l'erreur persiste en production, le message d'erreur émis sera moins précis que si nous avions des types union. Cependant, cet inconvénient potentiel est compensé par la possibilité d'utiliser des types d'union en premier lieu.

Dans un monde parfait, nous devrions pouvoir utiliser PHP 8.0 sur tous nos sites et le mettre à jour dès qu'une nouvelle version sort 😌 Mais ce n'est pas toujours le cas. Apprenez tout ce que vous devez savoir sur la transpilation du code PHP ici 👇Click to Tweet

Avantages de la transpilation du code PHP

Le transpilage permet de coder une application à l'aide de la dernière version de PHP et de produire une version qui fonctionne également dans des environnements exécutant d'anciennes versions de PHP.

Cela peut être particulièrement utile pour les développeurs créant des produits pour les systèmes de gestion de contenu (CMS) hérités. 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 PHP 5.6 à 7.2 - qui sont tous en fin de vie (EOL), ce qui signifie qu'ils ne reçoivent plus de mises à jour de sécurité - s'élève à 34.8%, et ceux qui fonctionnent sur n'importe quelle version PHP autre que 8.0 représente un énorme 99.5 % :

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

Par conséquent, les thèmes et plugins 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 au transpiling, ceux-ci ont pu être codés avec PHP 8.0, et toujours être 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 PHP autre que la plus récente (même dans la plage 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 au transpiling, les développeurs peuvent créer des modules Drupal accessibles au public à l'aide de 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 leurs environnements pour une raison ou une autre. Néanmoins, grâce au transpiling, les développeurs peuvent toujours coder leurs livrables à l'aide de PHP 8.0 et les exécuter sur ces environnements hérités.

Quand transpiler PHP

Le code PHP peut toujours être transpilé à moins qu'il ne contienne une fonctionnalité PHP qui n'a pas d'équivalent dans la version précédente de PHP.

C'est peut-être le cas avec les attributs, introduits dans PHP 8.0 :

#[SomeAttr]
function someFunc() {}

#[AnotherAttr]
class SomeClass {}

Dans l'exemple précédent utilisant des fonctions fléchées, le code pouvait être transpilé car les fonctions fléchées sont du sucre syntaxique. Les attributs, en revanche, créent un comportement complètement nouveau. Ce comportement pourrait également être reproduit avec PHP 7.4 et inférieur, mais uniquement en le codant manuellement, c'est-à-dire pas automatiquement basé sur un outil ou un processus (l'IA pourrait apporter une solution, mais nous n'en sommes pas encore là).

Attributs destinés au développement, tels que #[Deprecated], peuvent être supprimés de la même manière que les types d'union sont supprimés. 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.

A ce jour, aucun transpileur ne peut prendre du code avec des attributs PHP 8.0 et produire automatiquement son code PHP 7.4 équivalent. Par conséquent, si votre code PHP doit utiliser des attributs, alors le transpiler sera difficile ou irréalisable.

Fonctionnalités PHP pouvant être transpilées

Ce sont les fonctionnalités de PHP 7.1 et supérieures 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 échecs.

Version de PHPEn vedette
7.1Tout
7.2- object type
– élargissement du type de paramètre
- PREG_UNMATCHED_AS_NULL drapeau dans preg_match
7.3– Missions de référence dans list() / déstructuration du tableau (Sauf à l'intérieur foreach — #4376)
– Syntaxe flexible Heredoc et Nowdoc
– Les 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 de fusion nulle
– Déballage à l'intérieur des baies
– Séparateur de littéral numérique
- strip_tags() avec tableau de noms de balises
– types de retour covariants et types de paramètres contravariants
8.0– Types de syndicats
- mixed pseudo-type
- static type de retour
- ::class constante magique sur les objets
- match expressions
- catch exceptions uniquement par type
– Opérateur à sécurité nulle
– Promotion immobilière de classe constructeur
– Les virgules de fin dans les listes de paramètres et la fermeture use listes

Transpileurs PHP

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

Rector est un outil de reconstruction PHP, qui convertit le code PHP en fonction de règles programmables. Nous saisissons le code source et l'ensemble de règles à exécuter, et Rector transformera le code.

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

sortie "diff" du recteur
sortie « diff » du recteur

Quelle version de PHP transpiler vers

Pour transpiler le code entre les versions PHP, les règles correspondantes doivent être créées.

Aujourd'hui, la bibliothèque Rector comprend la plupart des règles de transpilation du code dans la plage de PHP 8.0 à 7.1. Par conséquent, nous pouvons transpiler de manière fiable notre code PHP jusqu'à la version 7.1.

Il existe également des règles de transpilation de PHP 7.1 à 7.0 et de 7.0 à 5.6, mais celles-ci ne sont pas exhaustives. Des travaux sont en cours pour les compléter, nous pourrions donc éventuellement transpiler le code PHP vers la version 5.6.

Transpilation vs rétroportage

Le rétroportage est similaire au transpilage, mais plus simple. Le rétroportage du 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 de la langue simplement en copiant/collant/en adaptant le code correspondant à partir de la nouvelle version de la langue.

Par exemple, la fonction str_contains a été introduit en PHP 8.0. La même fonction pour PHP 7.4 et ci-dessous peut être facilement implémentée 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;
    }
  }
}

Parce que le rétroportage est plus simple que le transpilage, nous devrions opter pour cette solution chaque fois que le rétroportage fait le travail.

Concernant la plage entre PHP 8.0 et 7.1, on peut utiliser les librairies polyfill de Symfony :

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

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

Version de PHPEn vedette
7.2Les fonctions:

  • spl_object_id
  • utf8_encode
  • utf8_decode

Constantes :

  • PHP_FLOAT_*
  • PHP_OS_FAMILY
7.3Les fonctions:

  • array_key_first
  • array_key_last
  • hrtime
  • is_countable

Des exceptions:

  • JsonException
7.4Les fonctions:

  • get_mangled_object_vars
  • mb_str_split
  • password_algos
8.0Interfaces:

  • Stringable

Des cours:

  • ValueError
  • UnhandledMatchError

Constantes :

  • FILTER_VALIDATE_BOOL

Les fonctions:

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

Exemples de PHP transpilé

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

Code PHP

La match expression 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 PHP 7.4 équivalente, en utilisant le switch opérateur:

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 fonctionnalité de promotion de propriété de constructeur, également introduite dans PHP 8.0, permet aux développeurs d'écrire moins de code :

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

Lors de la transpilation 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. Transpiler ce code vers PHP 7.3 les remplace par des docblocks :

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

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

Paquets PHP

Les bibliothèques suivantes sont transpilées pour la production :

Bibliothèque/descriptionCode/remarques
Recteur
Outil de reconstruction PHP qui rend le transpilation possible
- Code source
– Code transpilé
- Remarques
Normes de codage faciles
Outil pour que le code PHP adhère à un ensemble de règles
- Code source
– Code transpilé
- Remarques
API GraphQL pour WordPress
Plugin fournissant un serveur GraphQL pour WordPress
- Code source
– Code transpilé
- Remarques

Avantages et inconvénients de transpiler PHP

L'avantage de transpiler PHP a déjà été décrit : il 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 pour PHP pour que la production s'exécute dans une application ou un environnement hérité.

Cela nous permet effectivement de devenir de meilleurs développeurs, produisant du 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 à partir de PHP 8.0, object de PHP 7.2), parmi d'autres fonctionnalités modernes de PHP.

En utilisant ces fonctionnalités, nous pouvons mieux détecter les bogues pendant le développement et écrire un code plus facile à lire.

Voyons maintenant les inconvénients.

Il doit être codé et maintenu

Rector peut transpiler le 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 également être transpilées

Cela devient un problème chaque fois que leur transpilation produit des erreurs, car nous devons ensuite fouiller dans leur code source pour en découvrir la raison possible. Si le problème peut être résolu et que le projet est open source, nous devrons soumettre une pull request. Si la bibliothèque n'est pas open source, nous pouvons rencontrer un barrage routier.

Le recteur 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érifiera 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 également ê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 d'origine 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.

Le transpilage doit avoir lieu pendant l'intégration continue (CI)

Étant donné que le code transpilé remplacera naturellement le code source, nous ne devons pas exécuter le processus de transpilation sur nos ordinateurs de développement, ou nous risquons de créer des effets secondaires. L'exécution du processus pendant une exécution CI est plus appropriée (plus de détails 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 rector.php de configuration dans le répertoire racine du projet contenant les ensembles de règles requis. Pour rétrograder le code de PHP 8.0 à 7.1, nous utilisons cette configuration :

use RectorSetValueObjectDowngradeSetList;
use SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;

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 Rector's process commande en mode sec, en passant le(s) emplacement(s) à traiter (dans ce cas, tous les fichiers du dossier src/):

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

Pour effectuer le transpilage, nous exécutons le process commande, qui modifiera les fichiers dans leur emplacement existant :

vendor/bin/rector process src

Veuillez noter : si nous courons rector process dans nos postes de développement, le code source sera converti sur place, sous src/. Cependant, nous souhaitons produire le code converti à un emplacement différent pour ne pas écraser le code source lors de la rétrogradation du code. Pour cette raison, l'exécution du processus est la plus appropriée pendant l'intégration continue.

Optimiser le processus de transpilation

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

Concernant les tests, nous saurons déjà où se trouvent ceux de notre projet — par exemple, sous le dossier tests/. Nous devons également découvrir où se trouvent ceux des dépendances - par exemple, sous leurs sous-dossiers tests/, test/ et Test/ (pour différentes bibliothèques). Ensuite, nous demandons à Rector d'ignorer le traitement de ces dossiers :

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

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

Concernant les dépendances, Composer sait lesquelles sont destinées au développement (celles sous l'entrée require-dev in composer.json) et lesquels sont destinés à la production (ceux sous l'entrée require).

Pour récupérer depuis 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 alimenter dans la commande Rector, qui traitera ensuite les src/ dossier plus les dossiers 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 ces dépendances utilisant déjà la version PHP cible. Si une bibliothèque a été codée avec PHP 7.1 (ou toute version ci-dessous), alors il n'est pas nécessaire de la transpiler en PHP 7.1.

Pour y parvenir, nous pouvons obtenir la liste des bibliothèques nécessitant PHP 7.2 et supérieur et ne traiter que celles-ci. Nous obtiendrons les noms de toutes ces bibliothèques via Composer's why-not commande, comme ceci:

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

Parce que cette commande ne fonctionne pas avec le --no-dev flag, pour n'inclure que les dépendances pour la production, nous devons d'abord supprimer les dépendances pour le développement et régénérer l'autochargeur, 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

Compositeur info --path La commande récupère le chemin d'un package, 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 pour obtenir tous les chemins à transpiler :

Besoin d'une solution d'hébergement qui vous donne un avantage concurrentiel ? Behmastervous offre une vitesse incroyable, une sécurité de pointe et une mise à l'échelle automatique. Découvrez nos forfaits

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

Enfin, nous fournissons cette liste au recteur (plus les src/ dossier):

vendor/bin/rector process src $paths

Pièges à éviter lors de la transpilation de 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 chaînées ne sont pas toujours traitées

Une règle chaînée 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 type de retour ItemInterface doit d'abord être converti en self, en raison de la règle DowngradeCovariantReturnTypeRector
  • Le type de retour self doit alors être retiré, en raison de la règle DowngradeSelfTypeDeclarationRector

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

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

Cependant, Rector ne sort que l'étage intermédiaire :

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

Le problème est que le recteur 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 à exécuter une nouvelle exécution de Rector pour les appliquer.

Pour identifier les règles chaî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 le transpilage. La deuxième fois, nous utilisons le --dry-run flag pour découvrir s'il y a encore des changements à faire. S'il y en a, la commande se terminera avec un code d'erreur et la sortie « diff » indiquera quelle(s) règle(s) peuvent encore être appliquées. Cela signifierait que la première exécution n'était pas terminée, certaines règles chaînées n'étant pas traitées.

Recteur en cours d'exécution avec indicateur --dry-run
Recteur en cours d'exécution avec indicateur de marche à sec

Une fois que nous avons identifié la ou les règles enchaînées non appliquées, 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 manquante spécifique sur le fichier spécifique où elle doit être appliquée :

// rector-chained-rule.php
use RectorCoreConfigurationOption;
use RectorDowngradePhp74RectorClassMethodDowngradeSelfTypeDeclarationRector;
use SymfonyComponentDependencyInjectionLoaderConfiguratorContainerConfigurator;

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 disons à 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 du compositeur peuvent être incohérentes

Les bibliothèques pourraient déclarer une dépendance à développer (c'est-à-dire sous require-dev in composer.json), tout en faisant référence à un certain code d'eux pour la production (comme sur certains fichiers sous src/, Pas tests/).

Habituellement, ce n'est pas un problème car ce code peut ne pas être chargé en production, 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 renverra une erreur si un fichier fait référence à un morceau de code d'une bibliothèque non installée (car 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 depuis le composant Messenger :

class EarlyExpirationHandler implements MessageHandlerInterface
{
    //...
}

Toutefois, symfony/cache déclare symfony/messenger être une dépendance pour le développement. Ensuite, lors de l'exécution de Rector sur un projet qui dépend de symfony/cache, cela renverra une erreur :

[ERROR] Could not process "vendor/symfony/cache/Messenger/EarlyExpirationHandler.php" file, due to:             
  "Analyze error: "Class SymfonyComponentMessengerHandlerMessageHandlerInterface 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 du recteur, 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 à charger automatiquement par Rector :
return static function (ContainerConfigurator $containerConfigurator): void {
  // ...

  $parameters->set(Option::AUTOLOAD_PATHS, [
    __DIR__ . '/vendor/symfony/messenger',
  ]);
};
  1. Faites dépendre votre projet 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 --dry-run flag lors de l'exécution de Rector, ou 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 (CI), où nous pouvons lancer des coureurs temporaires pour exécuter le processus.

Le moment idéal pour exécuter le processus de transpilation est lors de la génération de la version pour notre projet. Par exemple, le code ci-dessous est un workflow pour GitHub Actions, qui crée la sortie d'un plugin WordPress :

name: Generate Installable Plugin and Upload as Release Asset
on:
  release:
    types: [published]
jobs:
  build:
    name: Build, Downgrade and Upload Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Downgrade code for production (to PHP 7.1)
        run: |
          composer install
          vendor/bin/rector process
          sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php
      - name: Build project for production
        run: |
          composer install --no-dev --optimize-autoloader
          mkdir build
      - name: Create artifact
        uses: montudor/action-zip@v0.1.0
        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 workflow contient une procédure standard pour publier un plugin WordPress via GitHub Actions. Le nouvel ajout, pour transpiler le code du plugin de PHP 7.4 à 7.1, se produit dans 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

Ensemble, ce flux de travail exécute désormais les étapes suivantes :

  1. Extrait le code source d'un plugin WordPress à partir de son référentiel, écrit avec PHP 7.4
  2. Installe ses dépendances Composer
  3. Transpile son code de PHP 7.4 à 7.1
  4. Modifie l'entrée « Nécessite PHP » dans l'en-tête du fichier principal du plugin à partir de "7.4" à "7.1"
  5. Supprime les dépendances nécessaires au développement
  6. Crée le fichier .zip du plugin, excluant tous les fichiers inutiles
  7. Télécharge le fichier .zip en tant qu'actif de version (et, en plus, en tant qu'artefact vers l'action GitHub)

Test du code transpilé

Une fois le code transpilé en PHP 7.1, comment sait-on qu'il fonctionne bien ? Ou, en d'autres termes, comment savons-nous qu'il a été complètement converti et qu'aucun reste de versions supérieures de code PHP n'a été laissé pour compte ?

Semblable à la transpilation du code, nous pouvons implémenter la solution dans un processus CI. L'idée est de configurer l'environnement du coureur 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), alors le linter renverra 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 CI de l'installer en tant que projet Composer autonome :

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

Chaque fois que le code contient PHP 7.2 et supérieur, PHP Parallel Lint renvoie 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 :

  1. Consultez le code source
  2. Faites 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. Basculer la version PHP de l'environnement en 7.1
  6. Exécutez le linter sur le code transpilé

Ce workflow d'action GitHub 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

Veuillez noter que plusieurs bootstrap80.php les fichiers 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, donc le linter renverrait des erreurs lors de leur traitement. Cependant, l'exclusion de ces fichiers est sûre car ils ne seront chargés en production que lors de l'exécution de PHP 8.0 ou supérieur :

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

Que vous créiez un plugin public pour WordPress ou que vous mettiez à jour du code hérité, il existe de nombreuses raisons pour lesquelles l'utilisation de la dernière version de PHP peut être impossible 👩‍💻 Découvrez comment la transpilation peut vous aider dans ce guide 👇Click to Tweet

Résumé

Cet article nous a appris comment transpiler notre code PHP, nous permettant d'utiliser PHP 8.0 dans le code source et de créer une version qui fonctionne sur PHP 7.1. Le transpilage se fait via Rector, un outil de reconstruction PHP.

Transpiler notre code fait de nous de meilleurs développeurs car nous pouvons mieux détecter les bogues dans le développement et produire un code naturellement plus facile à lire et à comprendre.

Le transpilage nous permet également de découpler notre code avec les exigences PHP spécifiques du CMS. Nous pouvons maintenant le faire si nous souhaitons utiliser la dernière version de PHP pour créer un plugin WordPress ou un module Drupal accessible au public sans restreindre sévèrement notre base d'utilisateurs.

Avez-vous des questions sur la transpilation de PHP ? Faites le nous savoir dans la section "Commentaires!

Articles Relatifs

0 Commentaires
Commentaires en ligne
Voir tous les commentaires
Retour à bouton en haut