◀ Retour au blog
PHP / Symfony

Migration Symfony de 2.8 à 6.4

Publié le 14 Apr 2024· 7 min de lecture
#Symfony#Migration#PHP

Migrer Symfony sans interruption de service

La migration d'une application Symfony de la version 2.8 à 6.4 est un défi majeur. Chez Keytchens, nous avons réalisé cette migration sur une plateforme en production gérant des commandes en temps réel, sans aucune interruption de service.

Quatre versions majeures séparent Symfony 2.8 de Symfony 6.4. Entre les deux, la structure des dossiers a changé, l'injection de dépendances est devenue automatique, le système de sécurité a été réécrit et PHP est passé de la version 5 à la version 8. Cet article présente une méthode pour franchir ces étapes une par une, en gardant l'application en production à chaque instant.

Le principe : suivre le modèle de versions de Symfony

Symfony publie une version mineure tous les six mois, et la dernière mineure de chaque majeure (x.4) est une version LTS. La règle clé est la suivante : une fonctionnalité n'est jamais supprimée dans une version majeure sans avoir été dépréciée dans la version précédente. Une application qui tourne sur la dernière mineure d'une majeure sans aucune dépréciation peut donc passer à la majeure suivante sans casse.

La migration se découpe ainsi en paliers, chacun validé et déployé en production avant de passer au suivant.

Stratégie de migration progressive

Plutôt qu'une réécriture complète, nous avons opté pour une migration incrémentale :

  • Phase 1 : Symfony 2.8 → 3.4 (compatibilité ascendante)
  • Phase 2 : Symfony 3.4 → 4.4 (migration vers la structure Flex)
  • Phase 3 : Symfony 4.4 → 5.4 (suppression des dépréciations)
  • Phase 4 : Symfony 5.4 → 6.4 (adoption PHP 8.1+)

Chaque palier impose aussi une version minimale de PHP : PHP 7.1.3 pour Symfony 4.4, PHP 7.2.5 pour Symfony 5.4 et PHP 8.1 pour Symfony 6.4. Il est souvent plus simple de monter d'abord la version de PHP sur la version de Symfony courante, puis de migrer Symfony : on ne change qu'une variable à la fois.

Avant de commencer

  1. Des tests : sans tests fonctionnels couvrant les parcours critiques, chaque palier est un pari. Commencez par là.
  2. Un inventaire des dépendances : composer outdated et la liste des bundles tiers. Un bundle abandonné, non compatible avec la version cible, doit être remplacé avant la migration, pas pendant.
  3. Un environnement de préproduction alimenté avec une copie anonymisée des données de production.
  4. Un suivi des erreurs (logs centralisés, outil de type Sentry) pour détecter immédiatement une régression après déploiement.

Outils indispensables

Le PHPUnit Bridge de Symfony liste toutes les dépréciations déclenchées pendant les tests. Rector applique automatiquement une grande partie des corrections :

# Détecter les dépréciations
composer require --dev symfony/phpunit-bridge
SYMFONY_DEPRECATIONS_HELPER='max[total]=0' php bin/phpunit

# Rectifier automatiquement
composer require --dev rector/rector
vendor/bin/rector process --dry-run
vendor/bin/rector process

Avec max[total]=0, la moindre dépréciation fait échouer la suite de tests : c'est le réglage à viser en intégration continue avant de changer de majeure. Rector se configure dans un fichier rector.php à la racine du projet, en choisissant l'ensemble de règles correspondant à la version cible :

<?php

use Rector\Config\RectorConfig;
use Rector\Symfony\Set\SymfonySetList;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
    ->withSets([SymfonySetList::SYMFONY_54]);

Lancez toujours Rector avec --dry-run d'abord et relisez le diff : c'est un outil puissant, pas infaillible. Côté conteneur de services, php bin/console debug:container --deprecations (disponible depuis Symfony 5.1) liste les dépréciations levées à la compilation.

À partir de la structure Flex, la version de Symfony se pilote dans composer.json, puis se met à jour en une commande :

