Aller au contenu principal
Retour au blog

PHPStan 2.0 niveau 10 en CI/CD : montée progressive, zéro warning

Flavien Métivier22 avril 20258 min

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 if rendues impossibles par les types déclarés, return inaccessibles, 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 T sont 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
        - tests

Génère la baseline sur ce niveau :

vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon --no-progress

Puis 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


 | tr '\n' ' ')

if [ -z "$CHANGED" ]; then
    echo "Aucun fichier PHP modifié — analyse ignorée."
    exit 0
fi

echo "PHPStan niveau $TARGET_LEVEL sur les fichiers modifiés :"
echo "$CHANGED"

# shellcheck disable=SC2086
vendor/bin/phpstan analyse \
    --level="$TARGET_LEVEL" \
    --no-progress \
    --error-format=table \
    $CHANGED

Votre dette technique s'accumule ?

Demander un audit

Intègre ce script dans le workflow de PR comme étape bloquante dès le deuxième sprint. Pour chaque montée globale de niveau, une PR de refacto dédiée régénère la baseline globale — le message de commit type chore(phpstan): level 7 → 8, baseline 1 243 → 897 erreurs transforme l'historique Git en tableau de bord de qualité lisible par tous. Si tu veux tracer la courbe dans un outil externe, un simple wc -l phpstan-baseline.neon donne le nombre de lignes d'erreurs restantes — imparfait mais instantané.

Les extensions Symfony indispensables à activer en priorité

Sans extension Symfony, PHPStan ne comprend pas le container de services, les formulaires ni les entités Doctrine. Il génère des faux positifs qui saturent la baseline de bruit inutile et démotivent l'équipe. Active ces extensions avant de monter au niveau 10 — elles transforment des faux positifs en vrais bugs, ce qui est précisément l'objectif :

composer require --dev phpstan/phpstan-symfony:"^2.0"
composer require --dev phpstan/phpstan-doctrine:"^2.0"  # si tu utilises Doctrine
composer require --dev phpstan/phpstan-deprecation-rules:"^1.2"

Adapte phpstan.neon pour inclure ces extensions et pointer vers le container XML compilé par Symfony :

# phpstan.neon — configuration complète niveau 10
includes:
    - vendor/phpstan/phpstan-symfony/extension.neon
    - vendor/phpstan/phpstan-doctrine/extension.neon
    - vendor/phpstan/phpstan-deprecation-rules/rules.neon
    - phpstan-baseline.neon

parameters:
    level: 10
    paths:
        - src
        - tests
    symfony:
        containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
    doctrine:
        objectManagerLoader: tests/object-manager.php

L'extension Symfony utilise le container XML compilé pour résoudre les types réels des services injectés. Si le fichier var/cache/dev/...Container.xml n'existe pas lors de l'analyse CI, génère-le dans le job avant l'analyse via bin/console cache:warmup --env=dev (voir le workflow complet ci-dessous). Sans ça, l'extension lève des faux positifs sur tous les services injectés.

  • phpstan-symfony : résout les types des services tagués, valide les FormType, détecte les injections #[Autowire] mal typées.
  • phpstan-doctrine : analyse les associations @OneToMany/@ManyToOne, vérifie la cohérence entre le type PHP de la propriété et le type de la colonne en base.
  • phpstan-deprecation-rules : transforme les usages de code déprécié en erreurs PHPStan — indispensable pour anticiper les ruptures Symfony sans surprises en production.

Étape 3 — Le gate CI strict zéro warning dans GitHub Actions

Une fois le niveau 10 atteint et la baseline vidée (ou proche de zéro), place le gate CI qui refusera toute régression. Voici un workflow GitHub Actions complet, adapté à PHP 8.4 et Symfony 7.2, avec cache Composer et warmup du container :

name: PHPStan

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  analyse:
    name: Static Analysis — Level 10
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP 8.4
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          coverage: none
          tools: composer:v2

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ hashFiles('composer.lock') }}
          restore-keys: composer-

      - name: Install dependencies
        run: composer install --no-progress --prefer-dist --optimize-autoloader

      - name: Warm up Symfony container
        run: bin/console cache:warmup --env=dev
        env:
          APP_ENV: dev
          APP_SECRET: a_dummy_secret_for_phpstan

      - name: Run PHPStan
        run: |
          vendor/bin/phpstan analyse \
            --no-progress \
            --error-format=github \
            --memory-limit=512M

Le flag --error-format=github produit des annotations directement dans l'interface GitHub : chaque erreur apparaît en ligne dans l'onglet Files changed de la PR, sans avoir à fouiller les logs bruts. Le timeout-minutes: 15 protège ton budget Actions contre un run bloqué sur un très large codebase. Le cache Composer réduit les runs suivants à une vingtaine de secondes sur la majorité des projets. Note que le APP_SECRET factice est nécessaire uniquement pour que le kernel Symfony boot lors du warmup — il n'a aucune valeur de sécurité dans ce contexte.

Pièges classiques à éviter sur la route du niveau 10

  • Ne pas régénérer la baseline après la montée de niveau : l'ancienne baseline référence des numéros de ligne qui ne correspondent plus, PHPStan lève des warnings parasites indébuggables qui découragent l'équipe.
  • Oublier d'exclure vendor/ : si tes paths sont larges, ajoute excludePaths: - vendor — PHPStan 2.0 peut analyser les dépendances tierces si tu ne le lui interdis pas explicitement.
  • Versionner la baseline dans .gitignore : erreur fréquente qui la rend invisible aux autres membres de l'équipe et annule intégralement le plan de montée progressive.
  • Activer le niveau 10 sans les extensions Symfony : les faux positifs liés au container vont saturer ta baseline de bruit et fausser l'indicateur de progression — impossible de distinguer un vrai bug du bruit.
  • Monter trop vite : un palier par sprint (deux semaines) est le rythme maximum tenable sans sacrifier la vélocité. Chaque palier mérite une PR de refacto dédiée, validée par l'équipe, avec baseline régénérée et nombre d'erreurs documenté dans le message de commit.

De zéro warning à zéro régression : le vrai bénéfice

PHPStan 2.0 niveau 10 représente probablement le meilleur ratio effort/bugs-évités disponible dans l'écosystème PHP aujourd'hui. La stratégie baseline + montée par palier + gate CI n'est pas une contrainte imposée à l'équipe — c'est une dette technique payée progressivement, sans jamais arrêter les livraisons. Sur les projets où j'ai appliqué cette méthode, les bugs de type Call to a member function on null ont disparu de la production en moins de deux sprints, avant même d'avoir atteint le niveau 10. La montée finale au niveau 10 ne fait alors que verrouiller ce que l'équipe a déjà intégré comme standard de qualité au quotidien.

Tu veux mettre en place cette stratégie sur un codebase Symfony existant mais tu manques de bande passante, ou tu hérites d'un projet sans aucune analyse statique en place ? Le Bear Scan est un audit technique qui inclut la mise en place de PHPStan 2.0, la génération de la baseline initiale et un plan de montée de niveau adapté à la taille et aux priorités de ton équipe — sans bloquer tes livraisons en cours. Contacte-moi pour en discuter.

Cet article vous a plu ? Partagez-le !

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.