Blog

Migration Symfony 7 → 8 : Le guide de préparation (de 7.4 LTS à 8.4 LTS)

Symfony 7.4 ou Symfony 8 ? Notre position pour les projets en production, ce que la 8 change vraiment, et la méthode pour préparer dès maintenant un passage sans surprise à la 8.4 LTS de novembre 2027.

Mathieu Ducrot Mathieu Ducrot
|
|
29 min de lecture
| Tech
Résumez cette page avec votre IA préférée :
Migration Symfony 7 vers 8 : préparer le passage à la 8.4 LTS

Symfony 8.0 est sortie le 27 novembre 2025, le même jour que la 7.4 LTS. Depuis, la 8.1 a suivi en mai 2026, la 8.2 arrive fin novembre 2026, et la 8.4, prochaine version LTS, est annoncée pour novembre 2027. La question nous est posée à chaque cadrage de projet et à chaque revue de contrat de maintenance : faut-il passer en 8 maintenant ?

Notre réponse est stable et nous préférons l’écrire noir sur blanc plutôt que de la reformuler à chaque fois.

La position de SmartBooster

  • Pour un projet en production, nous conseillons la version LTS. Aujourd’hui, c’est Symfony 7.4, couverte jusqu’en novembre 2029. La prochaine LTS, la 8.4, est prévue pour novembre 2027.
  • Symfony 8 tourne chez nous, en interne. Nos projets internes sont sur la branche 8.x pour monter en compétence sur les nouvelles fonctionnalités. Les projets clients restent en 7.4, sauf demande explicite du client.
  • Un nouveau projet client démarre sur Symfony 8 seulement si sa mise en production est clairement postérieure à la sortie de la 8.4 LTS. Dans les autres cas, il démarre en 7.4 et sera migré vers la 8.4 quand elle sera là.

Ce guide explique cette position, détaille ce que Symfony 8 apporte et ce qu’il change, puis donne la méthode pour préparer le passage dès maintenant, étape par étape. La route est encore longue : la 8.4 LTS n’est attendue que dans plus d’un an, et deux mineures doivent encore sortir d’ici là, chacune avec ses nouveautés et ses dépréciations. Nous mettrons donc ce guide à jour au fil du cycle, pour qu’il soit notre référence le jour de la bascule, comme nos guides de la 5 vers la 6 et de la 6 vers la 7 l’ont été pour les cycles précédents. L’objectif ne change pas : le jour où la 8.4 LTS sort, la bascule doit être une formalité, pas un projet.

Sommaire

  1. Symfony 7.4 ou Symfony 8 : quelle version choisir ?
  2. Les nouveautés de Symfony 8, déjà disponibles en 7.4
  3. Et la performance ?
  4. Ce que Symfony 8 change concrètement
  5. Étape 1 : Phase de nettoyage et d’audit
  6. Étape 2 : Mise à jour vers la dernière version 7.4
  7. Étape 3 : Migration vers PHP 8.4 minimum, 8.5 de préférence
  8. Étape 4 : Traitement des dépréciations
  9. Étape 5 : Le crash-test en 8.x, sans mise en production
  10. Étape 6 : Bascule vers la 8.4 LTS et validation

Symfony 7.4 ou Symfony 8 : quelle version choisir ?

La règle : Symfony 8.0, c’est la 7.4 sans le code déprécié

Le billet officiel Preparing for Symfony 7.4 and Symfony 8.0 le résume en une formule : Symfony 8.0 = Symfony 7.4 moins toutes les dépréciations. Les deux versions ont exactement les mêmes fonctionnalités. La 7.4 conserve les couches de compatibilité accumulées pendant le cycle 7.x et signale leur usage ; la 8.0 les supprime, soit 13 202 lignes de code retirées.

Autrement dit, un projet en 7.4 n’attend aucune fonctionnalité de la 8.0. Ce qui distingue les deux versions, c’est leur calendrier de support et leur exigence PHP.

Symfony 7.4 LTSSymfony 8.0Symfony 8.4 LTS
Sortienovembre 2025novembre 2025novembre 2027 (prévue)
PHP minimum8.28.48.4
PHP conseillée chez SmartBooster8.58.58.5
Correctifs de bugsjusqu’en novembre 2028jusqu’en juillet 2026jusqu’en novembre 2030
Correctifs de sécuritéjusqu’en novembre 2029jusqu’en juillet 2026jusqu’en novembre 2031
Dépréciationstoutes celles du cycle 7.xaucunetoutes celles du cycle 8.x

Les dates sont celles publiées par Symfony sur symfony.com/releases. Nous les suivons projet par projet dans notre calendrier de fin de support Symfony. Mises bout à bout, elles dessinent le chemin d’un projet en production : rester sur la 7.4, et rejoindre la 8.4.

