PHPStan auf Level max: Strategien für ein wachsendes Projekt
AI generated
<?php
8.4
PHP · PHPStan · Static Analysis · CI/CD
PHPStan Level max: Strategien für ein wachsendes Projekt
von Level 0 bis zur maximalen Strictness

PHPStan Level max deckt Fehlerklassen auf, die Unit-Tests oft übersehen: falsche Argumenttypen, riskante Null-Zugriffe, unerreichbaren Code. Ein wachsendes Projekt erreicht dieses Level jedoch nicht über Nacht, sondern über Level-Ratcheting, eine gepflegte Baseline-Datei, sauber typisierte Generics und eigene Regeln für projektspezifische Konventionen.

17 Min. Lesezeit Level-Ratcheting · Baseline · Generics · Custom Rules PHP 8.4 · Composer · CI/CD

1. Was PHPStan Level max wirklich bedeutet

PHPStan kennt Rule-Level von 0 bis 9, jedes höhere Level aktiviert zusätzliche, strengere Prüfungen auf denselben Code. Der Alias level: max in der Konfiguration steht nicht für eine feste Zahl, sondern für das jeweils höchste verfügbare Level einer PHPStan-Version, aktuell identisch mit Level 9, aber bei jedem Major-Release potenziell um neue Prüfungen erweitert. Wer PHPStan Level max in phpstan.neon einträgt, bindet sich also nicht an einen statischen Zustand, sondern an die kontinuierlich wachsende Menge dessen, was das Tool an Typfehlern, unmöglichen Vergleichen und riskanten Zugriffen erkennen kann.

Der praktische Wert davon liegt darin, dieselbe Klasse von Fehlern zu finden, die sonst erst zur Laufzeit oder in einem Code-Review auffällt: Zugriffe auf möglicherweise null, Methodenaufrufe auf Objekten mit falschem Typ, unerreichbarer Code nach einem return, oder Rückgabewerte, die nicht zur deklarierten Signatur passen. Anders als ein Unit-Test prüft PHPStan diese Fälle statisch, ohne den Code auszuführen, und deckt dadurch auch Pfade ab, die kein Test jemals durchläuft. Das gilt unabhängig davon, ob das Projekt ein Framework wie Symfony einsetzt, eine eigene Micro-Architektur nutzt oder, wie in vielen Agentur-Setups, auf einer Plattform wie Magento aufsetzt, PHPStan analysiert reinen PHP-Code, unabhängig vom darüberliegenden Ökosystem.

Der verbreitetste Fehler beim Einstieg ist, die höchste Stufe direkt in ein bestehendes, gewachsenes Projekt einzutragen. Das Ergebnis sind typischerweise mehrere tausend gemeldete Fehler auf einen Schlag, eine Liste, die niemand ernsthaft durcharbeitet, weshalb die Analyse in der Praxis meist ignoriert oder aus der CI-Pipeline wieder entfernt wird. Die folgenden Abschnitte beschreiben, wie ein Projekt die Zielstufe stattdessen schrittweise und ohne Produktivitätsverlust erreicht.

2. Level-Ratcheting: Schrittweise von Level 0 zu PHPStan Level max

Level-Ratcheting bezeichnet die Strategie, das PHPStan-Level nicht in einem Schritt, sondern über mehrere kleine, geplante Erhöhungen anzuheben, jede davon als eigener Commit oder Pull Request. Man beginnt mit einem niedrigen Level, typischerweise 0 oder 1, das nur grundlegende Fehler wie unbekannte Klassen oder Funktionen meldet, behebt die gefundenen Probleme, hebt das Level um eine Stufe an und wiederholt den Vorgang. Der Begriff Ratchet (Sperrklinke) beschreibt treffend das Prinzip: Das Level darf sich nur in eine Richtung bewegen, nach oben, niemals unbeabsichtigt zurückfallen.


