Methoden und Doctrine-Typen
PHPStan ist der mächtigste statische Analyzer für PHP — aber in Symfony-Projekten stoßen Teams schnell auf Grenzen: magische Methoden in Repositories, Doctrine-Spaltentypen die sich von PHP-Typen unterscheiden, Container-Parameter ohne Typen und komplexe Generic-Annotierungen. Dieser Beitrag zeigt, wie man PHPStan in Symfony systematisch auf Level 9 bringt.
Inhaltsverzeichnis
- 1. Warum PHPStan in Symfony-Projekten besondere Herausforderungen hat
- 2. PHPStan Grundkonfiguration für Symfony-Projekte
- 3. Magische Methoden und Properties typisieren
- 4. Doctrine-Spaltentypen und PHP-Typen korrekt abbilden
- 5. Generics für Repositories und Collections
- 6. phpstan-symfony: Container-Parameter und Service-Typen
- 7. Eigene PHPStan-Rules für Architektur-Regeln
- 8. Stubs für externe Libraries ohne Typen
- 9. PHPStan-Level im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum PHPStan in Symfony-Projekten besondere Herausforderungen hat
PHPStan analysiert PHP-Code statisch, ohne ihn auszuführen — und das ist genau das Problem mit Symfony-Projekten. Symfony macht intensiven Gebrauch von Patterns, die PHPStan ohne zusätzliche Konfiguration nicht versteht: Dependency Injection durch den DI-Container, magische Methoden in Doctrine Repositories (findByName(), findOneByEmail()), dynamische Konfigurationsparameter als Strings, und die Typkonvertierungen zwischen PHP und Datenbank-Spaltentypen, die Doctrine transparent durchführt. Ohne die richtigen Extensions würde PHPStan in jedem dieser Bereiche Fehler melden, die technisch keine echten Fehler sind.
Das Ziel ist PHPStan Level 9 — das höchste Analyse-Level, das unter anderem strikte Typen für alle Rückgabewerte, Argument-Typen und Property-Typen verlangt. Level 9 ist in Symfony-Projekten ohne die richtigen Extensions und Annotierungen nicht erreichbar, weil zu viele Framework-Konstrukte dynamisch sind. Mit phpstan/phpstan-symfony und phpstan/phpstan-doctrine werden die wichtigsten Framework-Konstrukte korrekt analysiert. Darüber hinaus braucht man Generics für Collections und Repositories sowie sorgfältig ausgewählte PHPDoc-Blöcke für die wenigen Stellen, die statisch nicht vollständig herleitbar sind.
2. PHPStan Grundkonfiguration für Symfony-Projekte
Die PHPStan-Konfiguration für Symfony-Projekte beginnt mit der phpstan.neon-Datei im Projekt-Root. Die wichtigsten Parameter: level: 9 für maximale Analyse-Tiefe, paths für die zu analysierenden Verzeichnisse (typischerweise src/ und tests/), und die includes-Liste der Extension-Konfigurationen. Die Extension phpstan/phpstan-symfony bringt die extension.neon-Datei mit, die für Symfony-spezifische Analysen zuständig ist — sie versteht Service-Container, Form-Typen, Twig-Extensions und Event-Subscriber-Interfaces.
Ein häufiger Konfigurationsfehler: Der container_xml_path muss auf den kompilierten DI-Container zeigen, damit phpstan-symfony die Service-Typen kennt. Ohne diesen Pfad kann PHPStan nicht prüfen, ob Services die richtigen Interfaces implementieren. Der Pfad lautet typischerweise var/cache/dev/App_KernelDevDebugContainer.xml. Das Analysieren des Tests-Verzeichnisses erfordert oft einen separaten Level oder zusätzliche Ignore-Regeln, weil Test-Klassen bewusst vereinfachte Typen verwenden. Ein Baseline-File hilft, bestehende Fehler zu erfassen und schrittweise zu beheben, statt Level 9 auf einmal in einem alten Projekt einzuführen.
# phpstan.neon — PHPStan configuration for a Symfony 7 project at level 9
includes:
- vendor/phpstan/phpstan-symfony/extension.neon
- vendor/phpstan/phpstan-symfony/rules.neon
- vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan-doctrine/rules.neon
parameters:
level: 9
paths:
- src/
- tests/
# Point to the compiled DI container — required for service type resolution
symfony:
container_xml_path: var/cache/dev/App_KernelDevDebugContainer.xml
console_application_loader: bin/console
# Doctrine: enable object manager provider for type inference on repositories
doctrine:
objectManagerLoader: tests/object-manager.php
reportUnresolvableQueryBuilderTypes: true
# Ignore known false positives that cannot be fixed with PHPDoc alone
ignoreErrors:
# Symfony's getParameter() returns mixed — acceptable at framework boundaries
- message: '#Call to method getParameter\(\) on an unknown class#'
path: src/
# Treat all files as if they have strict_types=1
treatPhpDocTypesAsCertain: false
3. Magische Methoden und Properties typisieren
Doctrine Repositories haben eine __call()-Methode, die dynamisch Methoden wie findByEmail() und findOneByStatus() erzeugt. Ohne Annotation weiß PHPStan nicht, was diese Methoden zurückgeben. Die korrekte Lösung ist eine @method-PHPDoc-Annotation auf der Repository-Klasse, die jede magische Methode mit ihren Typen explizit deklariert. Das fühlt sich redundant an, ist aber der einzige Weg, PHPStan korrekte Typen zu liefern, ohne auf @phpstan-ignore zurückzufallen.
Bei magischen Properties — __get(), __set(), __isset() — ist die Situation ähnlich. Die @property-PHPDoc-Annotation auf der Klasse deklariert den Typ einer Property, die dynamisch generiert wird. Symfony Form-Klassen nutzen dieses Muster, wenn getData() einen mixed-Typ zurückgibt. Mit einem spezifischen @return FormInterface<ProductFormData>-Annotation auf dem Form-Builder oder einem Generics-Parameter auf der Form-Klasse lässt sich PHPStan Level 9 auch hier erreichen. Die Herausforderung ist, diese Annotierungen konsistent zu pflegen, wenn sich die Domain-Klassen ändern.
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* Repository for Product entities with typed magic method declarations.
*
* @extends ServiceEntityRepository<Product>
*
* Magic methods generated by Doctrine's __call() — declared for PHPStan level 9:
* @method Product|null findOneByName(string $name)
* @method Product|null findOneBySku(string $sku)
* @method Product|null findOneBySlug(string $slug)
* @method Product[] findByCategory(int $categoryId)
* @method Product[] findByStatus(string $status)
* @method Product[] findAll()
* @method Product|null find(mixed $id, mixed $lockMode = null, mixed $lockVersion = null)
* @method Product[] findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
*/
final class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
/**
* Find active products below a price threshold — fully typed, no magic.
*
* @return list<Product>
*/
public function findActiveBelow(float $maxPrice): array
{
return $this->createQueryBuilder('p')
->where('p.price < :maxPrice')
->andWhere('p.status = :status')
->setParameter('maxPrice', $maxPrice)
->setParameter('status', 'active')
->orderBy('p.price', 'ASC')
->getQuery()
->getResult();
}
}
4. Doctrine-Spaltentypen und PHP-Typen korrekt abbilden
Doctrine-Spaltentypen und PHP-Typen sind nicht identisch, und das ist eine der häufigsten Quellen für PHPStan-Fehler in Symfony-Projekten. Ein decimal-Spaltentyp gibt in PHP einen string zurück, kein float — Doctrine macht keine automatische Konversion. Ein datetime_immutable-Typ gibt DateTimeImmutable zurück, aber date gibt ebenfalls DateTimeImmutable zurück. Wer die Property-Typen in Doctrine-Entities nicht präzise auf die tatsächlichen PHP-Rückgabetypen des Doctrine-Typsystems abstimmt, bekommt PHPStan-Fehler, die echte Risiken widerspiegeln.
Die Extension phpstan/phpstan-doctrine kennt das Mapping zwischen Doctrine-Typen und PHP-Typen und prüft, ob die deklarierten PHP-Typen der Entity-Properties mit dem Doctrine-ORM-Mapping übereinstimmen. Wenn eine Property als float $price typisiert ist, aber mit #[ORM\Column(type: 'decimal')] annotiert, meldet phpstan-doctrine einen Fehler: decimal liefert string, nicht float. Die Korrektur ist entweder die Property als string $price zu typisieren oder einen Custom Doctrine Type zu erstellen, der die Konversion transparent macht. Für Geldbeträge ist die Verwendung von string oder einem Value-Object mit einem Custom Type der empfohlene Ansatz.
5. Generics für Repositories und Collections
Generics in PHPStan ermöglichen es, typsichere Collections und Repositories zu definieren, ohne für jeden Entity-Typ eine eigene Interface-Methode zu schreiben. Das Template-Pattern: @template T of object auf der Basis-Repository-Klasse, @extends ServiceEntityRepository<T> auf der Unterklasse und @phpstan-return T auf Methoden. Damit weiß PHPStan, dass ProductRepository::find(1) ein Product zurückgibt und nicht object.
Für Doctrine Collections ist die Generic-Annotation besonders wertvoll: ArrayCollection<int, Product> kommuniziert, dass die Collection Integer-Keys und Product-Werte enthält. Ohne diese Annotation gibt Collection::get() mixed zurück — jede Verwendung des Elements erfordert eine explizite Prüfung oder einen Cast. Mit der Generic-Annotation erkennt PHPStan den Typ automatisch und meldet Fehler, wenn ein falscher Typ in die Collection eingefügt oder ein inkompatibles Ergebnis aus ihr herausgelesen wird. Die PHPDoc-Syntax für Generics ist in PHPStan vollständig unterstützt und eine der mächtigsten Techniken für Typ-Sicherheit in Symfony-Projekten.
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\OrderRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderRepository::class)]
class Order
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
// decimal → string in PHP — PHPStan would flag this as float
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private string $totalAmount = '0.00';
// datetime_immutable → DateTimeImmutable — correct mapping
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $placedAt;
/**
* @var Collection<int, OrderItem> — Generic annotation for PHPStan
*/
#[ORM\OneToMany(mappedBy: 'order', targetEntity: OrderItem::class, cascade: ['persist', 'remove'])]
private Collection $items;
public function __construct()
{
$this->items = new ArrayCollection();
$this->placedAt = new \DateTimeImmutable();
}
/**
* @return Collection<int, OrderItem>
*/
public function getItems(): Collection
{
return $this->items;
}
/**
* Add an item — PHPStan knows $item must be OrderItem due to collection generic.
*/
public function addItem(OrderItem $item): void
{
if (!$this->items->contains($item)) {
$this->items->add($item);
$item->setOrder($this);
}
}
public function getTotalAmount(): string { return $this->totalAmount; }
public function getPlacedAt(): \DateTimeImmutable { return $this->placedAt; }
}
6. phpstan-symfony: Container-Parameter und Service-Typen
Die Extension phpstan-symfony verbindet den kompilierten Symfony DI-Container mit PHPStan. Das ermöglicht, dass PHPStan versteht, welche Services welche Interfaces implementieren, welche Container-Parameter existieren und welchen Typ sie haben. Ohne diese Extension gibt $this->getContainer()->get('some_service') immer mixed zurück. Mit phpstan-symfony und dem Pfad zur Container-XML gibt der Call den korrekten Service-Typ zurück — sofern der Service mit einem Interface oder einer Klasse registriert ist.
Container-Parameter — über $this->getParameter('some_param') in Commands und Controllers aufgerufen — geben in Symfony 6+ konkrete Typen zurück, wenn sie korrekt im Container als getypte Parameter definiert sind. phpstan-symfony prüft, ob der erwartete Typ mit dem tatsächlichen Parameter-Typ übereinstimmt. Für Symfony Commands analysiert die Extension, ob der InputInterface korrekt verwendet wird: getArgument('name') gibt mixed zurück, was einen expliziten Cast oder eine Assertion erfordert. PHPStan mit phpstan-symfony wird so zum Architektur-Reviewer: Er erkennt, wenn Commands Argumente ohne Typ-Prüfung verwenden.
7. Eigene PHPStan-Rules für Architektur-Regeln
Eigene PHPStan-Rules sind eine unterschätzte Möglichkeit, Architektur-Regeln automatisch zu prüfen. Statt in Code-Reviews nach Verletzungen zu suchen, erzwingt eine Custom Rule die Regel bei jedem Analyse-Lauf. Typische Architektur-Regeln für Symfony-Projekte: Repositories dürfen nur von Service-Klassen aufgerufen werden, nicht von Controllern. Entities dürfen keine Service-Abhängigkeiten haben. Handler-Klassen dürfen nur in bestimmten Namespaces liegen. Jede dieser Regeln lässt sich als PHPStan-Rule implementieren, die auf den AST (Abstract Syntax Tree) des Codes zugreift und bei Verletzung einen Fehler meldet.
Eine Custom Rule implementiert PHPStan\Rules\Rule mit einer getNodeType()-Methode, die den AST-Knoten-Typ zurückgibt, der analysiert werden soll, und einer processNode()-Methode, die den Knoten prüft und eine Liste von RuleError-Objekten zurückgibt. Das Registrieren der Rule erfolgt über die phpstan.neon-Konfiguration unter services. Sobald registriert, läuft die Rule bei jedem vendor/bin/phpstan analyse-Aufruf und meldet Verletzungen wie alle anderen PHPStan-Fehler. Custom Rules in der CI-Pipeline einzusetzen schützt die Architektur automatisch vor Regression.
8. Stubs für externe Libraries ohne Typen
Manche externe Libraries haben keine vollständigen PHPDoc-Annotierungen oder verwenden @return mixed für Methoden, die in der Praxis immer einen konkreten Typ zurückgeben. Für diese Fälle schreibt man PHPStan-Stubs: PHP-Dateien, die nur die Interface-Signaturen mit korrekten Typen enthalten, ohne Implementation. PHPStan liest diese Stubs als Typ-Deklarationen und verwendet sie statt der tatsächlichen Library-Klassen. Das ermöglicht Level-9-Analyse auch in Projekten, die Libraries mit schwachen Typen verwenden.
In Symfony-Projekten sind die häufigsten Stub-Kandidaten ältere Symfony-Komponenten, die noch mixed zurückgeben, oder Third-Party-Bundles, die keine PHPStan-Extensions haben. Die Stub-Dateien landen in einem phpstan/stubs/-Verzeichnis und werden in der phpstan.neon unter stubFiles referenziert. Stubs zu pflegen ist Aufwand — deshalb ist es ratsam, zunächst zu prüfen, ob es ein Community-Stub-Projekt gibt (phpstan/phpstan-strict-rules, phpstan/phpstan-beberlei-assert etc.), bevor man eigene schreibt. Für den Symfony-Kern sind Stubs in der Regel nicht nötig, weil Symfony selbst sehr gut typisiert ist.
9. PHPStan-Level im Vergleich
Die neun PHPStan-Levels bauen aufeinander auf — jedes Level fügt strengere Prüfungen hinzu. Der richtige Einstiegslevel für bestehende Projekte ist der erste Level, der keine Fehler produziert, um eine Baseline zu schaffen.
| Level | Wichtigste Prüfungen | Symfony-Hürden | Empfehlung |
|---|---|---|---|
| 0–3 | Undefined variables, basic types | Wenige | Einstieg für Legacy-Projekte |
| 4–5 | Return types, argument types | Magic methods, Container | Neuprojekte ohne Extensions |
| 6–7 | Never types, union type checks | Doctrine-Typen, Generics | Mit phpstan-doctrine erforderlich |
| 8 | Strict mixed, nullable checks | Strict phpstan-symfony nötig | Mit vollständigen Extensions |
| 9 | Alle Prüfungen, strikte Typen | Custom Rules, Stubs, Generics | Ziel für neue Symfony-Projekte |
Der Sprung von Level 8 auf Level 9 ist in Symfony-Projekten der aufwendigste. Level 9 prüft, dass alle Methoden-Rückgabetypen präzise sind — keine mixed-Rückgaben, keine ungetypten Arrays. Mit phpstan-symfony, phpstan-doctrine, vollständigen @method-Annotierungen für Doctrine Repositories und Generic-Annotierungen für Collections ist Level 9 erreichbar und haltbar.
Mironsoft
PHPStan-Integration, Symfony Code-Qualität und statische Analyse
PHPStan Level 9 in eurem Symfony-Projekt einführen?
Wir führen PHPStan systematisch in bestehende Symfony-Projekte ein — von der Baseline über phpstan-symfony und phpstan-doctrine bis zu Custom Rules für eure Architektur-Standards.
Baseline & Migration
PHPStan-Baseline erstellen, schrittweise auf Level 9 migrieren ohne den Entwicklungs-Flow zu unterbrechen
Extension-Setup
phpstan-symfony, phpstan-doctrine und weitere Extensions konfigurieren für vollständige Symfony-Integration
Custom Rules
Architektur-Regeln als PHPStan Custom Rules automatisieren — Repository-Zugriff, Namespace-Trennung, Dependency-Verbote
10. Zusammenfassung
PHPStan Level 9 in Symfony-Projekten ist erreichbar, wenn man die richtigen Extensions und Annotierungs-Strategien konsequent anwendet. phpstan/phpstan-symfony verbindet den DI-Container mit der statischen Analyse. phpstan/phpstan-doctrine prüft das Mapping zwischen Doctrine-Spaltentypen und PHP-Typen. @method-Annotierungen auf Repositories typisieren die magischen Doctrine-Methoden. Generics für Collections und Repositories eliminieren mixed-Typen aus der Entity-Interaktion. Stubs schließen Lücken bei Libraries ohne vollständige Typen.
Custom Rules machen PHPStan zum automatischen Architektur-Wächter: Regeln, die sonst in Code-Reviews geprüft werden, werden bei jedem Analyse-Lauf durchgesetzt. In der CI-Pipeline integriert ist PHPStan damit eine Sicherheitsnetze, das Typ-Fehler, Architektur-Verletzungen und Symfony-Framework-Missverwendungen erkennt, bevor Code in die Produktion gelangt. Die Investition in vollständige PHPStan-Integration zahlt sich in reduziertem Debugging-Aufwand und mehr Vertrauen in Refactorings aus.
Symfony + PHPStan — Das Wichtigste auf einen Blick
Zwei Pflicht-Extensions
phpstan-symfony (Container, Services) + phpstan-doctrine (Spaltentypen, Queries). Ohne beide sind Level 8+ in Symfony-Projekten nicht sinnvoll erreichbar.
Magic Methods typisieren
@method-PHPDoc auf Repository-Klassen für alle Doctrine-Magic-Methoden. @extends ServiceEntityRepository<EntityClass> für Generics-Kompatibilität.
Doctrine-Typen beachten
decimal → string, nicht float. date → DateTimeImmutable. PHPStan-doctrine meldet Typ-Missmatches zwischen PHP-Annotation und ORM-Mapping.
Custom Rules
Rule::processNode() prüft AST-Knoten. In CI-Pipeline erzwingt das Architektur-Regeln automatisch — kein manueller Code-Review für strukturelle Verletzungen nötig.