gantt
  title Fenêtres de correctifs de sécurité, Symfony 7.4 LTS et branche 8.x
  dateFormat YYYY-MM
  axisFormat %Y
  section LTS
    Symfony 7.4 LTS          :active, s74, 2025-11, 2029-11
    Symfony 8.4 LTS (prévue) :s84, 2027-11, 2031-11
  section Versions standard
    Symfony 8.0              :done, s80, 2025-11, 2026-07
    Symfony 8.1              :done, s81, 2026-05, 2027-01
    Symfony 8.2 (prévue)     :s82, 2026-11, 2027-07
    Symfony 8.3 (prévue)     :s83, 2027-05, 2028-01
  section Projet en production
    Préparation en 7.4       :crit, prep, 2026-09, 2027-11
    Bascule vers la 8.4      :milestone, m1, 2027-12, 0d

Chaque version standard vit huit mois. Pour tenir sur la branche 8.x, un projet doit enchaîner 8.0, 8.1, 8.2 et 8.3 sans en manquer une. Sur la 7.4, il attend la 8.4 en travaillant ses dépréciations à son rythme.

Pourquoi nous conseillons la LTS pour un projet en production

Symfony recommande officiellement d’utiliser les versions standard « autant que possible », pour accéder aux nouveautés en continu et traiter les dépréciations petit à petit, d’une mineure à l’autre. C’est un bon conseil pour une équipe qui livre tous les jours et dispose d’un budget de mise à jour permanent. Ce n’est pas la situation de la plupart des applications métier que nous maintenons.

Une version standard reçoit 8 mois de correctifs. Symfony 8.0, sortie en novembre 2025, ne reçoit plus aucun correctif depuis juillet 2026. La 8.1 s’arrête en janvier 2027. Choisir une version standard, c’est s’engager à monter de version tous les six mois, sans exception, sous peine de tourner sur une version non couverte, y compris pour la sécurité. Une application qui reçoit une livraison par trimestre, avec des fenêtres de recette contraintes par le métier, ne tient pas ce rythme. Elle finit sur une 8.1 ou une 8.2 hors support, ce qui est exactement ce que la LTS existe pour éviter.

La 7.4 offre quatre ans de couverture. Ces quatre années sont le cadre dans lequel un contrat de maintenance se planifie : les montées de version PHP, les mises à jour de bundles et le traitement des dépréciations se font dans le lot courant de maintenance évolutive, au rythme du projet, sans jamais mettre la production hors couverture. C’est une question de qualité logicielle avant d’être une question de nouveautés : une version couverte et des dépréciations traitées au fil de l’eau valent plus qu’un numéro de version récent.

La version de Symfony n’est qu’une partie de l’équation

Une application Symfony n’est jamais « juste » du Symfony. Elle repose sur Doctrine, sur des bundles de sécurité, d’API, d’administration, et souvent sur des briques open source de taille conséquente comme EasyAdmin ou Sonata. Chacune de ces briques doit suivre la majeure, et le billet officiel le dit sans détour : l’écosystème compte des milliers de bundles, et leur mise à jour est un effort collectif qui demande du temps.

Les 13 202 lignes supprimées en 8.0 sont du code sur lequel ces bundles s’appuyaient. Un bundle qui n’a pas encore publié de version compatible bloque la migration entière, quel que soit l’état de votre propre code. Et une majeure ne se stabilise pas le jour de sa sortie : les 8.1, 8.2 et 8.3 apportent de nouvelles fonctionnalités, mais aussi de nouvelles dépréciations, que les mainteneurs de bundles absorbent au fur et à mesure.

Quand le projet utilise ce type de briques, il faut laisser le temps à Symfony de se stabiliser et à la communauté de prendre en compte les impacts. EasyAdmin, maintenu par un membre de la Core Team, suit généralement dans les semaines qui suivent une majeure. Sonata a un rythme de contributions plus irrégulier, et une migration qui dépend de lui se cale sur son calendrier, pas sur celui de Symfony. Dans les deux cas, viser la 8.4 LTS, deux ans après la 8.0, c’est arriver sur un écosystème qui a eu le temps de se mettre à niveau.

Ce que nous faisons chez SmartBooster

  • Nos projets internes sont sur la branche 8.x. C’est là que nous testons les commandes invocables, la configuration en array shapes, les formulaires multi-étapes ou le composant ObjectMapper, et que nous mesurons l’effort réel de chaque montée de mineure. Quand la 8.4 arrivera, nous l’aurons déjà pratiquée.
  • Les projets clients restent en 7.4, sauf demande explicite du client, documentée dans le contrat de maintenance avec l’engagement de suivi semestriel que cela implique.
  • Un nouveau projet client démarre en Symfony 8 uniquement si sa mise en production est clairement postérieure à novembre 2027. Un projet dont la recette commence en 2027 et qui passe en production après la sortie de la 8.4 peut démarrer directement sur la branche 8.x et atterrir sur la LTS. Un projet qui entre en production avant démarre en 7.4.

