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
- Des tests : sans tests fonctionnels couvrant les parcours critiques, chaque palier est un pari. Commencez par là.
- Un inventaire des dépendances :
composer outdatedet la liste des bundles tiers. Un bundle abandonné, non compatible avec la version cible, doit être remplacé avant la migration, pas pendant. - Un environnement de préproduction alimenté avec une copie anonymisée des données de production.
- 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/configdevientconfig/,web/devientpublic/, les templates vont danstemplates/et les bundles s'enregistrent dansconfig/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 :
ContainerAwareCommanda é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:clearet un rechargement de PHP-FPM sont indispensables après chaque palier.
Checklist par palier
- Monter PHP à la version requise et vérifier les tests.
- Atteindre zéro dépréciation sur la version courante.
- Mettre à jour Symfony et les bundles vers la majeure suivante.
- Passer Rector, relire le diff, corriger les cas restants.
- Valider en préproduction, puis déployer en blue-green.
- 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.