jobsphp

Intégration de PHPStan : le retour d'expérience de Maxime

Open Source & Outils. Intégration de PHPStan : le retour d'expérience de Maxime

Une analyse PHPStan sur PrestaShop 8.0 est passée de 9 à 3 minutes après l’évolution de l’outil.

Intégration de PHPStan: le retour d'expérience de Maxime

PHPStan 2.0 a par ailleurs réduit sa consommation mémoire de 50 à 70 % grâce à une refonte de ses structures AST internes et à la suppression de références circulaires. Ce ne sont pas des optimisations cosmétiques: sur une base PHP legacy, la durée et l’empreinte mémoire de l’analyse déterminent directement sa place dans le cycle de développement.

Le problème n’est donc pas de savoir si PHPStan détecte des erreurs. Il le fait. Le problème est d’intégrer l’analyse statique dans un dépôt qui contient déjà des centaines, parfois des milliers, d’anomalies de typage, d’appels ambigus et de contrats implicites sans bloquer immédiatement toute l’équipe.

La stratégie viable repose sur trois mécanismes: une montée progressive dans les niveaux de règles, une baseline pour isoler la dette existante et une exécution automatisée dans la CI/CD. Sans cette séquence, l’intégration de PHPStan devient un audit ponctuel. Avec elle, elle devient un contrôle permanent des régressions.

L’analyse statique comme rempart contre la dette technique

PHPStan analyse le code PHP sans l’exécuter. Il reconstruit les types, suit les appels, inspecte les retours de méthode et vérifie la cohérence des contrats visibles dans le code. Le moteur travaille donc sur la structure du programme, ses annotations, ses signatures et les extensions de connaissance disponibles pour les frameworks et bibliothèques utilisés.

Cette approche ne remplace pas les tests. Elle intervient plus tôt et sur un autre périmètre.

Un test fonctionnel peut confirmer qu’un chemin précis fonctionne avec une donnée précise. PHPStan peut signaler qu’une méthode annonce un retour User, alors qu’un chemin d’exécution renvoie parfois null, même si ce chemin n’a pas été couvert par la suite de tests. Il peut aussi détecter un argument incompatible, une propriété potentiellement non initialisée ou une valeur mixed propagée jusqu’à une couche critique.

Sur une application récente, cette analyse s’intègre généralement au développement dès le départ. Le code est typé, les interfaces sont explicites et les erreurs sont corrigées au fil de l’eau. Un projet patrimonial fonctionne autrement:

  • les méthodes historiques renvoient plusieurs types selon le contexte;
  • les tableaux associatifs remplacent des objets métier;
  • les propriétés sont initialisées indirectement;
  • les annotations PHPDoc contredisent parfois le comportement réel;
  • les dépendances injectées sont masquées par des conteneurs ou des fabriques;
  • les extensions CMS ajoutent des points d’entrée que l’analyse ne comprend pas toujours sans configuration spécifique.

Dans ce contexte, lancer PHPStan au niveau maximal sur l’ensemble du dépôt produit une photographie honnête, mais difficilement exploitable. Les erreurs nouvelles sont noyées dans l’historique. Le résultat est connu d’avance: l’équipe désactive le contrôle, réduit le niveau ou ignore le rapport.

Sur un code legacy, l’objectif initial n’est pas de corriger toute la dette. C’est d’empêcher qu’elle augmente.

Cette distinction change la conception de l’intégration PHPStan. Le premier succès ne se mesure pas au nombre total d’erreurs supprimées. Il se mesure à la capacité du dépôt à refuser une nouvelle erreur sans exiger la réécriture immédiate de son passé.

PHPStan et ses 11 niveaux de règles

PHPStan propose 11 niveaux de contrôle, de 0 à 10. Le niveau 0 couvre les vérifications de base. Le niveau 10 applique le contrôle le plus strict, notamment sur l’utilisation implicite du type mixed.

La progression n’est pas un simple réglage de difficulté. Chaque niveau augmente la quantité d’information que le projet doit fournir au moteur: types de paramètres, types de retours, propriétés, génériques, collections, valeurs nullables et contrats d’interface.

Une base legacy ne doit pas nécessairement commencer au niveau 0. Elle doit commencer au niveau qui produit un signal exploitable. Dans certains dépôts, le niveau 3 ou 4 est déjà suffisamment exigeant pour révéler les erreurs structurantes. Dans d’autres, le niveau 0 déclenche déjà un volume important de corrections liées à l’architecture historique.