# phpstan.neon
parameters:
    level: 6
    paths:
        - src
        - tests
    excludePaths:
        - src/Legacy/*
    tmpDir: var/cache/phpstan
    includes:
        - phpstan-baseline.neon

Jede Anhebung des level-Werts macht sofort sichtbar, wie viele neue Fehlermeldungen die nächste Stufe verursacht, meist deutlich weniger als der Sprung direkt auf die Maximalstufe gewesen wäre. Ein Team kann diese Anzahl gegen die verfügbare Kapazität abwägen: Sind es zwanzig neue Meldungen, lassen sie sich in derselben Woche beheben, sind es zweihundert, wandern die unkritischen Fälle zunächst in die Baseline-Datei, während echte Bugs sofort korrigiert werden. So bleibt das Team stets auf einem validen, grün laufenden Zwischenstand, statt wochenlang an einer Großbaustelle zu arbeiten.

Ein produktives Muster ist, das Ziel-Level als eigenen CI-Check zu etikettieren und pro Quartal oder Sprint eine feste Anhebung einzuplanen, etwa von Level 5 auf 6, dann auf 7, bis die Zielstufe erreicht ist. Für neue Module oder frisch geschriebenen Code lohnt sich zusätzlich eine per-Pfad-Konfiguration mit höherem Level als der Rest des Projekts, damit neuer Code von Anfang an dem strengeren Standard folgt, während Legacy-Bereiche schrittweise nachziehen.

3. Baseline-Dateien: Bestandscode einfrieren, ohne ihn zu ignorieren

Eine phpstan-baseline.neon, erzeugt mit vendor/bin/phpstan analyse --generate-baseline, friert alle zum Erzeugungszeitpunkt bestehenden Fehlermeldungen als konkrete ignoreErrors-Einträge ein, jeweils mit Regex-Pattern, Dateipfad und erwarteter Anzahl. Der entscheidende Unterschied zu einem pauschalen ignoreErrors-Eintrag ohne Baseline: Jeder eingefrorene Fehler ist exakt dokumentiert, an welcher Datei und in welcher Häufigkeit er auftritt. Ändert sich die Anzahl, etwa weil ein neuer, gleichartiger Fehler hinzukommt, schlägt die Analyse fehl, statt den neuen Fehler stillschweigend mit zu verschlucken.


# phpstan-baseline.neon
# Generated with: vendor/bin/phpstan analyse --generate-baseline
parameters:
    ignoreErrors:
        -
            message: '#^Call to an undefined method App\\Legacy\\OrderExporter::mapStatus\(\)\.$#'
            count: 3
            path: src/Legacy/OrderExporter.php
        -
            message: '#^Method App\\Repository\\InvoiceRepository::findOpen\(\) return type has no value type specified in iterable type array\.$#'
            count: 1
            path: src/Repository/InvoiceRepository.php

Die Baseline ermöglicht es, PHPStan Level max sofort für neuen Code zu erzwingen, während der bestehende Bestandscode nicht in einem einzigen Kraftakt umgeschrieben werden muss. Neue Fehler in neuem oder geändertem Code fallen sofort auf, weil sie nicht in der Baseline stehen, während bekannte Altlasten dokumentiert, aber nicht blockierend bleiben. Wichtig ist, die Baseline als technische Schuld zu behandeln, die sichtbar und messbar ist, nicht als dauerhaften Freibrief.

In der Praxis bewährt es sich, die Baseline regelmäßig neu zu generieren und dabei den Diff im Pull Request zu reviewen: Schrumpft die Datei, wurden echte Fehler behoben, wächst sie unerwartet, hat jemand neuen Code ohne ausreichende Typisierung eingecheckt. Bei großen Projekten lohnt sich eine Aufteilung in mehrere Baseline-Dateien pro Modul, eingebunden über separate includes-Einträge, damit ein Team gezielt an der Baseline seines eigenen Verantwortungsbereichs arbeiten kann, ohne die eines anderen Teams zu berühren.

4. Generics und Templates: @template, @extends und PHPDoc-Generics

Plain PHP kennt bis heute keine nativen Generics auf Sprachebene, PHPStan kompensiert diese Lücke jedoch vollständig über PHPDoc-Annotationen wie @template, @extends und @implements. Eine generische Sammlung deklariert einen Platzhalter-Typ T über @template T auf Klassenebene, referenziert ihn in Property- und Methoden-Docblocks, und PHPStan verfolgt diesen Typ durch die gesamte Aufrufkette, exakt wie ein echtes generisches Typsystem.


<?php

declare(strict_types=1);

namespace App\Collection;

/**
 * Generic, type-safe collection of homogeneous items.
 *
 * @template T
 */
class TypedCollection implements \Countable, \IteratorAggregate
{
    /** @var list<T> */
    private array $items = [];

