Efficience IT
·Formation

Comment PHPStan peut vous aider à améliorer la qualité de votre code PHP

Par Louis-Arnaud Catoire

L'analyse statique : un compilateur pour PHP

PHP est un langage interprété. Contrairement à Java ou Go, aucune phase de compilation ne vérifie la cohérence des types avant l'exécution. Pendant des années, les développeurs PHP ont découvert leurs erreurs de typage en production, à travers des TypeError ou des null inattendus. L'analyse statique comble ce vide : elle simule une compilation en parcourant l'AST (Abstract Syntax Tree) du code source, sans jamais l'exécuter.

PHPStan est l'outil de référence dans cet espace. Il parse votre code, reconstruit un graphe de types et applique des règles de vérification sur ce graphe. Là où les tests unitaires vérifient des comportements, PHPStan vérifie des invariants structurels : cohérence des signatures, nullabilité, exhaustivité des unions. Les deux approches sont complémentaires et non substituables.

Installation et première exécution

L'installation se fait via Composer :

composer require --dev phpstan/phpstan

Puis lancez l'analyse sur votre code :

./vendor/bin/phpstan analyse src

Centralisez la configuration dans un fichier phpstan.neon à la racine du projet :

parameters:
    level: 6
    paths:
        - src
    excludePaths:
        - src/Migrations

Avec ce fichier en place, un simple ./vendor/bin/phpstan analyse suffit. Consultez la référence de configuration pour la liste complète des options.

Les niveaux de règles : une adoption progressive

PHPStan propose 10 niveaux de règles, de 0 à 9. Chaque niveau inclut les vérifications du précédent.

Niveaux 0-2 couvrent les bases : classes et fonctions inconnues, nombre d'arguments incorrect, variables potentiellement non définies, types inconnus sur les expressions mixed.

Niveaux 3-5 ajoutent la vérification systématique des types de retour, la détection des appels sur des types nullables, et le contrôle des arguments passés aux fonctions.

Niveau 6 est le seuil recommandé pour les projets matures. Le typage strict entre en jeu : les annotations manquantes sont signalées. En dessous, PHPStan tolère l'implicite. Au-dessus, il l'interdit.

Niveaux 7-9 affinent les unions, les intersections, la nullabilité imbriquée. Le niveau 9 traite mixed comme un type opaque qui doit être narrowé avant utilisation, ce qui force une rigueur comparable à un langage fortement typé.

Pour un projet existant, commencez au niveau 0 et montez progressivement. Pour un nouveau projet, visez directement le niveau 6.

Exemples concrets de bugs détectés

Un appel de méthode sur un type potentiellement null :

function getUsername(User $user): string
{
    return $user->getProfile()->getName();
}

Si getProfile() peut retourner null, PHPStan signale : Cannot call method getName() on null. Ce type d'erreur passe systématiquement en code review humaine parce que le chemin nominal fonctionne.

Un type de retour incohérent :

function findUser(int $id): User
{
    $user = $this->repository->find($id);

    return $user;
}

Si find() retourne User|null, PHPStan détecte l'incohérence. En production, c'est un crash sur le premier appel avec un ID invalide.

Une condition dupliquée, signe de copier-coller :

