YOUR DIGITAL HUB
← Retour au blog

Migrer un legacy PHP 5.6 vers 8.3 : méthodologie strangler-pattern éprouvée

· 10 min de lecture
Visuel de couverture — Migration PHP 5.6 vers 8.3 avec le strangler pattern

Le contexte : pourquoi PHP 5.6 est un danger en 2026

PHP 5.6 est en fin de vie officielle depuis janvier 2019. Plus de sept ans sans patchs de sécurité. Pourtant, en 2026, nous auditons encore régulièrement des applications métier critiques tournant sur ce socle : ERP internes, plateformes B2B, back-offices de grands comptes. Le code fonctionne, l'équipe a d'autres priorités, le DSI repousse. Jusqu'au jour où une CVE sur zlib, ICU ou openssl oblige à bouger en urgence.

Le problème ne se limite pas à la sécurité. PHP 5.6 n'a ni typage scalaire strict, ni property types, ni attributes, ni enums, ni readonly. Les équipes contemporaines refusent d'y travailler, les juniors n'apprennent plus la syntaxe, et les bibliothèques modernes ont toutes abandonné ce support. Doctrine 3, Symfony 7, PHPUnit 11, Composer 2.8 : tout exige PHP 8.1 au minimum.

En 2026, le standard de marché est PHP 8.3 (support sécurité jusqu'en décembre 2027) ou PHP 8.4 (jusqu'en décembre 2028). Migrer n'est plus un projet optionnel.

Panorama des trois options de migration

Face à un legacy PHP 5.6, trois stratégies existent. Chacune a un profil de risque, un coût et un délai distincts.

Big bang

Tout réécrire sur une nouvelle branche, basculer en une fois. Séduisant pour un dirigeant pressé. Mais le big bang concentre tous les risques sur une seule date. Les régressions non détectées en test se révèlent en production, avec l'impossibilité de revenir en arrière rapidement. Sur une base de 200 000 lignes, nous avons vu des big bangs provoquer plusieurs semaines d'instabilité. Notre retour de terrain : le big bang ne se justifie que sur des applications petites (moins de 20 000 lignes), bien testées (couverture supérieure à 80%) et avec un trafic faible.

Migration incrémentale in-place

Mettre à jour PHP dans l'environnement actuel, bump de version progressif (5.6 → 7.0 → 7.4 → 8.0 → 8.3). Le code doit rester compatible à chaque palier. Cette approche suppose un code initial propre, peu d'extensions obsolètes, et une équipe disponible pour corriger les incompatibilités palier par palier. Elle se casse souvent entre 5.6 et 7.0 (suppression des fonctions mysql_*, ereg_*, modification de la résolution des constantes), puis entre 7.4 et 8.0 (suppression de each, create_function, changement du passage par référence implicite).

Strangler pattern

Construire le nouveau à côté de l'ancien, derrière une façade unique. Router le trafic module par module vers le nouveau code. L'ancien et le nouveau cohabitent pendant plusieurs mois, voire années. Stuart Halloway a popularisé le pattern en 2004 pour les migrations Java, Martin Fowler en a théorisé les bénéfices. C'est aujourd'hui la référence sur les migrations PHP majeures.

Pourquoi le strangler pattern gagne à chaque fois

Sur 11 migrations majeures menées par notre équipe ces 5 dernières années, le strangler pattern a été retenu 9 fois, avec zéro incident majeur en production. Les deux big bangs concernaient des applications de moins de 15 000 lignes.

Les avantages structurels du strangler pattern :

  • Rollback trivial. La façade route le trafic. Un flag, et on revient à l'ancien code sur un module donné en 30 secondes.
  • Livraisons continues. Pas de freeze fonctionnel de plusieurs mois. L'équipe métier peut continuer à demander des évolutions, elles sont implémentées sur le nouveau code.
  • Tests rétrocompatibles. Les tests Behat de non-régression sont rejoués identiques sur l'ancien et le nouveau code. Le contrat de comportement est maintenu.
  • Montée en compétence progressive. L'équipe absorbe le nouveau stack module par module, sans submersion cognitive.
  • Budget étalé. La migration se chiffre par lots de 3 à 8 semaines, ce qui s'intègre bien dans un budget annuel.

Plan en cinq phases

Notre méthodologie strangler pattern pour PHP se découpe systématiquement en cinq phases. Les durées indiquées sont des médianes observées sur 9 missions, pour une base de 150 000 à 400 000 lignes.

Phase 1 — Audit (3 à 6 semaines)

