Moderniser une application PHP legacy sans tout réécrire : la méthode pas à pas
Aurélien Piskorski — dev senior PHP/Symfony
Sommaire
- Pourquoi la réécriture complète échoue presque toujours
- Étape 0 — L’audit honnête (avant de toucher au code)
- Étape 1 — Poser le filet : des tests sur les parcours critiques
- Étape 2 — Migrer l’infrastructure avant le code
- Étape 3 — La montée de version PHP, par paliers
- Étape 4 (optionnelle) — Introduire un framework sans tout casser
- Ce que ça donne, concrètement
- Par où commencer chez vous
Votre application tourne. Elle fait vivre la boîte. Elle est aussi en PHP 5.6, sans tests, et le dernier dev qui la comprenait vraiment est parti il y a trois ans. Vous connaissez la suite du discours commercial habituel : « il faut tout réécrire ». Deux ans de projet, un budget à six chiffres, et pendant ce temps, l’ancienne appli continue de vieillir.
J’ai passé quatre ans à faire l’inverse chez un éditeur de logiciels : migrer une plateforme critique de PHP 5.6 / MySQL 5.5 vers PHP 8.2 / MySQL 8, en production, sans interruption de service. Voici la méthode, étape par étape — celle que j’appliquerais sur n’importe quelle application legacy à fort enjeu.
Pourquoi la réécriture complète échoue presque toujours
Le « big bang » a un défaut structurel : pendant toute la durée de la réécriture, il faut maintenir deux systèmes. L’ancien continue d’évoluer (le métier n’attend pas), le nouveau court après une cible mobile. Résultat classique : au bout de 18 mois, le nouveau système couvre 60 % du périmètre d’il y a 18 mois.
La réécriture se justifie dans un seul cas : quand le métier lui-même change au point que l’ancien modèle de données n’a plus de sens. Sinon, la migration progressive gagne à tous les coups — elle livre de la valeur chaque semaine au lieu de tout miser sur un lancement.
Étape 0 — L’audit honnête (avant de toucher au code)
Avant toute chose, trois inventaires :
- Le périmètre réel : quelles URL sont encore appelées ? Les logs d’accès sur 3 mois révèlent presque toujours que 30 à 50 % du code n’est plus utilisé. Ce code-là ne se migre pas — il se supprime.
- Les points de terreur : les modules que personne n’ose toucher. Ce sont eux qui dictent l’ordre de migration — pas en premier (trop risqué sans filet), pas en dernier (le filet doit être posé avant).
- L’infrastructure : versions de PHP/MySQL disponibles, possibilité de faire tourner deux versions côte à côte, état des sauvegardes. Une migration sans possibilité de retour arrière n’est pas une migration, c’est un saut sans parachute.
Étape 1 — Poser le filet : des tests sur les parcours critiques
Pas une couverture à 80 % — c’est illusoire sur du legacy. Des tests de caractérisation sur les 10 à 20 parcours qui font vivre la boîte : la réservation, la facturation, l’inscription. On ne teste pas que le code est « bien écrit », on fige le comportement observable : telle requête donne telle réponse. C’est le contrat qui autorisera tout le reste.
Sur une application sans architecture testable, les tests de bout en bout (HTTP → base de données) sont souvent les seuls possibles au début. C’est très bien : ils sont lents mais ils protègent vraiment.
Étape 2 — Migrer l’infrastructure avant le code
Contre-intuitif mais décisif : la première montée de version n’est pas celle de PHP, c’est celle de l’environnement.
- Containeriser l’existant tel quel (Docker) : l’application PHP 5.6 dans un conteneur, reproductible sur tous les postes et en CI. Fini le « ça marche sur le serveur mais pas chez moi ».
- Mettre en place la CI/CD sur l’existant : chaque commit passe les tests de caractérisation. Le déploiement devient un non-événement — condition indispensable pour déployer souvent, et donc migrer par petits pas.
- MySQL d’abord, en double lecture : la montée de MySQL 5.5 → 8 se prépare en répliquant vers la nouvelle version et en comparant les comportements (les changements silencieux de
sql_modeentre 5.5 et 8 sont un piège classique : dates zéro,GROUP BYpermissifs, troncatures silencieuses…).
Étape 3 — La montée de version PHP, par paliers
On ne saute pas de 5.6 à 8.2. On traverse : 5.6 → 7.0 → 7.4 → 8.1 → 8.2. Chaque palier a ses casses connues (mysql_* supprimé en 7.0, mots réservés, each() en 7.2, moteur d’erreurs de 8.0…).
Deux outils font 70 % du travail mécanique :
- L’analyse statique (PHPStan, niveau 0 puis montée progressive) révèle les incompatibilités avant l’exécution.
- Rector automatise les transformations répétitives : remplacement des appels dépréciés, modernisation de syntaxe. Sur des centaines de fichiers, c’est la différence entre trois semaines et trois mois.
Le reste — les 30 % restants — c’est du jugement humain : le code qui reposait sur un comportement non documenté, les extensions abandonnées à remplacer, les dépendances mortes à réécrire.
Règle absolue : un palier = déployé en production = observé une à deux semaines avant le suivant. C’est ce rythme qui garantit le zéro interruption : chaque étape est petite, réversible, et validée par le trafic réel.
Étape 4 (optionnelle) — Introduire un framework sans tout casser
Sur la plateforme dont je parle, on s’est arrêtés là : le code corrigé, testé et à jour couvrait le besoin — introduire un framework n’aurait rien apporté d’autre que du risque. La modernisation n’est pas une conversion religieuse : l’objectif, c’est une base saine, pas un logo.
Mais quand un framework s’impose vraiment — équipe qui grossit, produit qui repart en croissance, recrutement — on n’« installe » pas Symfony dans le legacy : on fait passer les nouvelles requêtes par Symfony, qui délègue à l’ancien code tout ce qu’il ne sait pas encore faire. C’est le pattern strangler fig :
- Symfony devient le point d’entrée HTTP. Une route inconnue ? Il transmet au legacy, qui répond comme avant. Zéro régression le jour 1.
- Chaque nouvelle fonctionnalité s’écrit côté Symfony — le legacy arrête de grossir.
- Les modules existants migrent un par un, en commençant par ceux que l’étape 0 a identifiés. Les tests de caractérisation valident que le comportement ne bouge pas.
- Le jour où plus aucune route ne passe par l’ancien routeur, on supprime — et personne ne s’en aperçoit. C’est le but.
Ce que ça donne, concrètement
Sur la plateforme dont je parle : quatre ans de migration continue pendant que le produit continuait d’évoluer — les nouvelles fonctionnalités n’ont jamais attendu la fin de la migration. À l’arrivée : PHP 8.2, MySQL 8, des conteneurs, une CI, des déploiements sereins. Et zéro minute d’interruption imputable à la migration.
Le coût n’est pas nul — c’est un investissement étalé, typiquement 20 à 30 % de la capacité de dev sur la période. Mais il se compare à l’alternative : une réécriture qui gèle le produit, ou un statu quo dont le prix (sécurité, recrutement impossible sur PHP 5, dépendance à un seul cerveau) augmente chaque année en silence.
Par où commencer chez vous
Si votre application coche « critique + vieille + pas de tests », l’ordre est toujours le même : audit du périmètre réel, tests de caractérisation sur les parcours vitaux, containerisation de l’existant, CI, puis les paliers. Les deux premières étapes tiennent en quelques semaines et ne modifient pas une ligne de logique métier — c’est le point de départ sans risque.
C’est exactement le type de mission que je mène en freelance. Si vous voulez un avis sur votre situation, 30 minutes suffisent pour un premier diagnostic honnête.