La décision en quatre questions

  1. Le projet est-il en production ou le sera-t-il avant novembre 2027 ? Si oui : 7.4.
  2. L’équipe dispose-t-elle d’un budget de mise à jour engagé tous les six mois ? Si non : 7.4.
  3. Le projet dépend-il d’EasyAdmin, de Sonata, d’API Platform ou d’un bundle métier peu maintenu ? Si oui : 7.4, et vérification de la compatibilité de chaque brique avant de planifier la 8.4.
  4. L’hébergement peut-il passer en PHP 8.4 au minimum, 8.5 de préférence ? Si non : 7.4, et la montée PHP devient le premier chantier à planifier, parce qu’elle conditionne tout le reste.

Les nouveautés de Symfony 8, déjà disponibles en 7.4

Les fonctionnalités mises en avant sur symfony.com/8 ont été introduites au fil du cycle 7.x, entre la 7.1 et la 7.4. Elles sont donc toutes utilisables sur la LTS. C’est l’intérêt de la LTS telle que Symfony la conçoit : ce n’est pas une version figée, c’est la dernière mineure d’un cycle, avec tout ce que ce cycle a apporté. Voici celles qui changent réellement la façon d’écrire une application métier, avec le code tel que nous le pratiquons sur nos projets internes.

Formulaires multi-étapes (FormFlow)

Un parcours d’inscription, un dépôt de dossier, un configurateur de devis : jusqu’ici, découper un formulaire en étapes supposait un bundle tiers ou du code maison pour stocker l’état en session, valider étape par étape et gérer le retour en arrière. Depuis la 7.4, le composant Form le fait nativement. Un flow étend AbstractFlowType et déclare ses étapes, chacune étant un type de formulaire ordinaire :

use Symfony\Component\Form\Flow\AbstractFlowType;

class DossierType extends AbstractFlowType
{
    public function buildFormFlow(FormFlowBuilderInterface $builder, array $options): void
    {
        $builder->addStep('identite', IdentiteType::class);
        $builder->addStep('entreprise', EntrepriseType::class);
        $builder->addStep('pieces', PiecesJointesType::class);

        $builder->add('navigator', NavigatorFlowType::class);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Dossier::class,
            'step_property_path' => 'etapeCourante',
        ]);
    }
}

L’objet de données porte l’étape courante et déclare, par groupe de validation, ce qui doit être valide à chaque étape. Le nom de l’étape devient automatiquement le groupe de validation actif : l’étape « identité » ne valide que l’identité.

class Dossier
{
    public function __construct(
        #[Valid(groups: ['identite'])]
        public Identite $identite = new Identite(),

        #[Valid(groups: ['entreprise'])]
        public Entreprise $entreprise = new Entreprise(),

        #[Valid(groups: ['pieces'])]
        public PiecesJointes $pieces = new PiecesJointes(),

        public string $etapeCourante = 'identite',
    ) {
    }
}

Côté contrôleur, un flow se manipule comme un formulaire, à deux méthodes près : isFinished() dit si l’utilisateur a validé la dernière étape, getStepForm() rend le formulaire de l’étape courante.

#[Route('/dossier')]
public function __invoke(Request $request): Response
{
    $flow = $this->createForm(DossierType::class, new Dossier())
        ->handleRequest($request);

    if ($flow->isSubmitted() && $flow->isValid() && $flow->isFinished()) {
        // $flow->getData() contient le Dossier complet

        return $this->redirectToRoute('app_dossier_confirmation');
    }

    return $this->render('dossier/flow.html.twig', [
        'form' => $flow->getStepForm(),
    ]);
}

Les boutons de navigation (NextFlowType, PreviousFlowType, FinishFlowType, ResetFlowType) acceptent des options de branchement : skip pour sauter une étape sous condition, back_to pour revenir à une étape précise, include_if pour n’afficher un bouton que dans certains cas. Un flow avec une étape conditionnelle se lit ainsi :

stateDiagram-v2
  direction LR
  [*] --> identite
  identite --> entreprise : suivant
  entreprise --> identite : precedent
  entreprise --> pieces : suivant
  entreprise --> recapitulatif : suivant (particulier, skip pieces)
  pieces --> entreprise : precedent
  pieces --> recapitulatif : suivant
  recapitulatif --> identite : reset
  recapitulatif --> [*] : terminer

