Aller au contenu
Efficience IT
·6 min de lecture·Qualité de code

PHPStan niveau max sur un projet Symfony : les 10 erreurs que vous allez trouver

Par Louis-Arnaud Catoire

Mis à jour le

PHPStan niveau max sur un projet Symfony : les 10 erreurs que vous allez trouver

Vous venez de monter PHPStan au niveau 10 sur votre projet Symfony. Le terminal affiche 847 erreurs. Vous refermez le couvercle de votre laptop et vous vous demandez si c'était une bonne idée.

Bonne nouvelle : c'en était une. Ces erreurs ne sont pas du bruit. Elles pointent vers de vrais problèmes que vous traînez depuis des mois, parfois des années. Si vous débutez avec PHPStan, commence par lire comment PHPStan peut améliorer la qualité de votre code PHP avant d'aller plus loin. Après avoir accompagné une dizaine d'équipes dans cette montée en niveau, les mêmes patterns reviennent systématiquement.

Voici les 10 erreurs que vous allez trouver, et surtout comment les corriger sans y passer trois sprints.

1. Les collections Doctrine sans typage générique

C'est l'erreur numéro un. Sans exception. Chaque entité Doctrine avec une relation OneToMany ou ManyToMany déclenche cette alerte.

// PHPStan n'aime pas ça
private Collection $tags;

// Ce qu'il attend
/** @var Collection<int, Tag> $tags */
private Collection $tags;

Doctrine utilise l'interface Collection qui est générique. PHPStan au niveau 10 exige que vous précisiez le type de la clé et de la valeur. Sans ça, il considère que votre collection contient du mixed, et chaque accès à un élément perd son typage.

La correction

Ajoutez le PHPDoc générique sur chaque propriété de type Collection. Si vous utilisez les attributs PHP 8, combine-les avec le PHPDoc :

#[ORM\OneToMany(targetEntity: Tag::class, mappedBy: 'article')]
/** @var Collection<int, Tag> $tags */
private Collection $tags;

Sur un projet de taille moyenne, comptez entre 50 et 200 occurrences. Un bon regex dans votre IDE règle ça en une heure. Les évolutions de Doctrine ORM 3.0 améliorent d'ailleurs le typage natif des collections.

2. Le retour de find() non vérifié

Le EntityRepository::find() retourne ?object. PHPStan vous rappelle que vous ne gérez pas le null.

$user = $this->userRepository->find($id);
$user->getName(); // PHPStan : Cannot call method getName() on object|null

La correction

Deux approches selon le contexte. Dans un contrôleur, utilise une exception HTTP :

$user = $this->userRepository->find($id)
    ?? throw new NotFoundHttpException('User not found');

Dans un service métier, lève une exception domaine ou retourne un type nullable que l'appelant gère.

Ne faites jamais de assert($user !== null) sauf dans les tests. C'est un pansement qui masque un vrai problème de gestion d'erreur. Respecter les bonnes pratiques des API REST impose de gérer proprement ces cas de retour null.

3. Les paramètres mixed du Container

Chaque appel à $container->getParameter() retourne mixed. PHPStan déteste ça au niveau max.

$locale = $this->getParameter('app.default_locale');
// Type : mixed

La correction

Utilisez un cast explicite ou un assert de type dans un service dédié :

$locale = (string) $this->getParameter('app.default_locale');

Mieux encore : injectez vos paramètres directement via le constructeur avec l'attribut #[Autowire] :

public function __construct(
    #[Autowire('%app.default_locale%')]
    private string $defaultLocale,
) {}

C'est la méthode recommandée depuis Symfony 6.2. Elle élimine l'erreur PHPStan et rend votre code plus testable. Pour en savoir plus sur la configuration du conteneur de services Symfony, la documentation officielle détaille toutes les options d'injection.

Besoin d'accompagnement sur votre projet ?

Parlons-en

4. Les formulaires et getData() qui retourne mixed

$form->getData() retourne mixed. Normal : Symfony ne peut pas savoir au moment du typage ce que votre formulaire contient.

$dto = $form->getData();
$dto->getEmail(); // Cannot call method getEmail() on mixed

La correction

Utilisez @var localement pour indiquer le type à PHPStan :

/** @var ContactDTO $dto */
$dto = $form->getData();

Certains préfèrent créer une méthode helper typée dans un AbstractController custom, mais c'est de l'over-engineering pour la plupart des projets.

5. Les types de retour des QueryBuilder

Le QueryBuilder de Doctrine est un cauchemar pour l'analyse statique. getResult() retourne mixed, getOneOrNullResult() aussi.

$results = $qb->getQuery()->getResult();
// Type : mixed

La correction

Utilisez les PHPDoc @return sur vos méthodes de repository :

/**
 * @return array<Article>
 */
public function findPublished(): array
{
    return $this->createQueryBuilder('a')
        ->where('a.publishedAt IS NOT NULL')
        ->getQuery()
        ->getResult();
}

PHPStan fait confiance au @return déclaré. Ça résout l'erreur et ça documente le contrat de votre méthode. L'extension phpstan/phpstan-doctrine améliore aussi la compréhension des types DQL.

6. Les event subscribers avec des signatures trop larges

Les listeners et subscribers Symfony reçoivent souvent un Event générique alors qu'ils attendent un type précis.

public function onKernelRequest(RequestEvent $event): void

PHPStan vérifie que la signature du listener correspond à ce que le dispatcher envoie. Si votre subscriber déclare écouter kernel.request mais que la signature ne matche pas le type attendu, vous avez une erreur.

La correction

Vérifiez que vos méthodes de listener acceptent exactement le type d'événement dispatché. L'extension phpstan/phpstan-symfony connaît les types d'événements du kernel et valide les signatures automatiquement.