La bonne granularité dépend de la topologie du dépôt:

  • application monolithique avec code métier et intégration CMS mélangés;
  • modules indépendants avec des frontières relativement propres;
  • bibliothèques réutilisées par plusieurs applications;
  • extensions WordPress ou Drupal soumises à des points d’entrée dynamiques;
  • code PrestaShop intégrant des classes anciennes et des mécanismes de surcharge.

Le passage d’un niveau au suivant doit être motivé par la qualité du signal, pas par une ambition abstraite. Un niveau élevé qui produit un rapport illisible n’est pas une politique de qualité. C’est une charge de maintenance supplémentaire.

Ce que les niveaux changent concrètement

Les premiers niveaux vérifient principalement la cohérence basique du code: classes inconnues, méthodes inexistantes, mauvais nombre d’arguments, retours incompatibles avec les signatures disponibles. À mesure que la configuration devient stricte, PHPStan suit plus précisément les types et les chemins possibles.

Le niveau 10 pousse cette logique sur les zones utilisant mixed de façon implicite. Ce type est souvent la conséquence d’un tableau non documenté, d’un retour de fonction historique ou d’une donnée issue d’une source dynamique. Dans un code moderne, mixed peut être temporaire et volontaire. Dans un code patrimonial, il devient fréquemment un point de propagation: une valeur non typée entre dans le système, traverse plusieurs services et atteint une opération qui attend un objet ou un scalaire précis.

Le niveau 10 ne transforme pas automatiquement cette valeur en type fiable. Il rend le problème visible. La correction exige alors une décision d’architecture: ajouter une validation, typer une méthode, créer un objet de transfert ou encapsuler la donnée dans une abstraction claire.

Une configuration progressive peut suivre ce modèle:

PhaseNiveau de contrôleObjectif techniqueEffet attendu
Cartographie0 à 2Identifier les erreurs structurelles et les appels invalidesObtenir un premier inventaire exploitable
Stabilisation3 à 5Renforcer les signatures et réduire les types ambigusRéduire les régressions sur le nouveau code
Typage avancé6 à 8Contrôler les retours, les propriétés et les collectionsFaire remonter les incohérences métier
Durcissement9 à 10Limiter l’usage implicite de mixedAtteindre un contrat de typage strict

Ce tableau ne constitue pas une règle universelle. Il donne une trajectoire. Le dépôt peut rester plusieurs semaines sur un niveau si les corrections révèlent une dette structurelle importante. Il peut aussi appliquer un niveau différent par répertoire: strict sur le code métier récemment développé, plus permissif sur un module ancien ou une intégration externe.

Le périmètre compte autant que le niveau

PHPStan ne doit pas forcément analyser tout le dépôt avec la même intensité. Les répertoires générés, les fixtures volumineuses et certaines intégrations tierces peuvent être exclus ou traités avec une configuration particulière. En revanche, l’exclusion ne doit pas servir à masquer le code métier qui produit le plus de risques.

Une règle simple fonctionne bien: réduire le périmètre pour améliorer le signal, jamais pour faire disparaître les problèmes.

Le code applicatif, les services, les commandes et les objets métier doivent être prioritaires. Les adaptateurs vers un CMS ou une API peuvent nécessiter des extensions et des annotations spécifiques. Les fichiers générés ou contrôlés par une dépendance externe relèvent d’une autre stratégie: mise à jour de la dépendance, extension d’analyse ou exclusion documentée.

La baseline: isoler l’existant sans le déclarer sain

La baseline est le mécanisme central de l’intégration PHPStan dans un code legacy. Avec l’option --generate-baseline, le projet consigne les erreurs existantes dans un fichier de configuration. PHPStan peut alors exécuter un niveau de contrôle élevé sans exiger la correction immédiate de tout l’historique.

La nuance est importante: une baseline ne corrige rien et ne valide rien. Elle établit une frontière temporelle. Les erreurs présentes au moment de sa génération sont tolérées. Les erreurs nouvelles doivent être corrigées ou explicitement traitées.

Le fichier de baseline devient ainsi une photographie technique du dépôt. Il permet de dire: l’état actuel contient ces anomalies, mais toute modification future doit éviter d’en ajouter.

Cette méthode répond à un problème classique de gouvernance du code. Sans baseline, une équipe doit choisir entre deux extrêmes:

1. corriger toute la dette avant d’activer PHPStan;

2. renoncer à l’analyse parce que le volume initial est trop élevé.

Les deux options sont rarement rationnelles. La première immobilise le développement pendant une période longue. La seconde laisse le dépôt se dégrader. La baseline permet une troisième voie: activer le garde-fou immédiatement et rembourser la dette par incréments.

