PHPStan 2.0, sorti le 11 novembre 2024, a remis les compteurs à zéro pour l'analyse statique PHP : 180+ entrées au changelog, un moteur d'inférence de types remanié en profondeur, et surtout le niveau 10 — le plus strict jamais disponible dans l'outil. Si ton projet tourne au niveau 7 ou 8, la tentation de passer directement à 10 est forte. Résiste. Sur un codebase de production non trivial, le delta peut représenter des milliers d'erreurs nouvelles en une seule passe. Ce tutoriel te donne la stratégie en trois temps — baseline sur l'existant, montée par palier ciblant les zones actives, gate CI strict zéro warning — pour intégrer PHPStan 2.0 niveau 10 dans ton pipeline GitHub Actions sans bloquer ton équipe ni sacrifier la rigueur de l'analyse.
PHPStan 2.0 niveau 10 : ce qui change vraiment dans le moteur
La version 2.0 n'est pas une évolution incrémentale. Ondřej Mirtes et son équipe ont remanié l'inférence pour mieux propager les génériques PHP natifs, affiner la validation des templates PHPDoc @template, et traquer les chemins de code mort qu'aucune version antérieure ne signalait. Le niveau 10, nouveau dans cette version, active trois grandes catégories de règles supplémentaires par rapport au niveau 9 :
- Dead code paths : branches
ifrendues impossibles par les types déclarés,returninaccessibles, paramètres jamais utilisés après leur déclaration. - Nullable strict : tout paramètre implicitement nullable (
?string,?int…) doit être explicitement vérifié avant usage, même dans des contextes que PHP 8.x tolère nativement au runtime. - Generics enforcement : les annotations
@template Tsont désormais vérifiées à la résolution des appels, pas uniquement à la déclaration de la méthode.
En pratique, sur un projet Symfony 7.2 de 50 000 lignes avec une couverture PHPDoc correcte, PHPStan 2.0 niveau 10 génère entre 300 et 2 000 erreurs supplémentaires par rapport à une configuration niveau 8 héritée. C'est précisément pourquoi une stratégie d'adoption structurée est indispensable — un basculement brutal arrête les livraisons pendant des semaines.
Étape 1 — Poser la baseline sur l'existant
La baseline PHPStan gèle toutes les erreurs actuellement connues sur le codebase. Le CI passera vert sur ces erreurs héritées, mais toute nouvelle erreur introduite dans une PR fera échouer le build immédiatement. C'est le point de départ indispensable pour ne pas bloquer l'équipe pendant la montée de niveau. Commence par installer PHPStan 2.0 :
composer require --dev phpstan/phpstan:"^2.0"Crée une configuration minimale phpstan.neon à la racine du projet, au niveau actuel de l'équipe — pas plus haut :
# phpstan.neon
parameters:
level: 5
paths:
- src
- testsGénère la baseline sur ce niveau :
vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon --no-progressPuis inclus la baseline dans la configuration et commite les deux fichiers ensemble :
# phpstan.neon — après génération de la baseline
includes:
- phpstan-baseline.neon
parameters:
level: 5
paths:
- src
- testsÀ partir de ce moment, le CI est vert sur le niveau 5 avec baseline. Chaque PR qui introduit une nouvelle erreur fait échouer le build. Et chaque PR qui corrige une erreur existante rétrécit la baseline : le compteur d'erreurs restantes diminue de façon visible dans le diff Git. Documente cette mécanique dans le README de l'équipe — c'est le seul indicateur de progression qui compte.
Étape 2 — Monter par palier en ciblant les zones actives
Grimper de niveau 5 à 10 d'un coup génère des milliers d'erreurs et bloque le travail pendant des semaines. La stratégie recommandée : monter d'un niveau tous les deux sprints, en concentrant les corrections sur les fichiers récemment modifiés — les zones actives du codebase. Le reste reste couvert par la baseline, ce qui évite de bloquer les tickets en cours sur des modules non touchés depuis six mois. Ce script bash extrait les fichiers PHP modifiés depuis le branchement de la PR et lance PHPStan sur eux au niveau cible, sans toucher aux fichiers gelés par la baseline :
#!/usr/bin/env bash
# analyse-changed.sh — PHPStan ciblé sur les fichiers modifiés depuis main
set -euo pipefail
TARGET_LEVEL="${1:-8}"
BASE_BRANCH="${2:-origin/main}"
CHANGED=$(git diff --name-only "$BASE_BRANCH"...HEAD | grep '\.php