    /**
     * @param class-string<T> $itemClass Fully qualified class name items must be instances of
     */
    public function __construct(private readonly string $itemClass)
    {
    }

    /**
     * @param T $item
     */
    public function add(object $item): void
    {
        if (!$item instanceof $this->itemClass) {
            throw new \InvalidArgumentException('Unexpected item type');
        }
        $this->items[] = $item;
    }

    /**
     * @return T|null
     */
    public function first(): ?object
    {
        return $this->items[0] ?? null;
    }

    public function count(): int
    {
        return count($this->items);
    }

    /**
     * @return \ArrayIterator<int, T>
     */
    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->items);
    }
}

/**
 * @extends TypedCollection<Invoice>
 */
final class InvoiceCollection extends TypedCollection
{
    public function __construct()
    {
        parent::__construct(Invoice::class);
    }
}

Die Annotation @extends TypedCollection<Invoice> bindet T in InvoiceCollection konkret an Invoice, wodurch first() für diese Unterklasse als ?Invoice statt als generisches ?object erkannt wird. Auf der höchsten PHPStan-Stufe wird jeder Aufruf, der eine falsche Klasse in add() übergibt oder das Ergebnis von first() ohne Null-Check als konkretes Objekt behandelt, sofort gemeldet, ein Sicherheitsnetz, das ohne native Generics in PHP sonst gar nicht existieren würde.

Für Kovarianz eignet sich @template-covariant T, wenn ein generischer Typ nur gelesen, nie geschrieben wird, das erlaubt PHPStan, ReadOnlyCollection<Invoice> dort zu akzeptieren, wo ReadOnlyCollection<BillableItem> erwartet wird, sofern Invoice BillableItem implementiert. Moderne IDEs lesen dieselben Annotationen und bieten dadurch korrekte Autovervollständigung für generische Rückgabewerte, ein zusätzlicher Produktivitätsgewinn, der weit über die reine Fehlererkennung hinausgeht.

5. Array Shapes: Präzise Typen für assoziative Arrays

Assoziative Arrays sind in PHP allgegenwärtig, aber ohne zusätzliche Typinformation für PHPStan praktisch unsichtbar: ein einfaches array im Rückgabetyp sagt nichts darüber aus, welche Keys vorhanden sind oder welchen Typ die Werte haben. Array Shapes schließen diese Lücke mit einer PHPDoc-Syntax wie array{name: string, age: int, email?: string}, die exakt beschreibt, welche Keys ein Array besitzt, welchen Typ jeder Wert hat und welche Keys optional sind, gekennzeichnet durch das ? nach dem Key-Namen.

Ergänzend existieren list<T> für sequenzielle Arrays mit garantiert lückenlosen Integer-Keys ab 0, und non-empty-array<T> für Arrays, die PHPStan als garantiert nicht leer behandelt, etwa nach einer expliziten Prüfung mit count($array) > 0. Diese Präzision zahlt sich vor allem auf der strengsten Analyse-Stufe aus, wo generische array-Typen ohne Value-Typ-Angabe ohnehin als Fehler gemeldet werden: Statt jede Funktion mit unspezifischem array-Parameter aufzuweichen, beschreibt ein Array Shape exakt die erwartete Struktur.

Der praktische Nutzen zeigt sich besonders bei Konfigurationsarrays und API-Antwortstrukturen: Ein Tippfehler im Key-Namen, etwa $config['databse'] statt $config['database'], wird sofort erkannt, wenn die Funktion einen Array Shape mit dem korrekten Key erwartet, ein Fehler, der zur Laufzeit sonst erst durch eine fehlende oder null-Rückgabe auffallen würde. Die Migration erfolgt schrittweise: Man beginnt mit den am häufigsten aufgerufenen Funktionssignaturen und ergänzt Array Shapes dort, wo der generische array-Typ bislang echte Typinformation verschleiert.

6. Strictness-Konfiguration: Die Stellschrauben von PHPStan Level max

Die höchste PHPStan-Stufe aktiviert zwar die meisten strengen Prüfungen automatisch, einige Verhaltensweisen bleiben aber über separate Boolean-Parameter in phpstan.neon einstellbar. checkMissingIterableValueType erzwingt, dass jedes iterable und array einen Value-Typ trägt, etwa array<int, Invoice> statt nacktem array, standardmäßig ab mittleren Levels aktiv, aber explizit erzwingbar, falls ein Projekt es früher durchsetzen will. checkGenericClassInNonGenericObjectType meldet, wenn eine generische Klasse ohne Typparameter referenziert wird, etwa Collection statt Collection<Invoice>, und deckt damit unvollständig migrierte Generics-Annotationen auf.