Avant de toucher une ligne, nous cartographions :

  • Inventaire exhaustif des modules, bundles, controllers, entities.
  • Graphe de dépendances via Deptrac, identification des cycles et des couplages forts.
  • Analyse statique sur le code 5.6 avec PHPStan niveau 0, puis montée progressive pour mesurer la dette.
  • Liste des extensions PHP utilisées, détection des mysql_*, ereg_*, each, create_function, mb_* en mode implicite.
  • Audit des dépendances Composer, mapping vers les versions compatibles 8.3.
  • Panorama des tests existants : couverture réelle (souvent moins de 15% sur du legacy), qualité des assertions.

Livrable : un rapport chiffrant la dette technique, priorisant les modules à migrer en premier (les plus risqués côté sécurité, ou les plus stables côté métier).

Phase 2 — Adapter (2 à 4 semaines)

Nous montons l'infrastructure de coexistence :

  • Mise en place d'une façade HTTP (reverse proxy Nginx ou Traefik) qui route par pattern d'URL.
  • Déploiement du nouveau runtime PHP 8.3 sur des conteneurs séparés.
  • Création d'un repo new/ qui héberge le code cible (Symfony 7 par exemple), partageant la base de données avec l'ancien.
  • Feature flags via Unleash ou un service maison, permettant de basculer par utilisateur, par groupe ou globalement.
  • Pipeline CI/CD dual : les deux codebases sont buildées et testées en parallèle.

Phase 3 — Strangler (6 à 12 mois selon la taille)

Le cœur du projet. Module par module, nous :

  1. Réécrivons le module dans le nouveau stack en suivant ses contrats HTTP exacts.
  2. Rejouons les scénarios Behat existants contre le nouveau code.
  3. Dark launch : le nouveau code reçoit le trafic en shadow mode, sans que sa réponse soit retournée à l'utilisateur.
  4. Comparaison diff entre ancien et nouveau : log des écarts, investigation.
  5. Canary : 1% du trafic bascule, puis 10%, 50%, 100%.
  6. Module figé sur le nouveau code, suppression progressive de l'ancien.

Sur une base 250 000 lignes, nous migrons typiquement 3 à 5 modules par mois, en équipe de 3 personnes.

Phase 4 — Cutover (2 à 4 semaines)

Le dernier module est passé. Nous :

  • Supprimons la façade de routage devenue inutile.
  • Décommissionnons l'infrastructure PHP 5.6 (sauf les environnements d'archive).
  • Recompilons la liste des extensions nécessaires, nettoyons les Dockerfiles.
  • Bascule officielle, communication interne.

Phase 5 — Décommission (1 à 2 mois)

Post-cutover, nous consacrons du temps à :

  • Nettoyer les feature flags obsolètes.
  • Supprimer les comparateurs diff ancien/nouveau.
  • Archiver les logs de l'ancien code.
  • Produire un rapport de clôture avec métriques avant/après : temps de réponse, dette PHPStan, couverture, CVE résolues.

Les outils qui font la différence

La migration strangler ne se fait pas à la main. Notre chaîne d'outils, rodée sur les missions récentes :

Rector

Rector automatise les transformations syntaxiques : passage du typage, remplacement de each par foreach, suppression de create_function, montée de niveau PHP.

<?php
// rector.php
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\Symfony\Set\SymfonySetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
    ->withPhpSets(php83: true)
    ->withSets([
        LevelSetList::UP_TO_PHP_83,
        SetList::CODE_QUALITY,
        SetList::DEAD_CODE,
        SetList::TYPE_DECLARATION,
        SymfonySetList::SYMFONY_72,
        SymfonySetList::SYMFONY_CODE_QUALITY,
    ])
    ->withParallel()
    ->withCache(cacheDirectory: __DIR__ . '/var/cache/rector');

Sur une base 250 000 lignes, Rector traite l'ensemble en une quinzaine de minutes et automatise 70 à 85% des transformations nécessaires. Le reste demande une intervention humaine.

PHPStan

PHPStan est le verrou contre les régressions. Nous fixons un niveau baseline initial, puis montons palier par palier. Configuration typique en cours de migration :

# phpstan.neon
parameters:
  level: 8
  paths:
    - src
  excludePaths:
    - src/Legacy
  bootstrapFiles:
    - vendor/autoload.php
  treatPhpDocTypesAsCertain: false
  reportUnmatchedIgnoredErrors: true
  tmpDir: var/phpstan

Le fichier phpstan-baseline.neon est versionné pour figer la dette, puis réduit sprint par sprint. Aucun PR ne peut ajouter d'erreur au-dessus de la baseline.

