PHPStan Max Level on Symfony: The Top 10 Errors

You just raised PHPStan to level 10 on your Symfony project. The terminal shows 847 errors. You close your laptop lid and wonder if it was a good idea.
Good news: it was. These errors are not noise. They point to real problems you have been dragging along for months, sometimes years. If you are new to PHPStan, start by reading how PHPStan can improve the quality of your PHP code (in French) before going further. After helping about ten teams through this level increase, the same patterns come back every time.
Here are the 10 errors you will find, and above all how to fix them without spending three sprints on it.
1. Doctrine collections without generic typing
This is error number one. Without exception. Every Doctrine entity with a OneToMany or ManyToMany relation triggers this alert.
// PHPStan n'aime pas ça
private Collection $tags;
// Ce qu'il attend
/** @var Collection<int, Tag> $tags */
private Collection $tags;
Doctrine uses the Collection interface, which is generic. PHPStan at level 10 requires you to specify the key type and the value type. Without that, it considers that your collection contains mixed, and every access to an element loses its typing.
The fix
Add the generic PHPDoc on every property of type Collection. If you use PHP 8 attributes, combine them with the PHPDoc:
#[ORM\OneToMany(targetEntity: Tag::class, mappedBy: 'article')]
/** @var Collection<int, Tag> $tags */
private Collection $tags;
On a medium-sized project, expect between 50 and 200 occurrences. A good regex in your IDE settles it in an hour. The improvements in Doctrine ORM 3.0 (in French) also improve native collection typing.
2. Unchecked return value of find()
EntityRepository::find() returns ?object. PHPStan reminds you that you do not handle null.
$user = $this->userRepository->find($id);
$user->getName(); // PHPStan : Cannot call method getName() on object|null
The fix
Two approaches depending on the context. In a controller, use an HTTP exception:
$user = $this->userRepository->find($id)
?? throw new NotFoundHttpException('User not found');
In a business service, throw a domain exception or return a nullable type that the caller handles.
Never use assert($user !== null) except in tests. It is a band-aid that hides a real error handling problem. Following REST API best practices (in French) requires handling these null return cases properly.
3. mixed container parameters
Every call to $container->getParameter() returns mixed. PHPStan hates that at max level.
$locale = $this->getParameter('app.default_locale');
// Type : mixed
The fix
Use an explicit cast or a type assert in a dedicated service:
$locale = (string) $this->getParameter('app.default_locale');
Even better: inject your parameters directly through the constructor with the #[Autowire] attribute:
public function __construct(
#[Autowire('%app.default_locale%')]
private string $defaultLocale,
) {}
This is the recommended method since Symfony 6.2. It removes the PHPStan error and makes your code more testable. To learn more about configuring the Symfony service container, the official documentation details all the injection options.
Need support on your project?
Let's talk4. Forms and getData() returning mixed
$form->getData() returns mixed. That is normal: Symfony cannot know at typing time what your form contains.
$dto = $form->getData();
$dto->getEmail(); // Cannot call method getEmail() on mixed
The fix
Use @var locally to tell PHPStan the type:
/** @var ContactDTO $dto */
$dto = $form->getData();
Some prefer to create a typed helper method in a custom AbstractController, but that is over-engineering for most projects.
5. QueryBuilder return types
Doctrine's QueryBuilder is a nightmare for static analysis. getResult() returns mixed, and so does getOneOrNullResult().
$results = $qb->getQuery()->getResult();
// Type : mixed
The fix
Use @return PHPDoc on your repository methods:
/**
* @return array<Article>
*/
public function findPublished(): array
{
return $this->createQueryBuilder('a')
->where('a.publishedAt IS NOT NULL')
->getQuery()
->getResult();
}
PHPStan trusts the declared @return. It fixes the error and documents your method's contract. The phpstan/phpstan-doctrine extension also improves the understanding of DQL types.
6. Event subscribers with signatures that are too broad
Symfony listeners and subscribers often receive a generic Event while they expect a specific type.
public function onKernelRequest(RequestEvent $event): void
PHPStan checks that the listener signature matches what the dispatcher sends. If your subscriber declares that it listens to kernel.request but the signature does not match the expected type, you get an error.
The fix
Check that your listener methods accept exactly the type of the dispatched event. The phpstan/phpstan-symfony extension knows the kernel event types and validates signatures automatically.
Install it if you have not already:
composer require --dev phpstan/phpstan-symfony
7. Class constants used as array keys
PHPStan level 10 checks the types of array keys. If you use a string constant as a key but your array is typed array<int, mixed>, it breaks.
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
The fix
Type your arrays correctly from the declaration. Use PHPDoc for complex arrays:
/** @var array<string, bool> */
private array $config = [];
Better still: replace associative arrays with typed objects. A simple DTO with named properties is always safer than an array.
8. Twig templates referenced as strings
If you use phpstan/phpstan-symfony, PHPStan checks that Twig templates exist. Typos in template names become errors.
return $this->render('article/shwo.html.twig', [
'article' => $article,
]);
The fix
Fix the template name. It is trivial, but it is exactly the kind of bug that reaches production because it hides behind a rarely used path. PHPStan finds it without running the code. That is its strength.
Enable template checking in your config:
parameters:
symfony:
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
9. Unreduced type unions
At level 10, PHPStan requires you to narrow type unions before calling a specific method.
public function process(User|Company $entity): void
{
$entity->getCompanyName(); // Method getCompanyName() does not exist on User
}
The fix
Use an instanceof to narrow the type:
public function process(User|Company $entity): void
{
if ($entity instanceof Company) {
$name = $entity->getCompanyName();
} else {
$name = $entity->getFullName();
}
}
Or better still, define a common interface if the two classes share a behavior:
interface Nameable
{
public function getDisplayName(): string;
}
Unreduced unions often reveal a design problem. If your method receives User|Company, ask yourself why these two types arrive at the same place.
10. Closures and callbacks without typing
Callbacks passed to array_map, array_filter, or usort often lack typing on their parameters.
$names = array_map(function ($user) {
return $user->getName();
}, $users);
PHPStan infers $user as mixed if $users is not typed. Even if $users is well typed, a closure without an explicit type can cause problems in some contexts.
The fix
Type your closure parameters:
$names = array_map(function (User $user): string {
return $user->getName();
}, $users);
Or use arrow functions for more concision:
$names = array_map(fn (User $user): string => $user->getName(), $users);
The strategy for climbing gradually
Do not go from level 0 to level 10 in a single commit. If you use PHPStan 2.0 (in French), the new level 10 checks are even more numerous. Pairing PHPStan with coding conventions (in French) shared by the team speeds up the level increase. Here is an approach that works:
Use the baseline
PHPStan can generate a baseline file that ignores all existing errors:
vendor/bin/phpstan analyse --generate-baseline
From then on, only new errors appear. You fix the existing ones gradually, without blocking ongoing development.
Climb level by level
Each level adds checks. See the list of PHPStan levels to know what each step brings. Stabilize one level before moving to the next. Levels 6 to 10 are the ones that reveal the most problems in a Symfony project, especially around Doctrine and forms.
Install the Symfony and Doctrine extensions
These two extensions are essential. You can also add phpstan/phpstan-strict-rules to go even further, and phpstan/extension-installer to register extensions automatically:
composer require --dev phpstan/phpstan-symfony phpstan/phpstan-doctrine
They bring an understanding of the ecosystem-specific types: the container, repositories, forms, events. Without them, you will fight false positives.
What it changes in practice
After fixing these 10 categories of errors, your project is no longer the same. Type bugs disappear before they even reach review. Refactorings become safer because PHPStan catches side effects. New developers understand method contracts without reading the implementation.
PHPStan at max level is not a perfectionist's whim. It is a safety net that catches the errors unit tests do not cover, that review does not see, and that QA does not reproduce. On a Symfony project in production, it is an investment that pays off with the first regression avoided. To go even further, a free audit (in French) of your Symfony codebase helps identify the priority work. If you want an outside view of the state of your codebase, our PHP architecture review (in French) identifies these problems systematically. In addition, our automated PHP tests (in French) cover the runtime part that PHPStan alone cannot validate.
Going further
- Eliminating dead code in your PHP projects (in French), detect and remove unused code with PHPStan
- Rector: master the evolution of your Symfony code (in French), automate code migrations alongside PHPStan
- Symfony migration to hexagonal architecture (in French), apply PHPStan to validate architecture constraints during a migration
- Official PHPStan documentation, getting started guide and analysis levels
Have a project in mind?
Our team replies within 48 hours to look at your needs and suggest a suitable approach.
Contact usRelated articles

PHP Static Analyzers: PHPStan, Deptrac, Rector Compared
PHPStan, Psalm, PHP-CS-Fixer, Deptrac, Rector, PHPMD: what each tool does, what it does not do, and the order in which to install them on a Symfony project.
Read more →
RGAA Accessibility Remediation for Symfony Apps
Since the European Accessibility Act, RGAA now covers far more organizations. How we fix a Symfony application: semantic Twig, ARIA, keyboard use and tests.
Read more →
PHP Dead Code: Detect and Remove Unused Code
Find and remove unused code in your PHP projects with PHPStan and shipmonk/dead-code-detector to improve code quality, maintainability and tooling speed.
Read more →