Pour nous, c’est la nouveauté la plus utile du cycle sur les applications métier : le code de navigation disparaît, la validation par étape est portée par le modèle, et le bundle CraueFormFlow, qui rendait ce service depuis des années, devient une dépendance de moins à suivre.

Commandes console invocables

Une commande console n’a plus besoin d’étendre Command ni de redéfinir configure(). Une classe, une méthode __invoke(), des arguments et options déclarés en attributs sur les paramètres : le code de plomberie disparaît. Depuis la 7.4, les énumérations sont acceptées en type d’argument, et un attribut #[Ask] demande la valeur manquante en interactif.

enum Region: string
{
    case Est = 'est';
    case Ouest = 'ouest';
}

#[AsCommand(name: 'app:serveur:ajouter', description: 'Déclare un serveur dans une région')]
class AjouterServeurCommand
{
    public function __invoke(
        SymfonyStyle $io,
        #[Argument, Ask('Dans quelle région ?')] Region $region,
        #[Option(description: 'Ne rien écrire, afficher seulement')] bool $dryRun = false,
    ): int {
        $io->success(sprintf('Serveur déclaré en région %s', $region->value));

        return Command::SUCCESS;
    }
}

Une valeur hors énumération est refusée avec la liste des valeurs admises, sans une ligne de validation à écrire. Quand une commande accumule les paramètres, #[MapInput] les regroupe dans un DTO, propriété par propriété, avec les mêmes attributs.

Configuration PHP en array shapes

Le format PHP « fluide » à base de builders (SecurityConfig, FrameworkConfig…) est déprécié en 7.4 et disparaît en 8.0. Il est remplacé par un tableau typé, dont la forme est générée depuis les bundles installés dans un fichier config/reference.php à commiter. L’éditeur autocomplète chaque clé, PHPStan valide les types.

// config/packages/security.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return App::config([
    'security' => [
        'firewalls' => [
            'main' => [
                'pattern' => '^/*',
                'lazy' => true,
            ],
        ],
        'access_control' => [
            ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'],
        ],
    ],
]);

Le YAML reste supporté et reste notre format par défaut sur les projets clients : il est lisible par quelqu’un qui n’est pas développeur PHP, et les recettes Symfony Flex le mettent à jour automatiquement. Le format PHP prend son sens sur les configurations qui dépendent de calculs ou de constantes.

Extensions Twig par attributs

Un filtre ou une fonction Twig se déclare sur une méthode, sans classe parente et sans séparer extension et runtime. Le chargement est paresseux par défaut : la méthode n’est chargée que lorsqu’un template l’utilise, ce que l’ancienne approche n’obtenait qu’en découpant le code en deux classes.

class AppExtension
{
    #[AsTwigFilter('numero_commande')]
    public function formaterNumeroCommande(string $numero): string
    {
        return sprintf('CMD-%s', strtoupper($numero));
    }

    #[AsTwigFunction('delai_livraison')]
    public function delaiLivraison(Commande $commande): string
    {
        // ...
    }
}

ObjectMapper : DTO vers entité sans code de recopie

Sur une application métier, une part non négligeable du code des contrôleurs et des services consiste à copier les champs d’un DTO validé vers une entité, puis dans l’autre sens. Le composant ObjectMapper, stabilisé en 8.0, s’en charge à partir des noms de propriétés, avec l’attribut #[Map] pour les cas qui divergent.

#[Map(target: Client::class)]
class ClientInput
{
    #[Map(target: 'adresseEmail')]
    public string $email = '';

    #[Map(transform: 'strtolower')]
    public string $identifiant = '';

    #[Map(if: false)]
    public string $champTechnique = '';
}

// Dans un service ou un contrôleur
$client = $mapper->map($input, Client::class);   // création
$mapper->map($input, $clientExistant);             // mise à jour

Les autres attributs qui remplacent du code

Le cycle 7.x a poursuivi le mouvement engagé en 6.x : ce qui se faisait en code impératif dans un contrôleur se déclare en attribut. Trois exemples que nous utilisons sur tous les nouveaux projets :

  • #[IsCsrfTokenValid('supprimer_client')] sur une action de contrôleur remplace le isCsrfTokenValid() manuel et la réponse 403 associée. Depuis la 7.2, la protection CSRF fonctionne aussi sans session, ce qui la rend compatible avec le cache HTTP et les API sans état.
  • #[MapUploadedFile] fait arriver un fichier envoyé directement en argument de contrôleur, validé par les contraintes File ou Image comme n’importe quel autre champ.
  • #[MapRequestPayload] et #[MapQueryString] désérialisent et valident le corps ou la query string dans un DTO typé. Le contrôleur ne lit plus $request du tout, ce qui explique au passage la suppression de Request::get().

Messages signés dans Messenger et explications des voters