Infection

Infection mesure la qualité réelle de la suite de tests par mutation testing. Sur le legacy 5.6, on démarre souvent à un MSI (Mutation Score Indicator) de 10 à 20%, ce qui signifie que la couverture affichée est trompeuse. En cours de migration, cible 60 à 70%.

Deptrac

Deptrac vérifie que la migration respecte l'architecture en couches ciblée (Domain, Application, Infrastructure). Configuration pour interdire que le Domain appelle Doctrine :

deptrac:
  paths:
    - ./src
  layers:
    - name: Domain
      collectors:
        - type: directory
          value: src/Domain/.*
    - name: Infrastructure
      collectors:
        - type: directory
          value: src/Infrastructure/.*
  ruleset:
    Domain: []
    Infrastructure:
      - Domain

Les pièges à éviter

Sur 9 missions strangler, les pièges qui reviennent le plus souvent.

Les sessions PHP incompatibles

La sérialisation par défaut des sessions change entre PHP 5.6 (php handler) et PHP 7+ (possibilité de php_serialize). Si l'ancien et le nouveau code se partagent les sessions sur un store commun (Redis ou base), il faut forcer le même handler, de préférence php_serialize, sur les deux environnements dès la phase 2.

L'encoding

PHP 5.6 laissait passer des chaînes non-UTF8 sans broncher. PHP 8.3 avec mb_strict_mode lève. Systématiser un middleware de validation à l'entrée, et forcer UTF-8 dans php.ini (default_charset = "UTF-8").

Les fonctions supprimées

each, create_function, money_format, ezmlm_hash, split : disparues en 7+. Grep récursif sur la base avant de démarrer la phase 3. Rector en traite la majorité, mais pas toujours dans du code généré dynamiquement.

Le passage par référence implicite

function foo(&$bar) fonctionnait avec des appels foo($x) en 5.6. En 7+, certains contextes génèrent des warnings, voire des erreurs. Auditer les fonctions qui modifient leurs arguments.

Les DateTime en immutable

PHP 8.2+ déprécie la mutation implicite des DateTime. Basculer vers DateTimeImmutable systématiquement, ce que fait Rector via DATE_IMMUTABLE set.

Les dépendances Composer fantômes

Le legacy embarque souvent des libs non déclarées en composer.json, chargées via un autoloader maison ou des require statiques. La façade strangler les perd. Lister toutes les inclusions de fichiers avec grep -r "require\|include" --include="*.php" avant la phase 2.

Retour de terrain : plateforme B2B, 500 000 utilisateurs

Nous avons mené en 2024-2025 la migration d'une plateforme B2B de distribution, trafic 2,4 millions de sessions par mois, 500 000 utilisateurs actifs. Legacy initial : PHP 5.6.40, Symfony 2.8, Propel 1.7, 280 000 lignes de code PHP, 40 000 lignes de JavaScript legacy.

Durée totale : 14 mois, 3 développeurs senior à temps plein plus un lead architecte à mi-temps.

Ciblage final : PHP 8.3, Symfony 7.1, Doctrine ORM 3, API Platform 4.

Métriques avant/après :

  • Temps de réponse p95 : 1 450 ms → 380 ms (OPcache + JIT + Doctrine optimisé).
  • Dette PHPStan : niveau 0 avec 8 200 erreurs → niveau 9 avec 0 erreurs.
  • Couverture de tests : 11% → 74% sur la couche métier.
  • Temps de build CI : 42 min → 9 min.
  • CVE ouvertes sur les dépendances : 31 critiques → 0.

Résultat opérationnel : zéro incident majeur en production pendant les 14 mois de migration. Trois incidents mineurs, tous détectés en canary avant bascule à 100%, rollback instantané à chaque fois.

Conclusion

Migrer un PHP 5.6 vers 8.3 en 2026 n'est pas un choix technologique, c'est une nécessité de conformité et de continuité d'activité. Le strangler pattern permet de l'exécuter sans ruine budgétaire ni gel fonctionnel. À condition de respecter la discipline des cinq phases, d'outiller sérieusement (Rector, PHPStan, Deptrac, Infection, Behat) et d'accepter 12 à 18 mois de cohabitation des deux codes.

Si votre équipe tourne encore sur une version 5.x ou 7.x, ne repoussez pas. Chaque mois supplémentaire coûte en sécurité, en capacité à recruter et en compatibilité d'écosystème. Écrivez-nous à contact@your-digital-hub.com, nous réalisons un audit initial en 3 à 5 jours, livrable exécutif, aucun engagement.