PHPStan 2.0 : niveau 10 et nouvelles fonctionnalités
Par Louis-Arnaud Catoire
PHPStan 2.0 marque un tournant dans l'analyse statique PHP. Au-delà des gains de performance et du nouveau niveau 10, cette version transforme la manière dont on conçoit, documente et sécurise une base de code. Que vous cherchiez à migrer depuis la 1.x ou à comprendre comment le système de types peut porter vos décisions d'architecture, cet article couvre l'essentiel.
Ce que le niveau 10 change concrètement
Le niveau 9 détectait déjà les incohérences de typage, les propriétés non initialisées et les conditions redondantes. Le niveau 10 s'attaque à un angle mort majeur : le type mixed implicite. Un paramètre sans type natif ni PHPDoc est implicitement mixed, et PHP autorise n'importe quelle opération dessus sans broncher. Le niveau 10 refuse ce flou.
Prenons un cas courant. Au niveau 9, ce code ne lève aucune erreur :
function processData($data)
{
return $data->getName();
}
Au niveau 10, PHPStan signale que $data est implicitement mixed. L'appel à getName() devient une erreur, car rien ne garantit que $data est un objet disposant de cette méthode. La correction est directe :
function processData(User $data): string
{
return $data->getName();
}
Ce même principe s'applique aux opérations arithmétiques. Ce code passe au niveau 9 :
function calculateTotal(array $items): int
{
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
Au niveau 10, $item est mixed, donc l'accès à price et quantity est interdit. La solution passe par un array shape :
/**
* @param array<array{price: int, quantity: int}> $items
*/
function calculateTotal(array $items): int
{
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
Ce n'est pas une simple annotation cosmétique. C'est un contrat vérifié statiquement : tout appelant qui passe un tableau dont la structure ne correspond pas sera détecté avant l'exécution.
Migrer de PHPStan 1.x à 2.0
Dépendances et extensions
La mise à jour se lance classiquement :
composer require --dev phpstan/phpstan:^2.0
Les extensions tierces (phpstan-symfony, phpstan-doctrine, phpstan-phpunit) doivent être mises à jour vers leurs versions compatibles 2.0. Vérifiez chaque extension avant de lancer la migration : une extension incompatible provoque des erreurs silencieuses ou des faux négatifs.
Redistribution des règles par niveau
Certaines règles ont changé de niveau. Des erreurs qui n'apparaissaient qu'au niveau 8 remontent désormais au niveau 7. Si vous maintenez un niveau fixe dans votre CI, relancez une analyse complète après la migration.
Stratégie baseline pour les projets existants
Sur un projet legacy de plusieurs centaines de milliers de lignes, activer le niveau 10 d'un coup produit des milliers d'erreurs. La baseline est l'outil de transition adapté :
phpstan analyse --generate-baseline
Cette commande génère un fichier phpstan-baseline.neon qui recense toutes les erreurs existantes. PHPStan ne les signale plus dans les analyses suivantes, mais toute nouvelle erreur est détectée immédiatement. La stratégie recommandée consiste à réduire cette baseline progressivement, module par module, en commençant par les couches les plus critiques (domaine métier, services partagés).
Les options de configuration dépréciées depuis la 1.x sont supprimées. Si votre phpstan.neon utilise des clés obsolètes, PHPStan affichera un message clair indiquant la syntaxe de remplacement.
Inférence de types et conditions always-true/always-false
PHPStan 2.0 améliore significativement son moteur d'inférence. Il détecte avec plus de précision les conditions qui sont toujours vraies ou toujours fausses, signe fréquent d'un bug logique ou d'un code mort.
Considérons ce fragment :
function handle(string $status): void
{
if ($status === 'active') {
// ...
}
if ($status === 'active') {
// ...
}
}
PHPStan ne signalera pas ces deux blocs comme redondants (le flow est linéaire). Mais dans un cas comme celui-ci :
function process(int $value): void
{
if ($value > 0) {
if ($value >= 0) {
// always true
}
}
}
La condition interne est toujours vraie. PHPStan la détecte et la signale. Ce type d'alerte révèle des branches mortes, des vérifications redondantes ou des incompréhensions du flow de données. Sur un projet mature, corriger ces alertes simplifie le code et réduit la surface cognitive pour les développeurs qui le maintiennent.
Cohérence des annotations @var
PHPStan 2.0 ne fait plus confiance aveuglément aux annotations @var. Quand le type déclaré diverge du type inféré, une erreur est remontée :
/** @var string $count */
$count = $repository->count([]);
count() retourne un int, mais l'annotation déclare un string. En 1.x, l'annotation l'emportait, masquant potentiellement un bug. En 2.0, l'incohérence est signalée. Ce changement encourage à supprimer les @var superflus et à ne les utiliser que lorsque PHPStan ne peut pas inférer le type lui-même (résultats de requêtes dynamiques, désérialisation).
Le type list et la précision des structures de données
PHPStan 2.0 distingue array<int, T> de list<T>. Un list garantit des clés entières consécutives commençant à 0, ce que array_values() produit toujours mais qu'un tableau arbitraire ne garantit pas :
/**
* @param list<string> $names
*/
function displayNames(array $names): void
{
foreach ($names as $index => $name) {
echo "$index: $name\n";
}
}
$users = [3 => 'Alice', 7 => 'Bob'];
displayNames($users);
PHPStan détecte que $users n'est pas une list<string>. Cette distinction semble mineure, mais elle élimine une catégorie de bugs subtils liés aux clés de tableaux après des opérations comme array_filter() sans array_values().
Fonctions pures et effets de bord
L'annotation @phpstan-pure permet de déclarer qu'une fonction ne produit aucun effet de bord. PHPStan 2.0 vérifie cette promesse statiquement :
/** @phpstan-pure */
function formatPrice(int $cents): string
{
return number_format($cents / 100, 2, ',', ' ') . ' €';
}
Cette fonction est pure : même entrée, même sortie, aucune modification d'état. En revanche, ajouter un appel à un logger la rend impure, et PHPStan le signale immédiatement. Marquer explicitement les fonctions pures dans les couches domaine et calcul renforce la prévisibilité et facilite le test unitaire.
Performance : mémoire et temps d'analyse
PHPStan 2.0 réduit la consommation mémoire de 50 à 70 % grâce à une refonte des structures AST internes et à l'élimination des références circulaires qui freinaient le ramasse-miettes. L'analyse de PrestaShop 8.0 passe de 9 à 3 minutes. Le cache a été refondu : fichiers plus compacts, chargement plus rapide, répertoire de cache réduit de plusieurs centaines de mégaoctets à quelques dizaines. En CI, la restauration et la génération du cache sont sensiblement plus rapides.
Le système de types comme outil d'architecture
Au-delà de la détection de bugs, le niveau 10 transforme PHPStan en outil d'architecture. Quand chaque paramètre, chaque retour de méthode et chaque structure de données est typé avec précision, le code devient auto-documenté. Les array shapes décrivent des contrats de données que le compilateur vérifie. Les types union (string|int) et intersection (Countable&Iterator) expriment des contraintes que seuls des tests d'intégration pouvaient couvrir auparavant.
Cette complétude du système de types a des conséquences architecturales directes. Un service dont toutes les méthodes publiques sont typées au niveau 10 n'a plus besoin de validation défensive en entrée : PHPStan garantit que les appelants respectent le contrat. Les invariants métier exprimés par des types (un PositiveInt via @phpstan-type, un non-empty-string pour un identifiant) sont vérifiés à chaque appel, sans code de garde.
PHPStan devient alors une forme de documentation vivante. Contrairement à un commentaire ou un wiki, un type PHPStan est vérifié à chaque exécution de la CI. Il ne peut pas dériver du code réel. Pour un architecte, c'est un levier puissant : les décisions de design (séparation des couches, contrats entre modules, structures de données partagées) sont encodées dans les types et appliquées automatiquement.
Imposer le niveau 10 sur l'ensemble d'un projet revient à dire : chaque interaction entre composants est explicite et vérifiée. C'est un investissement initial significatif, mais le retour se mesure en bugs évités, en onboarding accéléré et en refactorings sécurisés.
Pour aller plus loin
- Les 10 erreurs PHPStan niveau max sur Symfony — cas concrets et solutions
- Comment PHPStan peut améliorer la qualité de votre code PHP — guide d'introduction
- Rector : maîtrisez l'évolution de votre code Symfony — automatiser le refactoring
- Blog officiel PHPStan — annonce de la version 2.0 — détails complets de la release
- PHPStan sur GitHub — code source et issues
- PHPStan Playground — tester PHPStan en ligne
- PHP 8.4 — nouvelles fonctionnalités — la version PHP supportée par PHPStan 2.0