Deux ajouts du cycle 7.x qui comptent sur les applications sensibles. Messenger peut signer les messages qu’il dépose dans une file, et refuser à la consommation ceux dont la signature ne correspond pas : un message altéré dans la file (Redis, AMQP, base de données) n’est pas traité. Et les voters de sécurité peuvent expliquer leur décision : le Profiler et les logs disent pourquoi un accès a été refusé, au lieu d’un 403 muet à reconstituer voter par voter.

Et la performance ?

Symfony ne publie pas de benchmark global entre la 7.4 et la 8.0, et pour cause : le code exécuté est le même, moins les couches de compatibilité. Ne pas attendre de gain de temps de réponse du seul changement de majeure. Les gains de performance du cycle sont ailleurs, dans des fonctionnalités précises, et chacune se mesure sur un cas d’usage donné.

FonctionnalitéCe que Symfony annonceOù ça compte
JsonStreamer (7.3, stabilisé en 8.0)Encodage de 10 000 objets 10 fois plus rapide avec 50 % de mémoire en moins ; décodage de 50 000 objets 10 fois plus rapide avec 90 % de mémoire en moins, par rapport au Serializer. API Platform rapporte une part de sérialisation passée de 83 % à 45 % du temps de requête.Les API qui renvoient de gros volumes, les exports, les imports de fichiers JSON volumineux
Lazy objects natifs PHP 8.4 (7.3)Les services paresseux utilisent le mécanisme natif de PHP 8.4 au lieu des proxys générés par Symfony. Aucun changement de code, le gain vient avec la version de PHP.Toute application avec des services coûteux à instancier (clients HTTP, connexions externes)
Pré-compression des assets (7.3)CSS et JS compressés une fois au build, en Brotli, Zstandard ou Gzip, servis tels quels par FrankenPHP, Caddy ou Cloudflare. Plus de compression à la volée à chaque requête.Le temps de premier affichage, et le CPU du serveur web
Extensions Twig paresseuses (7.3)Filtres et fonctions déclarés par attributs ne sont chargés que lorsqu’un template les utilise.Les applications avec de nombreuses extensions Twig, dont la plupart ne servent pas sur une page donnée
Pages d’erreur statiques (7.3)Les pages 404 et 500 s’exportent en HTML statique, servi par le serveur web sans démarrer PHP.Les sites exposés au crawl et aux scanners, qui génèrent beaucoup de 404
Client HTTP avec cache (7.4)Cache client conforme à la RFC 9111, adossé au composant Cache.Les appels répétés à des API tierces dont les réponses portent des en-têtes de cache
Parsing HTML5 natif (7.4)Le parseur HTML5 natif de PHP 8.4 est utilisé quand il est disponible, plus rapide que la bibliothèque tierce précédente.Les tests fonctionnels et le scraping

Deux enseignements pour un projet en production. D’abord, la majorité de ces gains sont disponibles en 7.4 : rien n’oblige à passer en 8 pour en bénéficier. Ensuite, deux d’entre eux dépendent de PHP 8.4, pas de Symfony : c’est une raison de plus de traiter la montée PHP comme le premier chantier, en restant en 7.4.

Ce que Symfony 8 change concrètement

PHP 8.4 au minimum, 8.5 en pratique

C’est le changement le plus structurant. Symfony 7.4 accepte PHP 8.2, Symfony 8 exige PHP 8.4 au minimum. Chez nous, la version en place sur les projets est PHP 8.5, sortie en novembre 2025 : c’est celle que nous installons sur tout nouveau projet et vers laquelle nous montons les projets existants, comme le détaille notre guide technique de migration PHP 8.4 et 8.5. Sur beaucoup de projets, c’est la montée PHP qui représente l’essentiel de l’effort, pas la montée Symfony. Elle touche l’hébergement, les images Docker, la CI et les extensions PHP installées.

Le code déprécié en 7.4 qui disparaît en 8.0

Le fichier UPGRADE-8.0 liste l’intégralité des suppressions. Celles que nous rencontrons le plus souvent sur des applications métier :

  • Request::get() est supprimée. Cette méthode cherchait une valeur dans les attributs, puis dans la query string, puis dans le corps de la requête, sans que le code dise laquelle il attendait. Il faut lire explicitement $request->attributes, $request->query ou $request->request.
  • La configuration XML des applications est retirée, ainsi que le format PHP « fluide » à base de builders. La configuration se fait en YAML ou en PHP sous forme de tableaux typés, comme montré plus haut.
  • Command::getDefaultName() et Command::getDefaultDescription() disparaissent au profit de l’attribut #[AsCommand].
  • UserInterface::eraseCredentials() est supprimée. L’effacement des données sensibles passe par __serialize().
  • #[TaggedIterator] et #[TaggedLocator] sont remplacés par #[AutowireIterator] et #[AutowireLocator].
  • Les alias d’autowiring de RateLimiterFactory sont retirés, il faut typer sur RateLimiterFactoryInterface.

