PHPStan niveau max sur un projet Symfony : les 10 erreurs que vous allez trouver
Par Louis-Arnaud Catoire
Mis à jour le

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-en4. 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
- Éliminer le code mort dans vos projets PHP, détecter et supprimer le code inutilisé avec PHPStan
- Rector : maîtrisez l'évolution de votre code Symfony, automatiser les migrations de code en complément de PHPStan
- Migration Symfony vers l'architecture hexagonale, appliquer PHPStan pour valider les contraintes d'architecture lors d'une migration
- Documentation officielle PHPStan, guide de démarrage et niveaux d'analyse
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 gratuitArticles connexes

Code mort en PHP : détecter et supprimer le code inutilisé
Identifier et éliminer le code inutilisé dans vos projets PHP pour améliorer la qualité, la maintenabilité et les performances.
Lire la suite →
PHPStan 2.0 : niveau 10 et nouvelles fonctionnalités
PHPStan 2.0 introduit le niveau 10 pour une analyse statique PHP encore plus stricte. Découvrez les nouvelles fonctionnalités pour un code impeccable.
Lire la suite →
Comment PHPStan peut vous aider à améliorer la qualité de votre code PHP
PHPStan est un outil de vérification statique pour PHP. Il examine votre code à la recherche d'erreurs potentielles et améliore la qualité du code.
Lire la suite →