Moderniser son application legacy PHP

5 septembre 2019

Je travaille depuis plus de 8 ans sur un produit dont la partie web est développée en PHP. Lorsque je suis arrivé, nous étions en PHP 5.2 et le code était très procédurale. Au fil des années, nous avons fait évoluer le produit : mise en place d'un développement plus orientée objet, passage en PHP 5.3. Certains points techniques nous bloquaient pour monter en version supérieure à PHP 5.3. Nous nous retrouvions donc assez rapidement dans l'impossibilité d'utiliser bons nombres de modules et librairies développés par la communauté. Et avec la fin de vie de PHP 5.3 puis l'arrivée de PHP 7.0, il nous fallait absolument pouvoir résoudre ces problèmes.

Les bloqueurs

Pour pouvoir être compatible avec PHP 5.4+, nous devions trouver un moyen :

  • d'émuler l'activation des magic quotes : le produit était développé dès le début avec une fausse bonne directive PHP activée : les magic quotes.
  • de redéfinir les fonctions mysql_* qui ont été supprimées : nous utilisions cette extension pour toutes nos requêtes en base de données. Mais elle a été rendue obsolète en PHP 5.5.0 puis a été supprimée en PHP 7.0.

La solution

La seule solution qui nous est venue à l'esprit au début était de repasser sur l'intégralité du code et de corriger ce qui nous bloquait. Mais autant dire que la charge en termes de développements et de tests de non-régression était énorme et même pas envisageable ! Et en cherchant sur le net, un de mes collègues a trouvé une directive PHP qui allait résoudre tous ces problèmes.

L'auto prepend de PHP

PHP propose une directive qui permet de charger un fichier automatiquement avant chaque exécution d'un script PHP : auto_prepend_file C'est grace à cette directive que nous allons pouvoir contourner tous nos problèmes de compatibilité et monter de version PHP.

Ce que peut nous permettre l'auto_prepend_file

Fichier composer.json

{
    "autoload": {
        "psr-4": {"MyProject\\": "src/"}
    }
}

et notre polyfill :

<?php

require_once(__DIR__.'/../vendor/autoload.php');

Ayant désormais utiliser composer, nous pouvons inclure cette librairie en exécutant la commande suivante à la racine de notre projet :

composer require yidas/magic-quotes

Aperçu de notre polyfill

<?php

require_once(__DIR__.'/../vendor/autoload.php');

MagicQuotesGpc::init();
  • Si vous avez un fichier de configuration de l'application que vous incluez sur toutes les pages, vous pouvez l'inclure ici
<?php

require_once(__DIR__.'/../vendor/autoload.php');

MagicQuotesGpc::init();

require_once(__DIR__.'/config.php');
  • Redéfinition des méthodes mysql_* : nous pouvons redéfinir toutes les méthodes existantes en utilisant un driver encore disponible (mysqli ou PDO)

Exemple (la variable $link correspond à une ressource retournée par mysqli_connect):

function mysql_query($query, $link)
{
    return mysqli_query($link, $query);
}
<?php

require_once(__DIR__.'/../vendor/autoload.php');

MagicQuotesGpc::init();

// Toutes les fonctions mysql_* peuvent être redéclarées dans un fichier à inclure ici
function mysql_query($query, $link)
{
    return mysqli_query($link, $query);
}

Nous avons désormais résolu tout nos problèmes de compatibilité. Une fois validé, nous pouvons monter en version de PHP (jusqu'à 7.1 dans notre cas à ce moment-là) et moderniser encore notre legacy.

A nous le web !!

Si vous utilisez composer (et je vous le recommande fortement), vous pouvez désormais importer et utiliser toutes les librairies présentes sur le net. Quelques exemples :

  • Un module d'injection de dépendances. Il en existe plusieurs, pour l'exemple ici j'utilise celui de symfony
composer require symfony/dependency-injection

Vous pouvez désormais définir toutes vos dépendances dans un fichier services.yml

Aperçu de notre polyfill

<?php

require_once(__DIR__.'/../vendor/autoload.php');

MagicQuotesGpc::init();

// Toutes les fonctions mysql_* peuvent être redéclarées dans un fichier à inclure ici
function mysql_query($query, $link)
{
    return mysqli_query($link, $query);
}

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator;

$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/..'));
$loader->load('services.yml');

Nous pouvons donc utiliser notre variable $container dans tous nos fichiers de notre projet legacy.

  • Guzzle pour les requêtes HTTP : plus besoin d'écrire à la main ses requêtes HTTP avec curl.

  • Ramsey/UUID pour générer des UUID.

Et bien d'autres...

Mise en place sur un projet web

Afin que cette clause soit prise en compte, elle peut être appliquée :

  • globalement dans le fichier de configuration php.ini de votre serveur
  • localement (depuis PHP 5.3) en définissant un fichier .user.ini à la racine de votre projet (exemple : /var/www/my_project/.user.ini)
auto_prepend_file = /var/www/my_project/polyfill-php-compatibility.php

Le fichier polyfill-php-compatibility.php sera donc exécuté avant tout autre fichier PHP situé dans le projet my_project

Ligne de commande

Il est possible de spécifier cette directive lors de l'exécution en ligne de commande d'un script php

php -d auto_prepend_file="/var/www/my_project/polyfill-php-compatibility.php" mon_script.php

J'espère que cet article vous donnera des idées pour moderniser vos vieux projets PHP. Et si vous avez des questions, contactez-moi sur twitter.


Ressources

Cet article t'a plu ? Si oui, je te propose de t'inscrire à ma dev letter pour recevoir régulièrement dans ta boîte mail mes conseils, mes nouveaux articles, des vidéos à voir, des outils à découvrir et encore bien d’autres choses.

Je m'inscris