Aucun de ces points n’est difficile pris isolément. Ce qui coûte, c’est de les découvrir le jour de la bascule au lieu de les avoir traités un par un, en 7.4, avec les messages de dépréciation pour guide.

Et depuis : 8.1, 8.2 et la route vers la 8.4

La 8.1, sortie en mai 2026, a ajouté entre autres l’attribut #[Serialize] pour les contrôleurs d’API, les applications sans HttpKernel pour les workers et outils en ligne de commande, et le composant Tui. La 8.2 est attendue fin novembre 2026, la 8.3 en mai 2027, la 8.4 en novembre 2027. Chacune apporte ses fonctionnalités et ses propres dépréciations, que la 9.0 supprimera à son tour.

C’est la raison pour laquelle la 8.0 n’est pas une cible : elle n’est plus couverte, et ce qu’elle apporte est déjà dans la 7.4. La cible d’un projet en production, c’est la 8.4 LTS. Les six étapes qui suivent préparent cette bascule sans attendre qu’elle soit publiée.

Étape 1 : Phase de nettoyage et d’audit

Avant de toucher à la moindre contrainte de version, assainissez le projet. Migrer du code mort ou des dépendances abandonnées est une perte de temps et une source d’erreurs inutiles.

Suppression du code mort

Utilisez Rector avec le jeu de règles DeadCode ou PHPStan pour repérer les méthodes, propriétés et classes que plus rien n’appelle.

Audit des dépendances (composer.json)

