Wer PHPStan zum ersten Mal und gleich auf der strengsten Stufe gegen eine gewachsene Codebase laufen lässt, bekommt keine Liste, sondern eine Wand. Tausende Meldungen, von der echten Fehlerquelle bis zum reinen Rauschen, alle auf einmal. Diese Wand ist der Grund, warum statische Analyse in Legacy-Projekten oft schon nach wenigen Tagen wieder abgeschaltet oder gar nicht erst angewendet wird. Es liegt nicht an PHPStan. Es liegt an der Reihenfolge, in der man es einführt.
Beim Übernehmen eines Projekts gehört statische Analyse für mich auf jeden Fall zu einem der ersten Dinge, die ich einziehe, direkt neben die Tests. Wichtig ist aber, es richtig herum zu tun. Das Ziel ist nicht ein sauberer Code, sondern ein Werkzeug, das ab sofort jeden neuen Fehler abfängt, ohne dass die Altlasten den Betrieb blockieren.
Eines vorweg, damit die Erwartung stimmt: PHPStan ersetzt die Tests nicht. Es führt den Code nie aus, es liest ihn. Falsche Berechnungen, kaputte Fachregeln, Logikfehler sieht es nicht, die fangen die Characterization Tests. PHPStan fängt die andere Hälfte, alles, was schon an der Struktur des Codes als falsch bzw. unsauber erkennbar ist. Zwei Sicherheitsnetze für zwei unterschiedliche Arten von Fehlern. Wie das geht, der Reihe nach.
Warum maximale Strenge am Anfang scheitert
Die höchste Stufe sofort zu verlangen, klingt nach Gründlichkeit, ist aber das Gegenteil von nützlich. Drei Dinge passieren gleichzeitig. Die Meldungsmenge demotiviert, weil niemand vor einer vierstelligen Liste anfängt, sinnvoll zu priorisieren. Jeder Merge wird blockiert, weil die Pipeline rot ist und rot bleibt, egal was man als Nächstes tut. Und die echten Funde ertrinken im Rauschen, weil zwischen einem gefährlichen Typfehler und einer harmlosen fehlenden Annotation kein Unterschied mehr sichtbar ist.
Das Ergebnis ist vorhersehbar. Nach kurzer Zeit schaltet jemand genervt den CI-Schritt wieder aus, und das Werkzeug ist verbrannt, bevor es seinen ersten echten Fehler verhindert hat. Statische Analyse soll Vertrauen aufbauen, und Vertrauen wächst in kleinen Schritten, nicht in einem Sprung auf die Maximalstufe.
Den Ist-Zustand messen
Bevor man eine Stufe festlegt, schaut man, wo die Codebase realistisch steht. PHPStan kommt als Dev-Abhängigkeit ins Projekt:
composer require --dev phpstan/phpstan
Dann ein erster Lauf auf der niedrigsten Stufe, die nur die gröbsten Fehler meldet:
vendor/bin/phpstan analyse src --level 0
PHPStan kennt eine Reihe von Stufen, von 0, das fast nur echte kaputte Aufrufe findet, bis hinauf zur strengsten, die vollständige Typsicherheit verlangt. Praktisch heißt das: nacheinander Stufe 0, 1, 2 und so weiter laufen lassen und schauen, wo die Zahl der Meldungen von handhabbar auf unübersichtlich springt. Dieser Sprung markiert die realistische Obergrenze für den aktuellen Zustand, eine Stufe darunter ist der Startpunkt. Welche das ist, entscheidet der Code, nicht ein Wunschwert. Eine gewachsene Anwendung ohne Typdeklarationen startet deutlich tiefer als eine, die schon länger gepflegt wird, und das ist völlig in Ordnung.
Schon die niedrigen Stufen zahlen sich aus. Sie können in altem Code die Sorte Fehler sichtbar machen, die sonst erst in Produktion auffällt: Aufrufe von Methoden, die es gar nicht gibt, eine falsche Anzahl von Argumenten, problematische Zugriffe auf Eigenschaften. Welche Regel auf welcher Stufe greift, hängt von Version, Code und Erweiterungen ab, aber die Richtung stimmt: Das sind keine Stilfragen, das sind latente Abstürze. Allein dafür lohnt sich das Werkzeug, lange bevor man an die hohen Stufen denkt.
Ein Schritt gehört noch vor die erste ernsthafte Messung: die Framework-Erweiterungen. Der Extension-Installer hängt passende Erweiterungen automatisch in die Config:
composer require --dev phpstan/extension-installer
Welche Erweiterung man dazunimmt, hängt vom Stack ab: phpstan/phpstan-symfony und phpstan/phpstan-doctrine für Symfony und Doctrine. Für Laravel ist es Larastan, das als eigenes Paket dazukommt:
composer require --dev larastan/larastan
Der Extension-Installer allein macht aus PHPStan noch keine Laravel-Analyse, das passende Paket muss installiert und eingebunden sein. Ohne die richtige Erweiterung hält PHPStan korrekten Framework-Code für falsch, und dieser falsche Alarm verfälscht die Standort-Messung von Anfang an.
Die Baseline friert den Ist-Zustand ein
Jetzt kommt der Schritt, der alles trägt. Statt die gefundenen Altlasten abzuarbeiten, friert man sie ein:
vendor/bin/phpstan analyse --generate-baseline
Das schreibt alle aktuellen Meldungen in eine Datei, die phpstan-baseline.neon. Beim nächsten Lauf zieht PHPStan diese Datei ab und meldet nur noch, was nicht drin steht. Die Pipeline ist damit sofort grün, obwohl kein einziger Altfehler behoben wurde. Das ist kein Trick, sondern eine bewusste Grenze: Ab hier macht jede neue Meldung außerhalb der Baseline den Lauf wieder rot, und die alten baut man ab, wenn man die Dateien ohnehin anfasst.
Die phpstan-baseline.neon gehört dabei ins Repository, nicht in eine lokale Ablage. Nur wenn alle dieselbe eingefrorene Grenze sehen, prüft die CI bei jedem denselben Stand. Im Review des Baseline-Diffs sieht man dann, wenn neue Meldungen aufgenommen statt bestehende abgebaut werden: Die Datei wird länger statt kürzer. Dafür darf die Baseline aber nicht unbesehen neu erzeugt werden, sonst verschwindet genau dieses Signal.
Wer meine früheren Beiträge kennt, erkennt das Muster wieder. Die Baseline ist für die statische Analyse das, was der Characterization Test für das Laufzeitverhalten ist. Erst festhalten, was ist, dann bewusst entscheiden, was besser werden soll. Zwei Sicherheitsnetze auf zwei Ebenen, beide nach demselben Prinzip.
Die Config bleibt dabei schlank:
includes:
- phpstan-baseline.neon
parameters:
level: 4
paths:
- src
Mehr braucht es für den Anfang nicht. Die Stufe ist der gemessene Startpunkt, die Baseline hängt drin, und src ist der Bereich, der geprüft wird.
Ein Wort zur Abgrenzung, weil hier gern alles in einen Topf wandert. Die Baseline ist für den Großbestand an Altmeldungen da, die man über die Zeit abbaut. Für einen einzelnen, bewusst akzeptierten Fall gibt es den gezielten Inline-Kommentar @phpstan-ignore direkt an der Zeile, am besten mit der konkreten Fehlerkennung und einer Begründung daneben, damit die Ausnahme eng bleibt und nicht jede Meldung an dieser Zeile stummschaltet. Die Baseline sammelt, was später weg soll. Der Inline-Kommentar markiert eine einzelne, bewusste Ausnahme, die bleiben darf. Wer jede Ausnahme in die Baseline kippt, verliert den Unterschied zwischen Altlast und Entscheidung.
Stufe für Stufe schärfer
Von hier aus wird die Schraube langsam angezogen. Eine Stufe höher, der neue Schwung an Meldungen wird behoben oder, als Ausnahme, bewusst als noch bestehende Altlast in die Baseline aufgenommen, dann der nächste Schritt. Dieses Einfrieren sollte die Ausnahme bleiben, sonst verschiebt man die Grenze nur nach hinten, statt die Stufe wirklich zu halten. Jede Anhebung ist ein eigener, überschaubarer Pull Request, kein Sammel-Commit über die halbe Codebase. So bleibt jeder Schritt prüfbar und im Zweifel zurückrollbar.
Parallel schrumpft die Baseline im Tagesgeschäft. Wer eine Datei für ein Feature ohnehin öffnet, räumt die Altmeldungen darin gleich mit weg und streicht sie aus der Baseline. Das Streichen passiert von Hand: Man löscht den einzelnen Eintrag aus der phpstan-baseline.neon, sobald der Fehler behoben ist, statt die ganze Datei neu zu generieren. Nur so bleibt im Diff sichtbar, dass wirklich etwas abgebaut wurde, und nicht bloß alles neu eingefroren. Das verteilt die Altlast auf viele kleine Gelegenheiten, statt sie als großes Aufräumprojekt zu budgetieren, das nie genehmigt wird. Nach einigen Monaten ist die Baseline oft von selbst klein geworden, ohne dass je ein eigener Sprint dafür nötig war.
Wie weit nach oben? Die höchste Stufe ist kein Pflichtziel. Sie verlangt vollständige Typisierung bis in die letzte Ecke, und für eine Anwendung, die ihren Zweck erfüllt, ist das manchmal mehr Aufwand als Nutzen. Das ehrliche Ziel ist die höchste Stufe, die das Team im Alltag grün hält, ohne jeden zweiten PR zu blockieren. Das kann die strengste sein, muss es aber nicht. Das Werkzeug soll ein Problem lösen und den Code verbessern, nicht den Bestwert erzielen müssen.
Eine Frage bleibt: In welcher Reihenfolge baut man die Baseline ab? Nicht nach Dateiname und nicht quer durch, sondern nach Risiko. Zuerst die Stellen, die oft angefasst werden und fachlich wichtig sind, denn dort richtet ein unentdeckter Typfehler den größten Schaden an. Welche Stellen das sind, lässt sich messen statt raten. Genau darum geht es im nächsten Beitrag dieser Reihe. Bis dahin reicht die Faustregel: Wo ohnehin gearbeitet wird, wird die Baseline nebenbei leichter.
In die CI einhängen
Damit die Regel "kein neuer Fehler" nicht von der Disziplin einzelner abhängt, gehört PHPStan in die Pipeline. Lokal läuft es ohnehin schon: Wer sich den Aufruf als Teil des normalen Ablaufs angewöhnt, sieht die Funde vor dem Push. Die CI ist das Sicherungsseil darunter, nicht der einzige Ort, an dem geprüft wird. Der Schritt läuft bei jedem Push:
vendor/bin/phpstan analyse --no-progress --error-format=github
Das --error-format=github sorgt dafür, dass Funde direkt an der betroffenen Zeile im Pull Request auftauchen, nicht nur im Log. Auf großen Codebases braucht PHPStan dabei Speicher und etwas Zeit; ein höheres --memory-limit, in der CI zum Beispiel --memory-limit=1G oder mehr, und das Caching zwischen den Läufen halten das meist im Rahmen. Ab diesem Moment ist die Grenze automatisch bewacht. Wer einen neuen Fehler einbaut, sieht ihn im PR, bevor der Code gemergt wird. Genau an dieser Stelle hört statische Analyse auf, ein gut gemeintes lokales Werkzeug zu sein, und wird Teil des Prozesses.
Stolperfallen
Die Baseline als Dauerlösung. Sie ist ein Gerüst zum Abbauen, kein Teppich, unter den man die Altlasten kehrt. Wer sie nie kleiner macht, hat die Probleme nicht gelöst, sondern nur unsichtbar gemacht, und die nächste Übernahme findet sie wieder. Ein kurzer Blick auf die Größe der Baseline pro Quartal reicht, um zu sehen, ob sie tatsächlich schrumpft.
Zu schnell zu hohe Stufen. Eine Stufe, die das Team im Alltag nicht halten kann, produziert dieselbe Wand wie am Anfang, nur später. Lieber eine Stufe länger auf einem Niveau bleiben, das grün bleibt, als sich auf einem Niveau festzufahren, auf dem jeder PR an PHPStan scheitert.
Framework-Blindheit, zu spät bemerkt. Die Erweiterungen, die weiter oben erwähnt sind, gehören vor die erste Messung. Wer sie erst nachrüstet, nachdem Stufe und Baseline schon stehen, hat auf halbem falschen Alarm gemessen und muss die Baseline neu aufsetzen. Also zuerst die Erweiterungen, dann messen, dann einfrieren.
Statische Analyse ist bei einer Übernahme eines der Werkzeuge, die am schnellsten Wirkung zeigen, wenn man sie richtig herum einführt. Wenn Sie PHPStan in ein Projekt ziehen wollen und nicht sicher sind, auf welcher Stufe Sie anfangen und wie Sie die Altlast in den Griff bekommen, schildern Sie mir kurz Ihre Ausgangslage. Ich melde mich mit einer ersten Einschätzung. Termin buchen.