Une baseline exploitable doit rester contrôlable

Un fichier de baseline trop ancien devient rapidement une zone morte. Il continue de masquer des erreurs qui ont peut-être déjà été corrigées, ou il conserve des entrées attachées à du code supprimé. Il faut donc le traiter comme un artefact versionné, pas comme un fichier temporaire oublié dans le dépôt.

La réduction de la baseline peut suivre plusieurs stratégies:

  • supprimer les entrées liées aux lignes réellement corrigées;
  • régénérer périodiquement le fichier après une série de corrections;
  • associer les corrections à des périmètres fonctionnels précis;
  • suivre le volume d’erreurs restantes sans transformer ce volume en indicateur de performance isolé;
  • éviter d’ajouter mécaniquement toute nouvelle erreur à la baseline.

Le dernier point est critique. Régénérer la baseline après chaque échec de la CI revient à déplacer la ligne rouge au lieu de corriger le code. La commande est techniquement valide, mais le processus devient vide.

La dette doit être traitée par zones

La correction d’un code legacy est plus efficace lorsqu’elle suit les frontières réelles du système. Une équipe peut commencer par les services qui alimentent les commandes critiques, puis traiter les contrôleurs, les dépôts, les événements et enfin les adaptateurs CMS.

Cette séquence réduit les effets de propagation. Une propriété mieux typée dans un service peut faire apparaître plusieurs erreurs dans les appelants. Ce n’est pas un bruit supplémentaire: c’est le graphe de dépendances qui devient visible.

Un correctif local ne doit toutefois pas déclencher une réécriture générale sans nécessité. L’ajout d’un type de retour explicite sur une méthode stable est souvent préférable à une refonte complète de toutes les classes qui l’appellent. La précision vient par couches.

Brancher PHPStan dans la CI/CD

Une analyse statique exécutée uniquement sur le poste d’un développeur ne constitue pas une politique de qualité. Elle dépend de l’installation locale, de la version de PHP, des extensions activées et de la discipline individuelle. Le dépôt principal doit exécuter lui-même PHPStan.

GitHub Actions peut lancer l’analyse à chaque demande de fusion ou à chaque modification de la branche principale. Un hook Git pre-commit peut fournir un retour plus rapide, mais il ne remplace pas le contrôle côté serveur. Les hooks locaux peuvent être contournés. La CI/CD ne doit pas l’être.

Le pipeline minimal suit généralement cette séquence:

1. installer les dépendances avec une version verrouillée;

2. initialiser l’environnement PHP attendu par le projet;

3. exécuter PHPStan avec la configuration versionnée;

4. produire un code de sortie non nul en cas de nouvelle erreur;

5. publier le rapport dans le contexte de la demande de fusion;

6. conserver les sorties utiles au diagnostic sans archiver des données sensibles.

L’exécution doit rester déterministe. Une analyse qui dépend d’une version flottante d’une extension, d’un cache instable ou d’une résolution différente des dépendances produira des résultats difficiles à interpréter.

La version de PHPStan, les extensions d’analyse et les règles personnalisées doivent être mises à jour avec la même prudence qu’une dépendance d’exécution. Une montée de version peut révéler des erreurs déjà présentes mais précédemment invisibles. Dans ce cas, il faut distinguer trois situations:

  • une nouvelle erreur introduite par le code;
  • une erreur historique révélée par un moteur plus précis;
  • une incompatibilité de configuration ou d’extension.

La baseline peut absorber le deuxième cas pendant une phase de transition, mais elle ne doit pas être utilisée pour masquer le premier.

La CI ne doit pas mesurer la propreté historique du dépôt. Elle doit empêcher l’introduction de nouvelles incohérences.

Pré-commit et CI: deux vitesses, deux fonctions

Le hook pre-commit est utile pour les modifications ciblées. Il peut analyser les fichiers touchés ou un sous-ensemble du projet afin de réduire le temps d’attente. Son rôle est de raccourcir la boucle de correction.

La CI effectue le contrôle de référence. Elle analyse le périmètre défini par le projet, avec les versions et extensions officielles. Elle constitue l’autorité technique du dépôt.

Cette séparation évite une erreur fréquente: rendre le hook local tellement lourd qu’il devient inutilisable. Une analyse complète de plusieurs minutes avant chaque commit pousse les développeurs à la contourner. Un contrôle rapide local, complété par une analyse intégrale distante, offre un compromis plus robuste.