Pour chaque bundle tiers, trois questions, dans cet ordre :

  • Est-il toujours maintenu ? Un bundle sans publication depuis deux ans ne suivra pas Symfony 8. Mieux vaut le savoir maintenant et chercher une alternative. S’il est petit, l’internaliser est souvent la meilleure option.
  • A-t-il annoncé sa compatibilité Symfony 8 ? Regardez la contrainte symfony/* de sa dernière version sur Packagist. Un bundle qui déclare déjà ^7.4|^8.0 est prêt. Un bundle bloqué en ^7.0 est le point à surveiller.
  • Est-il encore utile ? Certains bundles installés pour une seule fonctionnalité sont remplacés nativement par le framework, la liste des nouveautés ci-dessus en donne plusieurs exemples.

Consignez le résultat dans un tableau : bundle, version, compatibilité Symfony 8 (oui, annoncée, non), action. Ce tableau est votre calendrier de migration réel, bien plus que celui de Symfony.

Étape 2 : Mise à jour vers la dernière version 7.4

Avant de changer de majeure, stabilisez l’application sur la dernière version de maintenance de la branche 7.4. C’est là que se trouvent les messages de dépréciation les plus complets et les derniers correctifs.

Ciblez 7.4.* sur tous les composants Symfony, puis :

composer update "symfony/*"

Mettez à jour dans le même temps les bundles tiers vers leur dernière version compatible 7.4. Les mainteneurs publient généralement des versions qui supportent à la fois la 7.4 et la 8.x : plus votre version de bundle est récente, plus la bascule de l’étape 6 a de chances de passer sans intervention.

À la fin de cette étape, les tests sont verts en 7.4, même s’ils affichent des dépréciations.

Étape 3 : Migration vers PHP 8.4 minimum, 8.5 de préférence

Symfony 8 exige PHP 8.4. Nous visons directement PHP 8.5, la version en place sur nos projets : les deux montées se font avec les mêmes outils, et viser la 8.5 évite de refaire le chantier dans un an. Cette étape se fait en restant sous Symfony 7.4, qui supporte pleinement PHP 8.4 et 8.5. Vous isolez ainsi les problèmes liés au langage avant de toucher au framework.

Mise à jour de l’environnement

PHP 8.5 sur les postes de développement, dans la CI et sur les serveurs de recette et de production. Si l’hébergement ne permet pas de choisir la version de PHP, c’est le moment de le découvrir, pas le jour de la bascule.

Adaptation du composer.json

"require": {
    "php": ">=8.4",
    ...
}

Correction des ruptures

PHP 8.4 déprécie notamment les paramètres nullables implicites (string $x = null doit s’écrire ?string $x = null), un cas fréquent dans le code métier ancien. Rector, avec les sets PHP_84 puis PHP_85, traite l’essentiel automatiquement. Le détail est dans notre guide de migration PHP 8.4 et 8.5, et notre expertise en migration PHP couvre le cas des projets dont l’hébergement ou les extensions compliquent la montée.

Validation

Tests verts en 7.4 sous PHP 8.5. Vous avez levé la contrainte la plus lourde de Symfony 8 sans avoir changé une ligne de configuration du framework.

Étape 4 : Traitement des dépréciations

C’est l’étape qui fait toute la différence entre une migration qui se prépare et une migration qui se subit. Symfony 8 est, par définition, la 7.4 sans les couches de compatibilité : une application en 7.4 sans aucune dépréciation bascule sans réécriture.

Identifier les dépréciations

La méthode recommandée par Symfony est de lancer la suite de tests avec l’affichage des dépréciations :

php bin/phpunit --display-deprecations

Le rapport distingue deux familles, et la distinction compte :

  • Les dépréciations directes, déclenchées par votre propre code. Elles sont à vous, vous les corrigez.
  • Les dépréciations indirectes, déclenchées par le code de vendor/. Elles sont aux mainteneurs des bundles. Vous ne pouvez pas les corriger, mais vous pouvez les signaler, et elles vous disent quels bundles ne sont pas encore prêts pour la 8. Reportez-les dans le tableau de l’étape 1.

Sur les projets encore équipés du phpunit-bridge, la variable SYMFONY_DEPRECATIONS_HELPER remplit le même rôle. Le Profiler, dans la barre d’outils de debug, affiche les dépréciations de la page en cours : utile pour un contrôle ponctuel, insuffisant pour couvrir l’application.

Pas de suite de tests ?

Symfony recommande dans ce cas des tests de fumée (smoke tests) : une requête sur chaque page principale, qui vérifie seulement que la réponse est un code 200. Ils s’écrivent en quelques heures et suffisent à faire remonter la majorité des dépréciations. C’est aussi, souvent, le premier pas vers une vraie couverture de tests, que nous mettons en place dans le cadre de notre accompagnement qualité logicielle.

Automatiser avec Rector

Les sets SYMFONY_7X de Rector couvrent une bonne partie des corrections. Passez par les sets intermédiaires (SYMFONY_71, SYMFONY_72, SYMFONY_73, puis le dernier set publié pour la 7.4) dans l’ordre, une exécution à la fois, avec relecture du diff entre chaque. Ce qui reste après Rector est du code métier qui demande un jugement : c’est là que se concentre le travail manuel.

Exemples de corrections

1. Request::get()

// AVANT (déprécié en 7.4, supprimé en 8.0)
$page = $request->get('page');

// APRÈS : la source est explicite
$page = $request->query->getInt('page', 1);

2. Commandes console

// AVANT
class ExportCommand extends Command
{
    protected static $defaultName = 'app:export';
}

// APRÈS
#[AsCommand(name: 'app:export', description: 'Exporte les commandes du jour')]
class ExportCommand
{
    public function __invoke(#[Option] bool $dryRun = false): int
    {
        // ...
        return Command::SUCCESS;
    }
}

3. Injection de services taggés

// AVANT
public function __construct(#[TaggedIterator('app.exporter')] iterable $exporters) {}

// APRÈS
public function __construct(#[AutowireIterator('app.exporter')] iterable $exporters) {}

4. Entité utilisateur

Supprimez eraseCredentials() de votre entité User. Si elle stockait un mot de passe en clair le temps de l’inscription, excluez le champ dans __serialize().

Finalisation

À la fin de cette étape, l’application tourne toujours en 7.4, et la suite de tests affiche « 0 deprecations » pour votre code. Les dépréciations indirectes restantes sont listées, bundle par bundle, avec l’état de leur correction en amont.

Étape 5 : Le crash-test en 8.x, sans mise en production

C’est l’étape qui distingue ce guide des précédents : le passage en 8 n’est pas la destination immédiate, la 8.4 n’est pas encore sortie. Mais rien n’empêche de vérifier dès aujourd’hui que le projet passerait.

Créez une branche dédiée, ciblez la dernière mineure 8.x publiée et lancez la mise à jour :

composer update "symfony/*" --with-all-dependencies

Trois issues possibles :

  • Composer refuse. Un bundle n’a pas de version compatible 8.x. Vous avez le nom du bloqueur, et votre tableau de l’étape 1 gagne une ligne à surveiller.
  • La mise à jour passe, les tests cassent. Une dépréciation fantôme que rien n’a loggée ou une signature de méthode surchargée dans votre code. Corrigez en 7.4, puisque le correctif y est compatible, et relancez.
  • Tout est vert. Le projet est prêt pour la 8. Vous ne la déployez pas, mais vous savez que la bascule tiendra.

Nous jouons ce crash-test dans la CI, sur un job non bloquant, à chaque sortie de mineure Symfony. Il coûte quelques minutes de calcul et donne une information que rien d’autre ne donne : la distance réelle entre le projet et la prochaine LTS, mesurée tous les six mois au lieu d’être découverte le jour venu.

Un crash-test vert ne veut pas dire « déployez la 8.2 ». Une mineure 8.x reste une version standard, couverte huit mois. Le crash-test valide la compatibilité, la décision de version reste celle de la section précédente.

Étape 6 : Bascule vers la 8.4 LTS et validation

Le jour où la 8.4 est publiée, si les étapes précédentes ont été tenues, la bascule est une mise à jour de dépendances. Ciblez 8.4.* dans le composer.json, y compris dans la section extra.symfony.require, et lancez :

composer update "symfony/*"
php bin/console cache:clear
make deploy 🔥

Puis déroulez la checklist de validation, la même qu’à chaque étape :

  • Analyse statique : PHPStan et Psalm au niveau du projet, sans nouvelle erreur. Les deux outils ne remontent pas les mêmes choses, nous expliquons pourquoi nous les faisons tourner ensemble.
  • Tests unitaires et d’intégration : 100 % verts, avec une attention aux flux qui traversent Messenger, le Serializer et les formulaires, là où les signatures ont le plus bougé.
  • Schéma Doctrine : php bin/console doctrine:schema:validate.
  • Recette manuelle sur les parcours critiques, Profiler ouvert.
  • Scan de sécurité : un passage de ZAP Proxy sur l’environnement de recette, comme pour tout audit en boîte noire. Une majeure change des en-têtes, des cookies, des comportements par défaut : c’est le scan qui le dit, pas la suite de tests.
  • Image Docker : l’image de production embarque une nouvelle version de PHP et de ses extensions. Passez-la à OSV Scanner avec le reste de l’outillage Dockerfile, pour ne pas livrer une CVE connue avec le framework à jour.
  • Dépréciations : la 8.4 embarque celles du cycle 8.x. Relancez --display-deprecations : le compteur repart, et c’est le début de la préparation de la 9.

L’occasion de solder le front

Une montée de majeure mobilise l’équipe sur le projet, ouvre une fenêtre de recette complète et met l’attention sur les dépendances. Si ce n’est pas déjà fait, c’est le moment de traiter deux chantiers front que l’on repousse d’habitude parce qu’ils ne justifient pas une recette à eux seuls :

  • Webpack Encore vers Vite : Symfony a fait d’AssetMapper sa solution par défaut, et Encore n’est plus qu’une option maintenue. Sur un projet avec un vrai front, Vite a divisé notre build de production par quatre et ramené le démarrage en développement sous la seconde. Notre guide de migration Webpack vers Vite détaille le passage.
  • Tailwind CSS 3 vers 4 : le moteur a changé, la configuration passe en CSS, et les classes renommées se détectent avec l’outil de migration officiel. Notre guide de migration Tailwind v3 vers v4 liste les cas qui demandent une intervention manuelle.

Les deux se recettent en même temps que la migration Symfony, sur les mêmes parcours, avec les mêmes personnes : le coût marginal est faible, alors qu’isolés ils demanderaient chacun leur propre cycle de validation.

Les conseils de SmartBooster

Ne réservez pas cette checklist à la fin. Chaque étape de ce guide se termine par une suite de tests verte et une analyse statique propre. Une migration qui avance sur des tests rouges « à corriger plus tard » accumule exactement la dette qu’elle prétend résorber.

Conclusion

Symfony 8 n’apporte rien qu’un projet en 7.4 n’ait déjà. Ce qu’il apporte, c’est un cycle : deux ans de mineures, de nouveautés et de dépréciations, qui se referme sur la 8.4 LTS en novembre 2027. Pour un projet en production, la bonne lecture n’est pas « faut-il passer en 8 ? » mais « serons-nous prêts pour la 8.4 ? ».

La réponse se construit maintenant, en 7.4 : PHP 8.5 en place, dépréciations traitées, bundles audités, crash-test vert dans la CI. Un projet dans cet état bascule vers la 8.4 le mois de sa sortie, sans projet de migration, dans le lot courant de sa maintenance. C’est la différence entre suivre les versions et les subir, et nous détaillons pourquoi un numéro de version ne dit rien par lui-même sur les technologies que nous suivons projet par projet.

Votre application est en Symfony 6.4 ou 7.x et vous voulez savoir où elle en est ? Notre expertise en migration Symfony commence par cet état des lieux : version, PHP, bundles, dépréciations, et un calendrier réaliste jusqu’à la 8.4.

Liens utiles

Mathieu Ducrot
Mathieu Ducrot CTO
Mots clés :
#Qualité#Productivité

Articles similaires

Vous avez un projet ?

Contactez-nous pour savoir comment nous pouvons vous aider.