Comment migrer un projet legacy Symfony?
Publié le
14 mai 2023
Symfony est un framework qui s’est installé dans l’écosystème PHP depuis 2005 (date de publication de la v1). Bien qu’à l’époque, cette version initiale ait apporté avec elle son lot d’innovations et a permis de faire avancer la communauté vers des standards plus établis, le framework a réellement pris son envol avec le lancement de sa v2 en 2011. Il n’a ensuite pas cessé de s’améliorer en suivant un calendrier de releases très clairement défini (https://symfony.com/releases#symfony-releases-calendar) pour atteindre à ce jour la version 6.
Bien qu’il soit extrêmement rare d’observer des applications utilisants toujours la version 1 du framework (devenu complètement désuète aujourd’hui), il existe tout de même de nombreuses applications toujours basées sur la version 2 de Symfony (bien qu’elle ne soit plus maintenue depuis Novembre 2019 😱).
C’est dans ce contexte que nous avons eu a traiter plusieurs demandes d’accompagnement de nouveaux clients cherchants remettre à niveau leurs applications Symfony vers une version stable et maintenue. Au fil de ces missions, nous avons mis au point un certain nombre de point de contrôle permettant de mieux structurer nos interventions et rationaliser le temps investi dans cette migration.
SPOILER ALERT: Il ne s’agit pas d’une recette magique, ce genre d’intervention est généralement très longue et fastidieuse, nécessite de faire des compromis et demeure une source de régressions potentielles extrêmement importante.
État des lieux
Cette étape consiste à faire l’inventaire des librairies utilisées sur le projet puisque leurs mises à jour va constituer le fil conducteur de notre migration. Nous tâcherons également de mettre en place un environnement isolé et reproductible afin de limiter au maximum les potentiels effets de bords provenants de source externe au projet. Enfin, il faudra procéder à un audit des tests automatisés déjà en place sur le projet (s’il y en a 😅) afin de determiner un indice de confiance que l’on peut leur attribuer.
1. Détecter, lister et évaluer les dépendances utilisées
Il s’agit ici d’établir une liste exhaustive des dépendances du projet, comportant les informations suivantes:
- Le nom de la librairie
- La version actuellement utilisée sur le projet
- La dernière version disponible pour cette librairie
- Cette dernière version est-elle compatible avec la version de Symfony visée ?
- Cette dernière version est-elle compatible avec la version de PHP visée ?
- Est-ce que la librairie est toujours maintenue ?
Nous appelons cette liste la “matrice des dépendances”. Elle permettra alors d’identifier plus clairement:
- Quelles librairies doivent être remplacées ?
- Quelles librairies doivent être mises à jour ?
2. Mettre en place un environnement de développement isolé et reproductible
Il s’agit ici de reproduire un environnement de développement le plus proche possible de la production. Il doit être stable et reproductible sur tous les postes des intervenants du projet. En effet, le premier intérêt de créer un tel environnement est de s’assurer qu’aucun paramètre extérieur ne peut altérer l’application. Ceci permettra alors d’obtenir une meilleure visibilité sur les éventuels bugs et incohérences fonctionnelles qui pourraient surgir suite à des mises à jour de librairies.
Il existe plusieurs solutions techniques permettant de répondre à ce besoin, et dans notre cas, nous utilisons docker la plupart du temps. (Vous pouvez également voir cet article Comment dockeriser une application Symfony ?
3. Mettre en place une préproduction ou d'une staging
Dans la plupart (pour ne pas dire l’intégralité 🥲) des missions sur lesquelles nous sommes intervenus, la suite de tests automatisés était très loin d’être satisfaisante. Il est alors nécessaire d’effectuer des validations fonctionnelles (manuelles donc) par le client afin de valider à chaque étape qu’aucune régression n’a été introduite.
Pour effectuer cette validation dans de bonne conditions, il faudra alors disposer d’un environnement en ligne dédié à ces tests et qui soit le plus proche possible de l’environnement de production.
4. Lister les fonctionnalités qui sont déjà cassées
Cela peut paraître risible au premier abord, mais il n’est pas rare de trouver dans ce type de projet des fonctionnalités déjà cassées. Il est important d’en prendre conscience pour décider, le cas échéant, la manière de traiter ces erreurs.
5. Rendre la codebase (plus) confortable
À ce stade, nous n’avons encore rien modifié, mais nous avons une idée un peu plus précise du chantier à venir et nous sommes en mesure de faire tourner le projet sur un environnement stable et reproductible sur plusieurs postes de travail.
L’objectif est de nettoyer tout ce qui peut l’être dès à présent, sans entamer la moindre mise à jour de dépendance. Voici les différents axes sur lesquels nous portons notre attention:
5.1 Coding Style
Même si cet aspect peut paraître anecdotique pour certains il est à notre sens indispensable. En effet, disposer d’une code base uniforme en terme de coding style est un atout non négligeable dans ce genre de chantier. Votre attention aura alors tout le loisir de se concentrer sur d’autres sujets bien plus important (et il y en aura beaucoup !). De plus, le temps de mise en place d’un outil comme php-cs-fixer pour automatiser cet aspect est absolument négligeable comparé à la charge de travail globale de votre migration.
5.2 PHPStan (ou équivalent)
Si vous ne connaissez pas cet outil alors on vous encourage vivement d’aller y jeter un oeil https://phpstan.org/ ! Pour résumer PHPStan est un outil d’analyse de code statique qui permet de valider un certains nombre de règles syntaxiques et logiques liées exclusivement au language PHP. En quelque sorte, c’est un garde fou qui permet de combler la permissivité de PHP.
Il existe (à notre connaissance) deux alternatives à cet outil: Psalm et Phan. Nous n’allons pas ici tenter d’exposer les avantages et inconvénients de ces différentes solutions, mais elles couvrent globalement le même besoin. L’importance est surtout d’utiliser l’une d’entre elles afin de repérer facilement les incohérences dans le code.
Le grand avantage de ces solutions, c’est qu’il n’est pas nécessaire d’écrire des tests pour les mettre en place. Un fichier configuration succinct suffit à lancer l’analyse sur l’intégralité de la codebase ! De plus, il est possible d’augmenter graduellement le niveau d’analyse pour améliorer progressivement la qualité du code et ne pas avoir à traiter des centaines d’erreurs d’une seule traite.
En revanche, ici commence vraiment votre aventure ! En effet, il est hautement probable que PHPStan mette en évidence des centaines d’erreurs dès les premiers niveaux (et cela ne va pas aller en s’améliorant). À titre informatif, le dernier projet sur lequel nous sommes intervenu comportait plus de 17 000 erreurs au total ! Toutes ces erreurs ne sont pas nécessairement synonymes de bugs et proviennent dans la grande majorité des cas d’un manque de typage dans la codebase.
Pourquoi est-ce si important d’en tenir compte ? Dans une application sans tests automatisés fiables, il sera vraiment difficile d’assurer la non régression fonctionnelle du projet. En revanche, grâce à PHPStan, on peut au moins valider la cohérence technique du code. Et ça c’est un atout considérable !
Pour illustrer ce propos, prenons le problème à l’envers, imaginons un projet idéal où toutes ces incohérences techniques ont déjà été traitées et où l’intégralité de l’application dispose d’un typage fort, à tout les niveaux. Lorsque l’on tentera de mettre à jour une dépendance, même si cette mise à jour implique des breaking changes, il suffira de lancer une analyse de PHPStan et il nous indiquera automatiquement tous les endroits du code où des incohérences ont été introduites. Il sera alors beaucoup plus simple de les corriger. Cela ne dispense pas de vérifier la cohérence fonctionnelle de la mise à jour, mais on peut alors se concentrer sur ce qui est réellement important: le métier !
Protip: PHPStan (ainsi que Psalm) dispose d'une fonctionnalité de baseline qui vous permettra d'enregistrer toutes les erreurs déjà existantes dans votre application dans un fichier "phpstan-baseline.neon". Cela vous laissera alors la flexibilité de corriger ces erreurs au rythme qui vous conviendra le mieux tout en vous protégeant de l'introduction de nouvelles erreurs.
L'acceptation
Un titre volontaire provocateur qui fait référence aux étapes du deuil... Mais oui, il s'agit de ça ! Il va falloir accepter que vous ne pouvez pas sauver le monde et que votre objectif, pour le moment, c'est de mettre à jour Symfony, pas de corriger l'intégralité des problèmes sur l'application. On ne dit pas qu'il ne faut pas corriger ces problèmes à terme, simplement qu'il faut se concentrer sur un objectif à la fois.
Voici donc quelques exemples de points sur lesquels nous avons appris à faire des concesssions:
Faire le deuil des tests bancales
Qu'entendons-nous par bancale ? Globalement tout les tests qui n'aident pas vraiment et qui, par exemple:
- Cassent aléatoirement.
- Sont couplés à d'autres tests (ex: si j'exécute de le test A puis le test B, ça passe, mais si j'exécute le test B puis A, ça ne passe pas).
- Ne testent rien (oui ça arrive plus souvent qu'on ne le pense).
- Ne permettent pas vraiment de comprendre ce qui est cassé.
Tout ces types de tests poluent notre perception de la non régression du projet. S'il est possible de corriger ces tests afin qu'ils soient plus pertinents, c'est évidemment le meilleur des scénario. En revanche, si ce n'est pas possible, alors nous suggérons de tout bonnement les supprimer. Nous les remplacerons par la suite par des tests plus pertinent.
Accepter les regressions en préproduction
Malgré toute notre bonne volonté et notre sérieux, si le projet ne dispose pas d'une couverture de tests satisfaisante, alors il y aura forcément un moment où des regressions seront introduites dans le projet. Cependant, si les prérequis évoqués plus tôt dans cet article ont été respecté, vous aurez toujours une preproduction à disposion, se posant en dernier filet de sécurité avant la mise en production. C'est ici qu'il faudra alors tenter de relever tous les problèmes encore présents dans l'application.
Entendons nous bien, nous sommes pas en train de vous expliquer que la préproduction est un garde fou à toutes épreuves. Mais, dans ce contexte, nous la percevons comme la dernière opportunité de déceler des bugs avant la mise en prod. Il s'agit simplement d'avoir conscience que notre stack de tests n'est pas satisfaisante et que la recette en préproduction ne doit pas s'effectuer comme une fonctionnalité.
Notre conseil, c'est de bien préparer le client à cet aspect et de l'avertir qu'à ce stade, il existe des chances non négligeables que des bugs (techniques ou fonctionnels) résident toujours dans l'application. Il aura donc, lui aussi, un rôle actif à jouer dans la détection de ces derniers.
Pour finir sur ce point, parlons du sujet qui fâche, mais l'erreur est humaine et il y aura vraissemblablement des régressions aussi en production... Encore une fois, on ne doute pas de votre professionalisme et votre investissement, mais même un projet très bien testé et à jour n'est pas à l'abris d'avoir des bugs en prod. Alors sur un projet legacy avec une couverture de test médiocre, la probabilité s'approche de la certitude. C'est ici que l'on parle vraiment d'acceptation. Nous avons tout de même quelques solutions paliatives pour limiter la casse.
Tout d'abord, vous pouvez établir conjointement avec le client, la liste des fonctionnalités qui ne doivent impérativement pas regresser (bien évidemment, cette liste ne peut pas être consituée de l'intégralité de l'application 😅). Vous pourrez alors éventuellement mettre en place un scénario de test manuel, permettant de vérifier systématiquement ces fonctionnalités lors des tests en preproduction. Si le contexte vous le permet, il serait encore plus judicieux d'écrire de véritables tests automatisés pour ces fonctionnalités (ce qui semble être de loin, la meilleure stratégie).
La mise en place d'outils de monitoring (tels que Sentry, Rollbar, Datadog, etc.) sur vos différents environnements vous permettra d'être proactif sur les erreurs éventuelles que rencontrerons les utilisateurs. Ils vous donnerons d'ailleurs des informations bien plus précises et détaillées que les utilisateurs eux-même.
Dependabot
- cleaner le
composer.json
: restreindre les requirements de version avant de commencer la montée en version - first step : monter toutes les versions patch des libraires
- se dissocier de la dependence
symfony/symfony
et se baser sur chaque composant - second step : essayer de monter les versions mineurs au max
- rester focus sur la montée de version : ne pas essayer de faire de refacto de code majeur
La boucle
Vous avez bien compris, pour avancer la migration, c'est step by step : Detection - Modification - Test. Et y aller doucement.
Estimation
Quand votre client vous demande une estimation, il serait plus judicieux de proposer une estimation par étape. L'état des lieux, analyser les librairies dépréciées, est-ce qu'il y a des testes cohérents ? Et ensuite souligner qu'une migration est du "best effort".
Par exemple, pour une code base assez clean, mais avec aucun teste, on a mit 5 semaines pour monter de Sf 3.4 à 4.4 et php 7.2.
Essayez d’éduquer vos clients et managers pour leur faire prendre conscience que la refacto est un gain de temps sur le long terme. Faire des migrations au fur et à mesure fait gagner de l'argent à long terme.
Notre résumé
Pas une tâche facile et plaisante. Si vous avez une CEO aussi sympa que la nôtre, elle prévoit pour vous un séjour en gîte entre collègues pour rendre la chose plus plaisante justement.
On vous souhaite dans tous les cas bon courage et de l’énergie.
Commentaires