PHPStan sur un code legacy: le retour d’expérience de Julie
», mais plutôt: « comment faire entrer PHPStan dans cette application qui a connu PHP 5, trois prestataires, deux refontes visuelles et un dossier includes/ dont personne ne prononce le nom sans baisser les yeux? »
C’est là que le fantasme de l’outil qualité de code PHP qui débarque, lance son analyse et révèle instantanément la vérité absolue devient dangereux. Sur un projet ancien, PHPStan ne dit pas seulement « il y a des erreurs ». Il met surtout en lumière ce que le projet raconte déjà: contrats implicites, tableaux fourre-tout, classes chargées par magie, PHPDoc fossilisé, variables qui changent de type au fil d’une méthode. Bref, du legacy vivant.
Le « retour d’expérience de Julie » doit être pris pour ce qu’il est ici: un cas pédagogique. Aucun témoignage public vérifiable ne permet d’identifier une Julie, son dépôt, son CMS ou son volume initial d’erreurs. Mais le scénario, lui, est très réel: une développeuse arrive sur une base mature, veut introduire l’analyse statique PHP sans bloquer l’équipe, et découvre que le vrai boulot commence bien avant le niveau 9.
Sur un code legacy, PHPStan n’est pas un tribunal. C’est une lampe torche: le but n’est pas d’éclairer tout le sous-sol d’un coup et de prendre peur.
Pourquoi l’analyse statique fait-elle autant de bruit dans une vieille base PHP?
Parce qu’un projet ancien fonctionne souvent grâce à des conventions qui n’existent nulle part ailleurs que dans les têtes — ou dans une conversation Slack de 2018, perdue entre une alerte de déploiement et un débat sur les tabs.
Un contrôleur récupère un tableau issu d’une requête SQL. Une fonction y ajoute une clé. Un template suppose que cette clé existe. Une classe est instanciée avec un alias historique. Une librairie maison charge des fichiers selon l’URL courante. Et, miracle: en production, ça tient. La plupart du temps.
PHPStan arrive avec une demande assez radicale: « Montre-moi les types. Montre-moi d’où vient cette classe. Montre-moi pourquoi cette clé de tableau est forcément présente. »
Sur un code moderne, avec Composer, des signatures typées et un découpage cohérent, la discussion est fluide. Sur une application legacy, l’outil se heurte d’abord à l’architecture réelle, pas à la qualité des développeurs. Nuance essentielle.
Les premières erreurs PHPStan legacy entrent généralement dans quatre familles:
- des erreurs de typage authentiques: appel de méthode sur une valeur potentiellement nulle, entier passé là où une chaîne est attendue, retour de fonction incohérent;
- des contrats invisibles: un tableau contient bien certaines clés, mais rien ne le documente;
- des symboles introuvables: classes, fonctions, traits ou constantes chargés par un mécanisme que PHPStan ne connaît pas encore;
- des imprécisions externes: une dépendance expose des PHPDoc incomplets ou erronés, et l’analyseur fait logiquement ce qu’on lui a demandé de faire.
C’est précisément pour cela qu’on ne traite pas les premiers résultats comme une to-do list brute. Une erreur d’analyse statique PHP n’est pas automatiquement un bug prêt à exploser à 9 h 03 lundi matin. Parfois, c’est un bug. Parfois, c’est une dépendance mal décrite. Parfois, c’est l’autoloader qui joue à cache-cache. Et parfois, oui, c’est juste le code qui s’est habitué à l’implicite.
Julie ne commence donc pas par promettre à l’équipe « zéro erreur vendredi ». Elle commence par créer une frontière: ce qui existe déjà d’un côté, ce que l’on ajoute à partir d’aujourd’hui de l’autre.
Faut-il vraiment démarrer au niveau 9?
Non. Et c’est probablement l’une des meilleures nouvelles quand on cherche à poser PHPStan sur du code legacy.
PHPStan propose 11 niveaux de règles, de 0 à 10. Les contrôles sont cumulatifs: monter d’un niveau ne remplace pas le précédent, il ajoute des exigences. Le niveau 0 est le moins strict et sert de niveau par défaut si aucun niveau n’est précisé. Le niveau 10 est le plus exigeant.
La tentation du niveau 9 est compréhensible. On a lu quelques posts, vu des configurations ultra-propres sur GitHub, et on veut ce badge mental: « notre code est sérieusement analysé ». Sauf qu’une configuration PHPStan niveau 9 sur une base qui ne sait pas encore résoudre ses propres classes, c’est comme lancer une refacto de domaine alors que le projet ne démarre qu’avec une variable d’environnement oubliée. Techniquement courageux. Opérationnellement, chaotique.
La progression saine ressemble plutôt à ça:
1. Faire tourner l’analyse de façon reproductible. Avant de parler de qualité, on s’assure que la même commande donne le même résultat sur les postes et dans l’intégration continue. Cela implique le bon répertoire analysé, les exclusions cohérentes et surtout la résolution des symboles applicatifs.
2. Démarrer avec un niveau bas. Le niveau 0 n’est pas une honte. C’est un point d’entrée. On corrige les diagnostics qui sont immédiatement lisibles et on apprend la manière dont PHPStan perçoit le projet.
3. Monter après une phase de correction ciblée. Chaque hausse de niveau doit produire une discussion utile. Si elle déclenche une avalanche de messages indifférenciés, on n’a pas gagné en rigueur: on a juste déplacé de la dette dans l’interface du terminal.
4. Mettre les nouvelles modifications sous contrôle. L’objectif rapide n’est pas de rendre parfaite une application née il y a dix ans. Il est d’empêcher que les nouveaux fichiers et les nouvelles branches reproduisent les angles morts d’hier.
5. Durcir là où le code bouge. Un module de paiement, un domaine métier en refonte, une API récemment extraite: voilà de bons candidats pour une exigence plus haute. Tout le monolithe n’a pas besoin d’atteindre la même maturité le même mois.
| Niveau de maturité du projet | Approche PHPStan pertinente | Ce que l’équipe cherche vraiment |
|---|---|---|
| Application très implicite, chargements maison | Niveau bas, configuration de découverte des symboles | Obtenir une analyse fiable |
| Base stable mais très endettée | Niveau progressif avec dette isolée | Ne plus laisser entrer de nouveaux problèmes |
| Modules activement refactorés | Règles plus strictes sur le périmètre concerné | Rendre les contrats explicites |
| Projet récent, types déjà présents | Niveaux élevés, correction continue | Prévenir les régressions fines |
Le rythme compte plus que le chiffre affiché. Une équipe qui passe durablement d’un niveau 2 à un niveau 4, en comprenant ce qu’elle corrige, construit une culture. Une équipe qui force le niveau 9, ajoute une ignore-list géante et oublie l’outil six mois plus tard construit… une décoration de pipeline.
La baseline: une trêve, pas une amnistie
C’est ici que la baseline de PHPStan devient franchement utile.
Sur un projet legacy, on peut générer un fichier qui recense les erreurs présentes au moment où l’on introduit l’outil. La commande --generate-baseline produit généralement un fichier nommé phpstan-baseline.neon. Une fois inclus dans la configuration, ce fichier évite que les erreurs historiques déjà recensées soient remontées à chaque exécution.
Dit autrement: le projet conserve sa dette visible dans un inventaire, mais le bruit ne bloque plus chaque nouvelle contribution.
C’est une mécanique très saine quand elle est utilisée avec honnêteté. Julie peut faire entrer PHPStan dans l’intégration continue sans demander à toute l’équipe d’arrêter le produit pendant trois semaines pour typifier chaque tableau ancestral. Les erreurs connues restent dans la baseline; une nouvelle erreur, elle, fait échouer l’analyse. Le présent cesse d’aggraver le passé. C’est déjà énorme.
Mais attention au petit piège qui transforme une bonne stratégie en tapis sous lequel on pousse les miettes: régénérer la baseline à chaque fois qu’une branche ajoute des diagnostics.
Non.
La baseline n’est pas un bouton « taisez-vous ». Elle ne corrige rien. Elle masque le stock existant afin de mieux surveiller le flux nouveau. La régénérer mécaniquement revient à inscrire la dette supplémentaire dans le patrimoine familial. On a tous connu ce dossier legacy_old_final_v2, pas besoin d’en faire un modèle de gouvernance.
Une baseline bien tenue protège le futur du projet. Une baseline régénérée sans discussion protège seulement notre confort du jour.
La documentation de PHPStan donne un ordre de grandeur utile: la baseline convient bien à quelques dizaines ou quelques centaines d’erreurs. Si l’analyse crache 15 000 diagnostics, le réflexe ne devrait pas être « générons un énorme fichier et passons à autre chose ». Il faut revenir à la configuration, au périmètre analysé, au niveau de règles ou aux symboles non résolus.
Dans un volume aussi massif, une part significative du bruit peut venir d’un problème structurel. Analyser des répertoires de cache, des dépendances non prévues, des fichiers générés ou un ancien module abandonné n’apporte pas une vérité plus pure. Ça apporte du volume. Et le volume est l’ennemi numéro un de l’adoption.
Pour les cas ponctuels, mieux vaut préférer une annotation locale @phpstan-ignore accompagnée d’une justification claire. Pas une incantation vague, pas un « à revoir » sans date ni contexte. Une explication qui dit pourquoi ce cas précis est sûr, temporaire ou impossible à exprimer proprement avec les informations disponibles.
Cette granularité change tout. La baseline décrit une dette historique. L’ignore locale documente une exception présente. Les mélanger, c’est perdre la carte du territoire.
Et quand PHPStan ne trouve même pas les classes?
Bienvenue dans la partie « sous le capot », celle où le projet révèle ses habitudes de chargement.
PHPStan doit être capable de résoudre les classes, interfaces, traits, fonctions et constantes utilisés par le code analysé. Dans un projet propulsé par Composer, le chemin est souvent déjà balisé. Dans une application plus ancienne, les choses se corsent: autoloader maison, fichiers inclus selon l’environnement, alias de classes, constantes définies dans un bootstrap historique, CMS qui charge ses modules à sa manière…
Le premier réflexe serait de lancer l’analyse, de voir « classe inconnue », puis de conclure que PHPStan est trop strict. Mauvais procès. L’outil ne peut analyser correctement qu’un univers de symboles qu’on lui donne réellement à voir.
Plusieurs mécanismes existent pour lui présenter cet univers:
- un fichier d’autoload peut être déclaré avec l’option
--autoload-file; - des fichiers de démarrage peuvent être indiqués dans
bootstrapFiles; - des fichiers ou répertoires supplémentaires peuvent être signalés via
scanFilesetscanDirectories.
Ces options n’ont pas le même rôle. C’est le genre de détail qui évite des heures de fausse piste.
Un fichier ajouté dans bootstrapFiles est réellement exécuté par PHP. C’est pratique pour charger un autoloader, définir des constantes attendues ou enregistrer des alias de classes. Mais c’est aussi une zone à manipuler avec des gants. Si ce bootstrap établit une connexion à une base de données, dépend d’une variable d’environnement absente, modifie l’état global ou appelle un service externe, l’analyse devient fragile — et parfois carrément surprenante.
On veut un bootstrap d’analyse, pas une mini-exécution de production déguisée.
Les options de scan répondent à une autre problématique: faire connaître des symboles à PHPStan sans forcément déclencher les effets de bord d’un fichier de démarrage. Dans un vieux CMS ou une librairie interne organisée en inclusions successives, cette distinction est un vrai game changer.
Julie avance alors en deux temps. D’abord, elle fait la liste des erreurs de symboles inconnus. Ensuite, elle cherche leur origine réelle: autoloader, fichier d’alias, répertoire de fonctions globales, module optionnel, dépendance chargée à la main. Ce travail semble moins glorieux que corriger des types union modernes, mais il a une vertu rare: il documente l’architecture existante.
Et parfois, la découverte est salutaire. Une classe n’est pas trouvée parce qu’elle n’est chargée que dans certains parcours HTTP? Une constante dépend d’un fichier inclus par une page d’administration? Un service n’existe que parce qu’un ordre d’inclusion accidentel le rend disponible? PHPStan n’invente pas le problème. Il retire juste le filtre nostalgique.
Les stubs résolvent-ils tous les problèmes de dépendances?
Pas du tout — mais ils ont une mission très précise.
Les fichiers de stubs permettent de corriger les PHPDoc incomplets ou imprécis d’une dépendance tierce. C’est particulièrement utile quand une bibliothèque expose une API correcte à l’exécution, mais décrit mal ses retours, ses paramètres génériques ou ses tableaux structurés. On peut alors donner à PHPStan une vision plus fidèle de cette API sans modifier directement le paquet installé.
Ce que les stubs ne font pas: remplacer la découverte des symboles.
Si PHPStan dit qu’une classe ou une fonction est introuvable parce que l’autoload du projet est incomplet, ajouter un stub n’est pas une vraie réparation. C’est coller une étiquette sur une porte qui n’existe pas encore dans le plan du bâtiment. Le stub améliore l’information de type; il ne charge pas magiquement les éléments absents de l’environnement d’analyse.
Autre nuance qui compte: PHPStan lit les PHPDoc dans les stubs, pas les types natifs ajoutés aux paramètres ou aux retours. Autrement dit, le stub est un outil de description pour l’analyseur, pas une seconde version exécutable de la bibliothèque.
Cette distinction aide à garder une configuration lisible:
- le chargement et la découverte des classes relèvent de l’autoload, des bootstraps et des répertoires scannés;
- la qualité des informations de type des dépendances relève des stubs;
- les exceptions réellement locales relèvent d’annotations ciblées;
- la dette préexistante relève de la baseline.
Quand chaque outil reste à sa place, la configuration ne devient pas un musée d’astuces incompréhensibles. Et, franchement, dans un projet legacy, c’est déjà une victoire très sous-estimée.
Comment faire de PHPStan un outil d’équipe plutôt qu’un gardien de portail?
Le succès de PHPStan ne se mesure pas au nombre de lignes dans un fichier .neon. Il se mesure au moment où quelqu’un ouvre une demande de fusion, voit un diagnostic et se dit: « Ah oui, ce retour peut être nul ici », au lieu de soupirer et d’ajouter une exclusion par réflexe.
Pour y arriver, Julie installe quelques habitudes simples.
D’abord, elle évite de traiter tous les diagnostics au même niveau de priorité. Un appel sur un objet potentiellement nul dans un parcours métier critique mérite une investigation rapide. Un tableau de configuration ambigu dans un écran rarement touché peut attendre une refacto dédiée. L’analyse statique n’impose pas une feuille de route produit; elle fournit de la matière pour en construire une.
Ensuite, elle fait de chaque correction un micro-contrat explicite. Un tableau qui représente une commande? On documente sa forme. Une méthode qui peut renvoyer l’absence de résultat? On l’assume dans son type et dans l’appelant. Une collection de valeurs homogènes? On arrête de la faire passer pour un mystérieux array générique, héritage sacré de l’époque où tout était un tableau et où personne ne posait de questions.
Enfin, elle protège le temps de l’équipe. Les corrections liées à PHPStan sont parfaites à intégrer:
- pendant une modification déjà prévue dans le même module;
- dans une petite tâche de maintenance attachée à un bug récurrent;
- lors d’une extraction de service ou d’une modernisation de couche;
- dans les zones où les nouveaux arrivants se perdent le plus souvent.
Cette approche fait de l’outil un compagnon de refacto, pas une dette supplémentaire. Le code devient plus expressif à mesure qu’on le touche. Pas besoin de déclarer une grande croisade contre le legacy à chaque sprint — les grandes croisades finissent souvent dans un tableur, juste à côté de la migration « prioritaire » de 2021.
Il faut aussi regarder la compatibilité d’exécution avec lucidité. PHPStan nécessite PHP 7.4 ou plus récent pour s’exécuter. Du code écrit pour PHP 5.6 ou antérieur peut parfois tourner sous PHP 7 avec peu de changements, mais ce n’est jamais une promesse automatique. Avant de brancher l’analyseur dans une chaîne d’intégration continue, on valide l’environnement réel du projet. L’outillage ne doit pas devenir l’occasion de découvrir, à 18 h un vendredi, que le serveur de préproduction vit encore dans une chronologie alternative.
Le vrai gain: rendre le code discutable
Le meilleur effet de PHPStan sur un code legacy n’est pas de faire disparaître une liste rouge. C’est de rendre le code discutable avec des mots précis.
Avant, on disait: « Cette méthode est un peu fragile. » Après quelques semaines d’analyse progressive, on peut dire: « Elle accepte trois formes de tableau, dont une sans clé id, et elle retourne parfois false alors que ses appelants attendent une instance. » Ce n’est pas la même conversation. La première est une impression. La seconde peut devenir une décision de conception.
C’est ça, le vrai retour d’expérience que l’on peut reproduire sans inventer de success story parfaite: commencer petit, configurer proprement, isoler le passif avec une baseline, ne pas masquer les erreurs nouvelles, et monter les niveaux lorsque l’équipe a réellement absorbé le précédent.
Le legacy ne devient pas moderne parce qu’on installe PHPStan. Il devient plus lisible, plus négociable, plus refactorable. Et à l’échelle d’un projet PHP qui doit encore servir des utilisateurs, accueillir de nouveaux développeurs et survivre à sa prochaine évolution, c’est largement mieux qu’un grand nettoyage héroïque.
Alors, niveau 9 dès lundi? Peut-être. Mais seulement après avoir demandé la bonne question: est-ce qu’on cherche un chiffre à afficher, ou un code que l’on comprend enfin un peu mieux?




