Refonte totale d'une application Symfony 3.1 obsolète vers Symfony 6.4, avec 96 % de couverture de tests

Scores de cette page :

EcoIndex A
SEO 100%
Accessibilité 100%
Performance 100%
Bonnes Pratiques 100%

Nouveau site en cours de finalisation ! Cette page sera progressivement complétée.

Projet de formation (OpenClassrooms) : reprise d'un MVP Symfony 3.1 non maintenable pour ToDo & Co, gestionnaire de tâches. Choix d'une refonte totale plutôt qu'un correctif, vers Symfony 6.4 LTS : authentification et rôles utilisateur/administrateur, suppression en cascade des tâches à la suppression d'un compte, 96 % de couverture de tests (PHPUnit), note Codacy passée de C à A, documentation technique et guide de contribution.

Contexte : d'un prototype bricolé à une vraie application

Projet réalisé en formation (parcours PHP/Symfony, spécialisation OpenClassrooms). Point de départ : ToDo & Co, une jeune entreprise qui propose un outil de gestion de tâches, avait fait développer une première version très rapidement — un « MVP » (Minimum Viable Product, ou « produit minimum viable »), c'est-à-dire une version volontairement minimale, juste assez fonctionnelle pour convaincre des investisseurs de financer la suite. Ce genre de version est sciemment codé vite, sans forcément soigner la qualité, la sécurité ou la solidité : l'objectif est de prouver qu'une idée fonctionne, pas de livrer un produit fini. Ce choix, assumé au départ pour Symfony 3.1, a rempli son rôle : la startup a levé des fonds.