Installez-la si ce n'est pas déjà fait :

composer require --dev phpstan/phpstan-symfony

7. Les constantes de classe utilisées comme clés de tableau

PHPStan niveau 10 vérifie les types des clés de tableau. Si vous utilisez une constante string comme clé mais que votre tableau est typé array<int, mixed>, ça casse.

private const STATUS_ACTIVE = 'active';

// Si $config est typé array<string, bool>
$config[self::STATUS_ACTIVE] = true; // OK

// Si $config vient d'une source non typée
$config[self::STATUS_ACTIVE] = true; // Offset 'active' on array{} does not exist

La correction

Typez vos tableaux correctement dès la déclaration. Utilise les PHPDoc pour les tableaux complexes :

/** @var array<string, bool> */
private array $config = [];

Mieux : remplacez les tableaux associatifs par des objets typés. Un simple DTO avec des propriétés nommées est toujours plus sûr qu'un tableau.

8. Les templates Twig référencés comme strings

Si vous utilisez phpstan/phpstan-symfony, PHPStan vérifie que les templates Twig existent. Les fautes de frappe dans les noms de templates deviennent des erreurs.

return $this->render('article/shwo.html.twig', [
    'article' => $article,
]);

La correction

Corrigez le nom du template. C'est trivial mais c'est exactement le genre de bug qui passe en production parce qu'il se cache derrière un chemin rarement emprunté. PHPStan le trouve sans exécuter le code. C'est sa force.

Activez la vérification des templates dans votre config :

parameters:
    symfony:
        containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml

9. Les unions de types non réduites

Au niveau 10, PHPStan exige que vous réduisiez les unions de types avant d'appeler une méthode spécifique.

public function process(User|Company $entity): void
{
    $entity->getCompanyName(); // Method getCompanyName() does not exist on User
}

La correction

Utilisez un instanceof pour réduire le type :

public function process(User|Company $entity): void
{
    if ($entity instanceof Company) {
        $name = $entity->getCompanyName();
    } else {
        $name = $entity->getFullName();
    }
}

Ou encore mieux, définis une interface commune si les deux classes partagent un comportement :

interface Nameable
{
    public function getDisplayName(): string;
}

Les unions non réduites révèlent souvent un problème de conception. Si votre méthode reçoit User|Company, demandez-vous pourquoi ces deux types arrivent au même endroit.

10. Les closures et callbacks sans typage

Les callbacks passés à array_map, array_filter ou usort manquent souvent de typage sur leurs paramètres.

$names = array_map(function ($user) {
    return $user->getName();
}, $users);

PHPStan infère $user comme mixed si $users n'est pas typé. Même si $users est bien typé, une closure sans type explicite peut poser problème dans certains contextes.

La correction

Typez les paramètres de vos closures :

$names = array_map(function (User $user): string {
    return $user->getName();
}, $users);

Ou utilise les arrow functions pour plus de concision :

$names = array_map(fn (User $user): string => $user->getName(), $users);

La stratégie pour monter progressivement

Ne passez pas du niveau 0 au niveau 10 en un commit. Si vous utilisez PHPStan 2.0, les nouvelles vérifications du niveau 10 sont encore plus nombreuses. Associer PHPStan à des conventions de codage partagées par l'équipe accélère la montée en niveau. Voici une approche qui fonctionne :

Utilise la baseline

PHPStan permet de générer un fichier baseline qui ignore toutes les erreurs existantes :

vendor/bin/phpstan analyse --generate-baseline

À partir de là, seules les nouvelles erreurs apparaissent. Vous corrigez l'existant progressivement, sans bloquer les développements en cours.

Monte niveau par niveau

Chaque niveau ajoute des vérifications. Consultez la liste des niveaux PHPStan pour savoir ce que chaque palier apporte. Stabilisez un niveau avant de passer au suivant. Les niveaux 6 à 10 sont ceux qui révèlent le plus de problèmes dans un projet Symfony, surtout autour de Doctrine et des formulaires.

Installe les extensions Symfony et Doctrine

Ces deux extensions sont indispensables. Vous pouvez aussi ajouter phpstan/phpstan-strict-rules pour aller encore plus loin, et phpstan/extension-installer pour enregistrer automatiquement les extensions :

composer require --dev phpstan/phpstan-symfony phpstan/phpstan-doctrine

Elles apportent la compréhension des types spécifiques à l'écosystème : le container, les repositories, les formulaires, les événements. Sans elles, vous allez vous battre contre des faux positifs.

Ce que ça change concrètement

Après avoir corrigé ces 10 catégories d'erreurs, votre projet n'est plus le même. Les bugs de type disparaissent avant même d'arriver en review. Les refactorisations deviennent plus sûres parce que PHPStan attrape les effets de bord. Les nouveaux développeurs comprennent les contrats des méthodes sans lire l'implémentation.

PHPStan au niveau max n'est pas un caprice de perfectionniste. C'est un filet de sécurité qui rattrape les erreurs que les tests unitaires ne couvrent pas, que la review ne voit pas, et que le QA ne reproduit pas. Sur un projet Symfony en production, c'est un investissement qui se rentabilise dès la première régression évitée. Pour aller encore plus loin, un audit gratuit de votre codebase Symfony permet d'identifier les chantiers prioritaires. Si vous souhaitez un regard extérieur sur l'état de votre codebase, notre audit de code PHP identifie ces problèmes de manière systématique.

Pour aller plus loin

Faites auditer votre code PHP

Un regard extérieur sur votre base de code peut révéler des problèmes structurels que l'habitude fait oublier. Profitez d'un audit technique gratuit de 30 minutes.

Demander un audit gratuit

Articles connexes