treatPhpDocTypesAsCertain steuert, ob PHPStan PHPDoc-Typangaben als absolut verlässlich behandelt (Standard: true) oder als unsichere Zusatzinformation, die zur Laufzeit abweichen könnte. Auf false gesetzt, verlangt PHPStan zusätzliche Laufzeitprüfungen, bevor es sich auf einen PHPDoc-Typ verlässt, sinnvoll in Projekten mit historisch unzuverlässigen oder veralteten Docblocks, aber auch strenger und aufwendiger in der Migration. Das Paket phpstan-strict-rules ergänzt die eingebauten Regeln um zusätzliche, nicht im Kern enthaltene Prüfungen, etwa ein Verbot impliziter Bool-zu-String-Konvertierungen oder eine Pflicht zu expliziten declare(strict_types=1)-Deklarationen in jeder Datei.

Weitere relevante Knobs sind checkUninitializedProperties, das typisierte Properties ohne Default-Wert und ohne garantierte Initialisierung im Konstruktor meldet, sowie checkBenevolentUnionTypes, das auf implizite, von PHP selbst tolerierte Typ-Koerzionen aufmerksam macht, etwa einen int, der stillschweigend als string weiterverwendet wird. Jeder dieser Parameter lässt sich einzeln vor dem Erreichen der Maximalstufe aktivieren, um die Migration in kleinere, unabhängig überprüfbare Schritte zu zerlegen, statt alle Verschärfungen gleichzeitig einzuführen.

7. Eigene Regeln schreiben: Das Rule-Interface implementieren

Manche projektspezifischen Konventionen lassen sich mit den mitgelieferten Regeln von PHPStan nicht abbilden, etwa das Verbot bestimmter Funktionsaufrufe, eine Pflicht zu final-Klassen in bestimmten Namespaces oder die Durchsetzung eines internen Naming-Schemas. Für solche Fälle implementiert man das Interface PHPStan\Rules\Rule, das genau zwei Methoden verlangt: getNodeType() gibt zurück, für welchen Node-Typ des zugrunde liegenden AST (bereitgestellt von nikic/php-parser) die Regel aufgerufen wird, processNode() erhält den konkreten Node und den aktuellen Scope und liefert eine Liste gefundener Fehler zurück.


<?php

declare(strict_types=1);

namespace App\PHPStan\Rules;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
 * Forbids public methods without an explicit return type declaration.
 *
 * @implements Rule<ClassMethod>
 */
final class RequirePublicReturnTypeRule implements Rule
{
    public function getNodeType(): string
    {
        return ClassMethod::class;
    }

    /**
     * @param ClassMethod $node
     * @return list<\PHPStan\Rules\IdentifierRuleError>
     */
    public function processNode(Node $node, Scope $scope): array
    {
        if (!$node->isPublic() || $node->returnType !== null) {
            return [];
        }

        return [
            RuleErrorBuilder::message(sprintf(
                'Public method %s() has no return type declaration.',
                $node->name->toString()
            ))
                ->identifier('app.missingReturnType')
                ->build(),
        ];
    }
}

Eigene Regeln werden über die services-Sektion in phpstan.neon registriert, mit dem Klassennamen und optional einem tags: [phpstan.rules.rule]-Eintrag, und laufen dann als vollwertiger Teil jeder Analyse neben den eingebauten Prüfungen der höchsten Stufe. Für die Absicherung eigener Regeln stellt PHPStan die Basisklasse PHPStan\Testing\RuleTestCase bereit, die Testfälle mit erwarteten Fehlermeldungen und Zeilennummern gegen Fixture-Dateien prüft, sodass eine eigene Regel genauso zuverlässig getestet wird wie jeder andere Teil der Codebasis.

8. CI-Integration: PHPStan Level max in der Pipeline durchsetzen