{
    "extra": {
        "symfony": {
            "require": "6.4.*"
        }
    }
}
composer update "symfony/*" --with-all-dependencies

Points critiques

Le changement le plus visible est la configuration des services. Symfony 2.8 imposait de déclarer chaque service à la main ; depuis Symfony 3.3, l'autowiring et l'autoconfiguration enregistrent automatiquement les classes de src/ :

# Migration des services (avant - services.yml)
services:
    app.manager.order:
        class: App\Manager\OrderManager
        arguments: ['@doctrine.orm.entity_manager']

# Après - services.yaml avec autowiring
services:
    _defaults:
        autowire: true
        autoconfigure: true
    App\:
        resource: '../src/'

Si du code récupère encore le service par son ancien identifiant, par exemple $container->get('app.manager.order'), déclarez un alias temporaire vers la classe, puis remplacez progressivement ces appels par de l'injection dans le constructeur. Les services sont privés par défaut depuis Symfony 4.0 : l'accès direct au conteneur doit disparaître.

Les autres points qui demandent de l'attention :

  • La structure des dossiers (passage à 4.x) : app/config devient config/, web/ devient public/, les templates vont dans templates/ et les bundles s'enregistrent dans config/bundles.php.
  • La sécurité : le système d'authentification Guard a été remplacé par le nouveau système d'authenticators, introduit en 5.1 et seul disponible en 6.0. Les authentificateurs personnalisés doivent être réécrits.
  • Les commandes et contrôleurs : ContainerAwareCommand a été supprimée en 5.0, et les contrôleurs doivent recevoir leurs dépendances par injection plutôt que via $this->get().
  • Les types de retour : Symfony 6 ajoute des types de retour natifs à ses interfaces. Les classes qui les implémentent ou les étendent (voters, normalizers, commandes) doivent déclarer les mêmes types.

Gestion du zero-downtime

Pour maintenir le service pendant la migration :

  • Déploiement blue-green avec Docker
  • Feature flags pour activer progressivement les nouvelles fonctionnalités
  • Tests de régression automatisés couvrant 85% du code
  • Rollback automatique en cas de détection d'erreurs

Le déploiement blue-green consiste à faire tourner deux environnements côte à côte : la version actuelle reçoit le trafic pendant que la nouvelle démarre et passe ses vérifications de santé. Le reverse proxy bascule ensuite le trafic, et l'ancienne version reste disponible pour un retour arrière immédiat. Condition indispensable : les deux versions doivent fonctionner avec le même schéma de base de données. Les migrations Doctrine doivent donc rester rétrocompatibles, selon le principe détaillé dans l'article Migrations base de données sans interruption.

Les feature flags complètent ce dispositif : le nouveau code est déployé mais désactivé, puis activé pour une partie des utilisateurs, et coupé en un instant si un problème apparaît.

Pièges à éviter

  • Sauter des paliers : passer directement de 2.8 à 4.4 cumule des centaines de changements et rend chaque erreur difficile à attribuer.
  • Mélanger migration et nouvelles fonctionnalités : une branche de migration qui vit des mois devient impossible à fusionner. Livrez chaque palier rapidement, en petites pull requests.
  • Ignorer les dépréciations des bundles tiers : elles bloqueront la majeure suivante tout autant que les vôtres.
  • Oublier le cache et OPcache au déploiement : un cache:clear et un rechargement de PHP-FPM sont indispensables après chaque palier.

Checklist par palier

  1. Monter PHP à la version requise et vérifier les tests.
  2. Atteindre zéro dépréciation sur la version courante.
  3. Mettre à jour Symfony et les bundles vers la majeure suivante.
  4. Passer Rector, relire le diff, corriger les cas restants.
  5. Valider en préproduction, puis déployer en blue-green.
  6. Surveiller les erreurs et les performances avant d'attaquer le palier suivant.

Cette migration a permis d'améliorer les performances de 40% et de réduire la dette technique de manière significative.