Le cache doit également être dimensionné avec méthode. PHPStan 2.0 a amélioré son empreinte mémoire, avec une réduction annoncée de 50 à 70 %, mais une optimisation du pipeline ne se résume pas à augmenter la mémoire disponible. Il faut examiner le nombre de fichiers analysés, les extensions chargées, la granularité des jobs et les invalidations de cache.

Sur un dépôt volumineux, la parallélisation peut réduire la durée totale, mais elle augmente la complexité opérationnelle. Plusieurs processus consomment davantage de ressources et peuvent dégrader la stabilité d’un agent CI limité. Le benchmark doit porter sur la durée globale, la consommation mémoire maximale et la reproductibilité des résultats.

PHPStan 2.0: le gain de performance ne dispense pas de mesurer

PHPStan 2.0 a introduit le niveau 10 et modifié en profondeur ses structures AST internes. La suppression de références circulaires a contribué à réduire la consommation mémoire de 50 à 70 %. Dans une base legacy, ce gain peut modifier le placement de l’analyse dans le pipeline.

Une vérification qui saturait un agent CI peut devenir compatible avec une exécution sur chaque demande de fusion. Mais il faut éviter le raccourci: meilleure efficacité du moteur ne signifie pas analyse gratuite.

Le coût dépend de plusieurs facteurs:

  • taille du graphe de classes;
  • nombre de fichiers réellement analysés;
  • quantité de code générique et de collections documentées;
  • extensions propres à Symfony, Doctrine, Drupal, WordPress ou PrestaShop;
  • richesse des annotations et des types inférés;
  • configuration des règles additionnelles;
  • disponibilité du cache;
  • version de PHP utilisée dans l’environnement.

Le benchmark pertinent compare une configuration à périmètre identique. Modifier en même temps le niveau de règles, le nombre de fichiers et la version du moteur ne permet pas d’identifier la cause d’un gain ou d’une régression.

Un protocole simple suffit:

  • relever la durée d’une analyse froide, sans cache utile;
  • relever la durée d’une analyse chaude, avec cache;
  • mesurer le pic d’allocation mémoire;
  • conserver le nombre d’erreurs détectées;
  • répéter la mesure sur le même agent ou sur des agents comparables;
  • comparer les résultats après chaque modification isolée.

La donnée publiée sur PrestaShop 8.0 — un passage de 9 à 3 minutes — donne un ordre de grandeur concret de ce qu’une optimisation peut apporter dans un contexte réel. Elle ne permet pas de prédire la durée d’un autre projet. Le moteur, le volume de code et la configuration sont différents.

Le typage strict améliore aussi le coût de maintenance

La performance d’exécution n’est pas le seul gain. Un code mieux typé réduit le nombre de chemins que le développeur doit reconstituer mentalement avant de modifier une méthode. La signature porte davantage d’information. Les erreurs remontent à la frontière où elles apparaissent.

Le bénéfice est particulièrement visible sur les services qui manipulent des collections et des objets issus d’un CMS. Un tableau sans structure documentée peut contenir des chaînes, des entiers, des objets et des valeurs nulles. Chaque appelant doit alors vérifier le contenu ou supposer un contrat. PHPStan rend ces hypothèses explicites.

Cette précision a un coût initial. Il faut ajouter des annotations, introduire des objets de valeur, distinguer null d’une absence de résultat et corriger des signatures trop larges. Mais le coût devient maîtrisable si le projet traite une zone à la fois, en conservant la baseline pour le reste.

Les limites dans un environnement CMS

L’intégration de PHPStan est plus directe dans une bibliothèque PHP isolée que dans un CMS. Drupal, WordPress et PrestaShop utilisent des mécanismes dynamiques: hooks, événements, extensions, conventions de nommage, chargement indirect et surcharges. Une partie du comportement n’est pas exprimée dans la signature des méthodes.

Le moteur ne peut analyser que les informations qu’il connaît. Si une fonction de CMS retourne une valeur dont le type n’est pas documenté, le résultat peut devenir mixed. Si une extension fournit des classes dynamiques, PHPStan nécessite parfois une extension dédiée ou des stubs pour comprendre les contrats.

La réponse ne consiste pas à désactiver l’analyse sur toute la couche CMS. Il faut distinguer le code de raccordement du code métier.

Une organisation efficace sépare:

  • les adaptateurs qui traduisent les données du CMS;
  • les services qui portent les règles métier;
  • les objets de transfert qui fixent les types;
  • les points d’entrée dynamiques;
  • les dépendances externes dont le contrat est fourni par des stubs ou des extensions.