Une fois les fonds obtenus, il fallait transformer ce prototype en application fiable, sécurisée et prête à accueillir de vrais utilisateurs. Le travail demandé couvrait quatre volets précis : corriger les anomalies existantes du MVP (notamment l'absence de lien entre une tâche et son créateur), implémenter de nouvelles fonctionnalités (rôles utilisateur/administrateur et autorisations associées), mettre en place des tests automatisés pour garantir la fiabilité du code, et produire une documentation technique ainsi qu'un audit de qualité et de performance.

Pourquoi tout reconstruire plutôt que réparer

Un site web repose sur un « framework », une boîte à outils logicielle qui fournit la structure de base du code (ici, Symfony). Comme tout logiciel, un framework évolue par versions, et les anciennes versions finissent par ne plus être maintenues par leurs créateurs : plus de corrections de failles de sécurité, plus de documentation à jour, plus de support en cas de problème. La version utilisée pour le MVP, Symfony 3.1, n'était plus maintenue depuis le 1er janvier 2017 — plusieurs années avant même le début de ce projet.

Continuer à faire évoluer l'application sur cette base aurait été comme rénover une maison pièce par pièce alors que les fondations elles-mêmes sont fissurées : chaque réparation prend plus de temps qu'elle ne devrait, et le risque de complications s'accumule. Le déclencheur concret de la décision : au moment de vouloir déployer l'application via Docker (un outil qui permet de faire tourner un projet dans un environnement standardisé, identique sur toutes les machines), la version actuelle de Docker Compose ne prenait tout simplement plus en charge Symfony 3.1 — il aurait fallu superposer une couche de compatibilité supplémentaire rien que pour faire fonctionner l'outillage de base.

Décision prise dès le départ du projet plutôt qu'en cours de route, en pesant plusieurs critères concrets : l'obsolescence technologique (risques de sécurité, bugs jamais corrigés, absence de ressources et de forums pour se dépanner), les performances (les versions récentes de Symfony gèrent mieux la mémoire et le traitement des requêtes), et le temps de développement — patcher une base aussi datée aurait au final pris plus de temps que reconstruire directement sur Symfony 6.4, une version à support prolongé (LTS, Long Term Support), garantissant plusieurs années de maintenance sans avoir à tout refaire.

Qui peut faire quoi dans l'application

Avant la refonte, l'application ne faisait aucune distinction entre les utilisateurs : n'importe quel compte connecté avait accès à tout, y compris à la gestion des autres comptes. Deux niveaux d'accès ont été créés : un rôle « utilisateur » standard (ROLE_USER), attribué par défaut à toute personne inscrite, qui peut créer, modifier et gérer ses propres tâches ; et un rôle « administrateur » (ROLE_ADMIN), qui peut en plus créer, modifier et supprimer les comptes des autres utilisateurs.

Ces restrictions ne sont pas de simples boutons masqués à l'écran pour les personnes non autorisées — ce serait une protection très fragile, contournable en tapant directement une adresse dans le navigateur. La vérification est faite directement par le serveur à chaque tentative d'action, avant même que la page ne s'affiche, via l'attribut #[IsGranted('ROLE_ADMIN')] de Symfony posé sur chaque route sensible du contrôleur de gestion des utilisateurs. Concrètement, quelqu'un qui tenterait d'accéder directement à une page de gestion des comptes sans y être autorisé se verrait refuser l'accès quoi qu'il arrive.

Relier chaque tâche à la personne qui l'a créée

Dans le MVP, les tâches n'étaient reliées à personne : impossible de savoir qui avait créé quoi, et donc impossible de restreindre qui pouvait la modifier ou la supprimer. Une relation author a été ajoutée sur l'entité Task (la structure de données représentant une tâche), renseignée automatiquement avec l'utilisateur connecté au moment de la création de la tâche — sans possibilité de la modifier ensuite, pour garder une trace fiable de qui a vraiment créé quoi.

Une difficulté technique concrète s'est posée : des tâches existaient déjà en base de données avant ce changement, sans aucun auteur. Rendre cette information obligatoire au niveau de la base de données aurait empêché l'application de fonctionner correctement avec ces tâches historiques dès le premier chargement. La contrainte « auteur obligatoire » a donc dû être retirée du schéma de la base de données (le champ author_id reste facultatif, techniquement NULL autorisé), pour que ces tâches restent utilisables — traitées comme appartenant à un auteur « anonyme » implicite, visibles et gérables uniquement par les administrateurs.

Que se passe-t-il quand on supprime un compte

Supprimer un utilisateur qui a des tâches en cours pose une question simple mais importante : que deviennent ses tâches ? Le choix fait ici : elles sont supprimées avec lui, pour éviter d'accumuler des tâches orphelines dans l'application au fil du temps.

Techniquement, comme la référence à l'auteur n'est pas obligatoire en base de données (voir plus haut), ce nettoyage ne pouvait pas se faire automatiquement via une règle de suppression en cascade au niveau du schéma — cela aurait nécessité une contrainte stricte incompatible avec les tâches sans auteur. Il fallait donc l'écrire explicitement dans le code du contrôleur : avant de supprimer un compte utilisateur, l'application va d'abord chercher toutes les tâches qui lui sont associées et les supprime une par une, avant de supprimer le compte lui-même.

Authentification générée puis documentée pour les futurs développeurs

Le mécanisme de connexion (authentification) n'a pas été codé entièrement à la main : Symfony fournit un générateur de code appelé « Maker Bundle », qui, via des commandes en ligne de commande (make:user, make:auth), crée automatiquement l'ensemble des fichiers nécessaires — le contrôleur de sécurité, la structure de données de l'utilisateur, le mécanisme de vérification des identifiants, et le fichier de configuration security.yaml qui centralise les règles de sécurité du site.

Générer ce code automatiquement fait gagner du temps, mais présente un risque : les futurs développeurs qui reprendront le projet ne sauront pas forcément ce que fait chacun de ces fichiers générés, ni lesquels ils peuvent modifier sans risque. Une documentation technique dédiée a donc été rédigée spécifiquement pour combler cet écart, à destination de développeurs encore juniors sur Symfony : quels fichiers modifier et pourquoi, comment se déroule concrètement le processus de connexion d'un utilisateur, et où sont stockés les comptes utilisateurs dans le système — un livrable explicitement demandé par le brief du projet.

Comment on sait que ça marche vraiment : les tests automatisés

Plutôt que de vérifier manuellement, à chaque modification du code, que rien n'est cassé ailleurs dans l'application, des « tests automatisés » ont été écrits — des petits programmes qui vérifient eux-mêmes que chaque fonctionnalité se comporte exactement comme prévu. Deux types de tests ont été mis en place avec PHPUnit : des tests unitaires, qui vérifient isolément chaque fonction des structures de données (par exemple, que le champ « auteur » d'une tâche se lit et s'écrit correctement), et des tests fonctionnels, qui simulent un vrai parcours utilisateur à travers les contrôleurs (que se passe-t-il si on est connecté ou non, administrateur ou non, en tentant chaque action de l'application).

Ces tests ont besoin de données pour fonctionner sans dépendre de la vraie base de données de production. Deux bases de données séparées ont donc été créées, une pour le développement quotidien, une dédiée uniquement aux tests, chacune peuplée de fausses données réalistes générées automatiquement par la bibliothèque Faker (de faux noms, emails et tâches, produits aléatoirement mais avec un format cohérent).

Ces tests peuvent être relancés à tout moment, en quelques secondes, pour s'assurer qu'une nouvelle modification n'a rien cassé ailleurs — un filet de sécurité qui aurait fait défaut au MVP. La « couverture de code » mesure la proportion du code de l'application qui est effectivement vérifiée par ces tests, en pourcentage : plus elle est élevée, plus la surface du comportement de l'application est surveillée automatiquement. Le rapport d'audit final de ce projet indique une couverture de 96,53 %, très au-dessus du minimum de 70 % exigé par le brief.

Le bilan qualité, en chiffres

Trois outils différents ont mesuré la qualité et la rapidité de l'application, avant et après la refonte, pour objectiver l'amélioration plutôt que de l'affirmer sans preuve :

Codacy, un outil qui analyse automatiquement le code source à la recherche de mauvaises pratiques (style d'écriture, code inutilisé, risques potentiels), attribuait une note C à l'ancienne version, avec 29 % d'éléments signalés comme problématiques. Après la refonte, la note est passée à A, avec seulement 3 % de signalements mineurs restants — 52 au total, répartis entre 47 questions de style d'écriture du code et 5 signalements de « code apparemment inutilisé » qui correspondent en réalité à des déclarations requises par le fonctionnement interne de Symfony (donc de fausses alertes de l'outil, pas de vrais défauts).

Lighthouse, l'outil de Google qui évalue la rapidité de chargement et la qualité générale d'un site, donne un score de 97 à 100 sur 100 sur la majorité des pages. Une seule page ressort à 83 sur 100, avec une cause identifiée et déjà documentée comme piste d'amélioration : des feuilles de style Bootstrap chargées en quasi-totalité alors que seule une petite partie de leurs fonctionnalités est utilisée, et des images qui gagneraient à être converties au format WebP, plus léger.

Le Symfony Profiler, un outil intégré à Symfony qui mesure précisément le temps que met le serveur à traiter chaque page ainsi que la mémoire consommée, montre un temps de réponse maximum observé de 225 millisecondes — largement sous le seuil à partir duquel un utilisateur perçoit une lenteur. Une piste de vigilance a été notée pour l'avenir : si le volume d'utilisateurs et de tâches venait à augmenter fortement, une mise en cache des données pourrait devenir nécessaire pour conserver ce niveau de réactivité.

Comment le travail a été organisé

Le travail a été découpé en grandes étapes (mise en place du projet, correction des anomalies, nouvelles fonctionnalités, tests, documentation et audit), suivies sur GitHub sous forme de jalons (« milestones »), eux-mêmes divisés en tâches précises (« issues »). Pour chaque jalon, une branche de code dédiée était créée ; pour chaque tâche, le code était développé séparément puis soumis à une relecture (« pull request ») avant d'être intégré définitivement au code principal du projet — une discipline qui limite le risque d'introduire un problème sans s'en apercevoir, et qui permet une validation continue plutôt qu'une correction de dernière minute.

La qualité du code et la rapidité de l'application n'ont pas été vérifiées uniquement à la fin du projet : Codacy était connecté en continu au dépôt de code, permettant de corriger les problèmes de style au fur et à mesure plutôt que de les laisser s'accumuler, et des vérifications Lighthouse régulières permettaient de détecter tôt d'éventuels ralentissements.

Transmettre le travail aux prochains développeurs

Au-delà du code et de la documentation d'authentification, un second document a été rédigé — un guide de contribution (CONTRIBUTE.md) — qui explique comment toute personne souhaitant proposer une modification au projet doit procéder : comment récupérer une copie personnelle du projet, créer une branche dédiée à sa modification, ouvrir une nouvelle tâche de suivi, puis soumettre son travail pour relecture avant intégration. Ce document précise aussi les exigences de qualité et de sécurité à respecter, pour que les contributions extérieures restent cohérentes avec le reste du projet, ainsi qu'un rappel de patience : chaque proposition doit être validée avant d'être intégrée, pas fusionnée automatiquement.

Pistes d'amélioration identifiées pour la suite

Le brief ne demandait pas de corriger la dette technique restante, seulement de l'auditer — plusieurs pistes ont malgré tout été documentées pour une prochaine itération : poursuivre l'optimisation des performances (nettoyage et minification des fichiers CSS/JS, conversion des images), retravailler l'interface visuelle (jugée fonctionnelle mais perfectible), permettre la récupération d'un mot de passe oublié, et envisager des fonctionnalités plus ambitieuses comme un calendrier intégré ou une synchronisation avec Google Agenda.

Note

Une limite assumée, signalée telle quelle lors de la soutenance plutôt que dissimulée : la gestion des adresses qui ne correspondent à aucune page de l'application (l'équivalent d'une « page introuvable », ou erreur 404) n'a pas été implémentée dans le temps imparti au projet. La solution est identifiée et documentée pour une prochaine itération : ajouter la route correspondante et un gabarit d'affichage dédié.