Die Grundlage jeder CI-Integration ist eine feste Composer-Abhängigkeit über composer require --dev phpstan/phpstan, ergänzt um phpstan/phpstan-strict-rules für zusätzliche Strenge und gegebenenfalls Erweiterungen für verwendete Frameworks oder Test-Bibliotheken. Ein composer.json-Skript-Eintrag macht den Aufruf für alle Entwickler und die Pipeline identisch, unabhängig davon, ob lokal oder in CI ausgeführt wird.


{
    "scripts": {
        "analyse": "phpstan analyse --memory-limit=1G",
        "analyse:baseline": "phpstan analyse --generate-baseline=phpstan-baseline.neon",
        "test": ["@analyse", "phpunit"]
    },
    "require-dev": {
        "phpstan/phpstan": "^1.11",
        "phpstan/phpstan-strict-rules": "^1.6"
    }
}

In der Pipeline selbst zahlt sich das eingebaute Result-Cache-Feature aus: PHPStan analysiert bei wiederholten Läufen standardmäßig nur geänderte Dateien neu, sofern der Cache-Ordner zwischen Pipeline-Läufen erhalten bleibt, etwa über einen CI-Cache-Key auf Basis der composer.lock. Bei großen Projekten reduziert das die Analysezeit erheblich, von mehreren Minuten auf wenige Sekunden bei unveränderten Dateien. Für Multi-Core-Runner beschleunigt --memory-limit in Kombination mit paralleler Prozessausführung, die PHPStan standardmäßig selbst übernimmt, die Analyse zusätzlich.

Ein entscheidender Punkt für die CI-Integration ist, den Build ausschließlich bei neuen, nicht in der Baseline enthaltenen Fehlern fehlschlagen zu lassen, nicht bei jedem historischen Eintrag. Das hält die Pipeline grün für bestehenden Code, während jeder neue Pull Request denselben strengen Maßstab an neuen oder geänderten Code anlegt. Ein separater, nicht blockierender Report-Job, der die Anzahl der Baseline-Einträge über die Zeit trackt, macht zusätzlich sichtbar, ob technische Schuld ab- oder zunimmt.

9. PHPStan-Level im direkten Vergleich

Die folgende Übersicht zeigt, welche Fehlerklassen ein Projekt auf welchem Level typischerweise aufdeckt, und dient als grobe Orientierung für die Planung des eigenen Level-Ratchetings hin zu PHPStan Level max.

Level Was typischerweise geprüft wird Empfehlung
0 Syntaxfehler, unbekannte Klassen, Funktionen und Konstanten Einstieg bei Legacy-Code ohne jede Analyse-Historie
5 Argumenttypen und Rückgabetypen grob geprüft, einfache Typkonflikte Guter Startpunkt für aktiv gepflegte Projekte
8 Methodenaufrufe auf möglicherweise null, strengere Nullability-Prüfung De-facto-Standard für neue Projekte
9 Striktere Typkompatibilität, Mixed-Type-Missbrauch, unvollständige Generics Für Bibliotheken mit öffentlicher API
max Alle verfügbaren Regeln inkl. neuester Checks, feinste Array-Shape- und Generics-Prüfung Ziel für Projekte mit hoher Qualitätsanforderung

Der Sprung zwischen den unteren Levels kostet in der Regel wenig Aufwand, weil hauptsächlich offensichtliche Fehler wie unbekannte Symbole gemeldet werden. Der größte Migrationsaufwand liegt meist zwischen Level 5 und 8, wenn Nullability-Prüfungen und strengere Typkompatibilität einsetzen. Der letzte Schritt zur Maximalstufe fügt selten fundamental neue Fehlerklassen hinzu, sondern verschärft bestehende Prüfungen und schließt letzte Lücken bei Generics und Array Shapes, weshalb er sich in einem gut vorbereiteten Projekt oft schneller erledigen lässt als die mittleren Levels.

10. Zusammenfassung

PHPStan Level max ist kein Zustand, den man an einem Wochenende erreicht, sondern das Ergebnis eines strukturierten Prozesses. Level-Ratcheting hebt das Level in kleinen, überschaubaren Schritten an, statt das Projekt mit einem einzigen riesigen Fehlerberg zu überfordern. Die phpstan-baseline.neon friert bekannten Bestandscode ein, ohne ihn zu ignorieren, und erzwingt gleichzeitig, dass neuer Code dem strengsten Standard folgt. Generics über @template und @extends sowie Array Shapes schließen Typlücken, die PHP selbst nicht abbilden kann, und machen gerade die strengsten Prüfungen erst wirklich präzise.