Le code métier doit être le plus strict possible. Les zones dynamiques doivent être courtes et contenir les conversions nécessaires. Plus une valeur mixed reste longtemps dans le système, plus elle rend l’analyse imprécise.

Dans WordPress, cela signifie éviter de faire circuler directement des tableaux issus de fonctions globales dans toute l’application. Dans Drupal, les services et les événements peuvent servir de frontières de typage. Dans PrestaShop, les modules doivent isoler les anciennes API et exposer des méthodes plus prévisibles au reste du code.

Ce travail n’a rien d’automatique. PHPStan détecte les incohérences. Il ne choisit pas l’abstraction correcte ni le contrat métier à imposer. L’outil fournit la pression technique; l’architecture fournit la réponse.

Un déploiement progressif qui tient dans le temps

L’intégration PHPStan ne doit pas être traitée comme une campagne unique. Une campagne possède une date de début et une date de fin. La dette de typage, elle, revient à chaque nouvelle fonctionnalité si le contrôle n’est pas permanent.

Une trajectoire robuste peut être organisée en cinq étapes:

1. Cartographier le dépôt. Identifier les répertoires métier, les dépendances dynamiques, les fichiers générés et les zones qui nécessitent des extensions d’analyse.

2. Lancer un niveau initial. Choisir le niveau qui produit des erreurs compréhensibles et suffisamment nombreuses pour révéler les problèmes structurants, sans noyer le rapport.

3. Générer la baseline. Enregistrer l’état existant avec --generate-baseline, versionner le fichier et documenter son rôle.

4. Bloquer les régressions. Ajouter PHPStan à la CI/CD et refuser toute nouvelle erreur hors baseline.

5. Réduire la dette. Corriger par module, supprimer régulièrement les entrées obsolètes et augmenter le niveau lorsque le périmètre devient stable.

Chaque étape doit produire un artefact vérifiable. Une configuration versionnée. Un rapport de CI. Une baseline diffable. Une zone de code dont les types sont réellement améliorés. Sans artefact, le projet dépend d’une intention.

Le retour d’expérience associé à Maxime ne peut pas être transformé en anecdote précise sans données vérifiables sur son dépôt, sa configuration et ses métriques. Le point technique reste toutefois général: l’intégration fonctionne lorsqu’elle sépare l’historique du nouveau code et lorsqu’elle rend les erreurs visibles au bon moment. Elle échoue lorsque PHPStan est présenté comme une opération de nettoyage préalable au développement.

Verdict

PHPStan est à utiliser en production sur un projet PHP legacy, à condition de l’intégrer progressivement.

Le niveau de règles doit monter par paliers. La baseline doit isoler l’existant, pas l’absoudre. La CI/CD doit bloquer les nouvelles erreurs. PHPStan 2.0 réduit fortement le coût mémoire et peut accélérer l’analyse, mais les performances doivent être mesurées sur le dépôt réel.

La méthode binaire est simple:

  • PHPStan sans baseline, sans périmètre et sans pipeline: à éviter.
  • PHPStan avec configuration progressive, baseline versionnée et contrôle CI/CD: à déployer.

Questions fréquentes

Pourquoi utiliser une baseline avec PHPStan ?
La baseline permet de consigner les erreurs existantes dans un fichier de configuration afin de les ignorer temporairement. Cela permet d'activer PHPStan sur un projet legacy sans avoir à corriger immédiatement tout l'historique de la dette technique.
Comment choisir le niveau de règles PHPStan pour un projet existant ?
Il n'est pas nécessaire de commencer au niveau 0. Il faut choisir le niveau qui produit un signal exploitable, capable de révéler des erreurs structurantes sans générer un rapport illisible.
PHPStan remplace-t-il les tests fonctionnels ?
Non, PHPStan et les tests fonctionnels sont complémentaires. PHPStan analyse la structure du code et les types sans exécution, tandis que les tests fonctionnels vérifient le comportement réel avec des données précises.
Comment intégrer PHPStan dans un pipeline CI/CD ?
L'analyse doit être automatisée dans la CI/CD pour bloquer toute nouvelle erreur. Il est recommandé de séparer les contrôles rapides en pré-commit pour les développeurs et l'analyse complète sur le serveur pour garantir la qualité du dépôt.
Quels sont les avantages de PHPStan 2.0 ?
PHPStan 2.0 a réduit sa consommation mémoire de 50 à 70 % grâce à une refonte de ses structures AST internes et à la suppression de références circulaires.