if ($status === 'active' || $status === 'active') {

PHPStan repère la redondance. La deuxième condition masque probablement un autre état qui n'est jamais vérifié.

La baseline : dette technique maîtrisée

Sur un projet existant, lancer PHPStan au niveau 6 peut générer des centaines d'erreurs. La baseline résout ce problème en gelant l'état actuel :

./vendor/bin/phpstan analyse --generate-baseline

Cette commande crée un fichier phpstan-baseline.neon qui liste toutes les erreurs existantes. Référencez-le dans votre configuration :

includes:
    - phpstan-baseline.neon

parameters:
    level: 6
    paths:
        - src

À partir de ce moment, seules les nouvelles erreurs sont signalées. La baseline n'est pas un passe-droit permanent : c'est un outil de gestion de la dette technique. Traitez-la comme un backlog. Chaque sprint, corrigez un lot d'erreurs et regénérez la baseline. Suivez son évolution : une baseline qui grossit est un signal d'alerte pour un lead.

Un réflexe utile : intégrez dans votre CI un check qui compte les entrées de la baseline et échoue si ce nombre augmente. Cela empêche l'accumulation silencieuse de dette.

Intégration CI et workflow d'équipe

PHPStan prend toute sa valeur dans un pipeline d'intégration continue. Dans un .gitlab-ci.yml :

phpstan:
    stage: test
    script:
        - composer install
        - vendor/bin/phpstan analyse --no-progress

Chaque merge request est vérifiée automatiquement. Si PHPStan échoue, le développeur corrige avant de merger. Le --no-progress supprime l'affichage de la barre de progression, inutile dans les logs CI.

Pour les équipes avec un cache CI, ajoutez --error-format=json et exploitez la sortie pour des dashboards de qualité ou des notifications Slack ciblées.

L'extension phpstan-symfony : aller au-delà du code vanilla

Les frameworks PHP utilisent massivement des patterns que l'analyse statique ne peut pas résoudre seule : injection de dépendances, configuration YAML, méthodes magiques. L'extension phpstan-symfony comble ce fossé :

composer require --dev phpstan/phpstan-symfony
includes:
    - vendor/phpstan/phpstan-symfony/extension.neon

parameters:
    level: 6
    paths:
        - src
    symfony:
        containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml

Avec cette configuration, PHPStan comprend les types retournés par $container->get(), valide les noms de services, type correctement les objets Request et InputInterface. Sans cette extension, des dizaines de faux positifs polluent l'analyse et poussent les développeurs à ignorer les vrais problèmes.

Règles custom : PHPStan comme gardien d'architecture

C'est ici que PHPStan cesse d'être un simple linter et devient un outil d'architecture. Vous pouvez écrire des règles custom qui encodent les décisions structurelles de votre projet.

Prenons un exemple concret : interdire qu'un service du domaine dépende directement de l'infrastructure.

use PHPStan\Rules\Rule;
use PHPStan\Analyser\Scope;
use PhpParser\Node;
use PhpParser\Node\Stmt\Use_;

/**
 * @implements Rule<Use_>
 */
class NoDomainToInfrastructureDependencyRule implements Rule
{
    public function getNodeType(): string
    {
        return Use_::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        $file = $scope->getFile();
        if (!str_contains($file, '/Domain/')) {
            return [];
        }

        foreach ($node->uses as $use) {
            $name = $use->name->toString();
            if (str_contains($name, 'Infrastructure\\')) {
                return [
                    'Les classes du Domain ne doivent pas importer depuis Infrastructure.',
                ];
            }
        }

        return [];
    }
}

Cette règle transforme une convention verbale ("le domaine ne dépend pas de l'infra") en une contrainte vérifiée à chaque commit. C'est la différence entre un ADR (Architecture Decision Record) que personne ne lit et une garde-fou automatisée.

Enregistrez vos règles custom dans la configuration :

services:
    -
        class: App\PHPStan\NoDomainToInfrastructureDependencyRule
        tags:
            - phpstan.rules.rule

D'autres cas d'usage courants pour les règles custom : interdire l'utilisation de new dans les controllers (tout doit passer par l'injection), forcer l'usage de value objects pour certains paramètres, ou empêcher les dépendances circulaires entre bounded contexts.

Le système de types avancé : generics et template types

À partir du niveau 7, PHPStan exploite pleinement son système de types. Les generics PHP via les annotations @template permettent d'exprimer des contrats que le langage natif ne supporte pas.

/**
 * @template T of object
 * @param class-string<T> $className
 * @return T
 */
function create(string $className): object
{
    return new $className();
}

Avec cette annotation, PHPStan sait que create(User::class) retourne un User, pas un object générique. Ce niveau de précision se propage dans tout le graphe de types et élimine des catégories entières de bugs liés au downcasting.

Les template types brillent dans les repositories, les collections typées et les factories. Un Repository<User> dont la méthode find() retourne User|null au lieu de object|null : c'est ce niveau de typage qui rapproche PHP d'un langage à typage fort sans sacrifier sa flexibilité.

Les conditional return types (@return ($flag is true ? Foo : Bar)) et les assertions (@phpstan-assert) complètent l'outillage pour modéliser des API complexes que le type system natif de PHP ne peut pas capturer.

PHPStan comme philosophie de design

Au-delà de l'outil, adopter PHPStan au niveau max transforme la façon de concevoir du code. L'obligation de satisfaire l'analyseur pousse vers des designs plus explicites : moins de mixed, moins de magie, des contrats clairs entre les couches. Le code devient sa propre documentation.

Un architecte qui configure PHPStan avec des règles custom, des generics et un niveau 9 ne cherche pas à satisfaire un outil. Il encode les invariants de son système dans un vérificateur automatique qui tourne à chaque commit. C'est de la gouvernance technique exécutable.

Pour aller plus loin