Strictness-Parameter wie checkMissingIterableValueType oder treatPhpDocTypesAsCertain erlauben es, einzelne Verschärfungen gezielt vorzuziehen, statt alles gleichzeitig einzuführen. Eigene Regeln über das Rule-Interface schließen die Lücke zwischen generischer Static Analysis und projektspezifischen Konventionen. Und eine saubere CI-Integration sorgt dafür, dass die Zielstufe nicht zu einer einmaligen Übung wird, sondern dauerhaft Teil des Entwicklungsworkflows bleibt, mit Baseline-Wachstum als messbarer Metrik für technische Schuld.

PHPStan Level max für ein wachsendes Projekt - Das Wichtigste auf einen Blick

Level-Ratcheting

Level schrittweise anheben, jede Stufe als eigener Commit, statt mit einem Sprung direkt auf die Maximalstufe zu wechseln.

Baseline-Strategie

phpstan-baseline.neon friert Bestandscode ein, neue Fehler in neuem Code fallen sofort auf, regelmäßig neu generieren.

Generics & Array Shapes

@template, @extends und array{key: type} schließen Typlücken, die PHP ohne native Generics offen lässt.

CI-Integration

Build nur bei neuen Fehlern fehlschlagen lassen, Result-Cache zwischen Läufen erhalten, Baseline-Größe als Metrik tracken.

11. FAQ: PHPStan Level max

1Was ist PHPStan Level max?
Ein Alias für das höchste verfügbare Regel-Level einer PHPStan-Version, aktuell identisch mit Level 9, potenziell erweiterbar bei künftigen Releases.
2Warum nicht direkt auf die höchste Stufe springen?
Ein gewachsenes Projekt erhält dabei typischerweise tausende Fehlermeldungen auf einmal. Level-Ratcheting mit schrittweisen Anhebungen ist praktikabler.
3Was macht eine phpstan-baseline.neon?
Friert bestehende Fehler mit Pattern, Pfad und Anzahl ein. Neue Fehler im selben Code fallen sofort auf, bekannte Altlasten bleiben dokumentiert.
4Wie halte ich die Baseline aktuell?
Regelmäßig mit --generate-baseline neu erzeugen und den Diff im Pull Request reviewen, um Wachstum sofort zu erkennen.
5Was bringen @template und Generics in PHPDoc?
Sie lassen PHPStan Typparameter durch generische Klassen verfolgen, obwohl PHP selbst keine nativen Generics kennt.
6Was ist ein Array Shape?
Eine PHPDoc-Syntax wie array{name: string, age: int}, die exakt Keys und Werttypen eines assoziativen Arrays beschreibt.
7Was macht treatPhpDocTypesAsCertain?
Steuert, ob PHPDoc-Typen als absolut verlässlich gelten (Standard) oder zusätzliche Laufzeitprüfungen erfordern.
8Wie schreibe ich eine eigene PHPStan-Regel?
Über das Interface PHPStan\Rules\Rule mit getNodeType() und processNode(). Registrierung über services in phpstan.neon, Tests über RuleTestCase.
9Wie integriere ich die höchste Analyse-Stufe in CI?
Festes Composer-Skript, CI-Cache für das Result-Cache-Verzeichnis, Build fehlschlagen lassen nur bei Fehlern außerhalb der Baseline.
10Wie lange dauert der Weg zur Maximalstufe?
Abhängig von Projektgröße und Codequalität, meist einige Wochen bis wenige Monate paralleler Weiterentwicklung mit Level-Ratcheting und Baseline.

Mironsoft

Static Analysis, Code-Qualität und CI-Pipelines für PHP-Projekte

Euer Projekt läuft noch auf einem niedrigen PHPStan-Level?

Wir analysieren eure Codebasis, planen ein realistisches Level-Ratcheting bis PHPStan Level max, richten Baseline-Dateien sauber ein und integrieren die Analyse fest in eure CI-Pipeline.

PHPStan-Audit

Bestandsaufnahme des aktuellen Levels und Roadmap für das Level-Ratcheting

Baseline-Setup

phpstan-baseline.neon pro Modul, Generics und Array Shapes für kritische Kernklassen

CI-Integration

Result-Cache, Baseline-Tracking und Build-Regeln, die nur bei echten neuen Fehlern greifen