@test
assert
PHPStan · Psalm · PHPUnit · Qualitätskette · CI/CD
PHPStan, Psalm und PHPUnit
als Qualitätskette gemeinsam nutzen

PHPUnit findet Laufzeitfehler – aber nur, wenn die entsprechenden Tests existieren. PHPStan und Psalm finden Typfehler statisch, bevor der Code je ausgeführt wird. Erst die Kombination beider Ansätze schließt die Lücken, die jedes Tool allein offen lässt. Dieser Artikel zeigt, wie man PHPStan, Psalm und PHPUnit als durchgehende Qualitätskette in PHP-Projekten integriert.

20 Min. Lesezeit PHPStan Level · Psalm Baseline · PHPUnit Coverage · CI-Integration PHP 8.x · PHPUnit 10/11 · PHPStan 1.x · Psalm 5.x

1. Warum eine Qualitätskette mehr leistet als einzelne Tools

Jedes Qualitäts-Tool für PHP hat eine andere Perspektive auf denselben Code. PHPUnit prüft das tatsächliche Laufzeitverhalten: Gibt die Methode den erwarteten Wert zurück? Wird die Ausnahme geworfen? Wie verhält sich das System unter konkreten Eingaben? PHPUnit ist blind für Typen – wenn eine Methode einen string zurückgibt, aber der Typ als int deklariert ist, findet PHPUnit das erst dann, wenn ein Test genau diesen Pfad ausführt und auf den Rückgabewert prüft.

PHPStan und Psalm analysieren den Code, ohne ihn auszuführen. Sie verstehen PHP-Typen, PHPDoc-Annotationen und Generics tiefer als PHP selbst und finden Typfehler, null-Dereferenzierungen und fehlerhafte API-Nutzungen sofort – ohne dass ein Test den betroffenen Pfad abdecken muss. Die Kombination ist stärker als die Summe ihrer Teile: PHPStan und Psalm reduzieren die Klasse der Fehler, für die PHPUnit-Tests geschrieben werden müssen, und erhöhen gleichzeitig die Qualität der Tests selbst, indem sie Typfehler in Testklassen aufdecken. Das Resultat ist eine Qualitätskette, die Fehler in mehreren Schichten abfängt.

2. PHPStan: Level, Baseline und Extensions sinnvoll einsetzen

PHPStan arbeitet mit Analyse-Levels von 0 bis 10. Level 0 prüft grundlegende Syntaxprobleme; Level 8 prüft strikte Typen einschließlich nullbarer Rückgaben und generischer Typen; Level 10 schließt auch implizite mixed-Typen ein. In bestehenden Projekten ist es selten möglich, direkt auf Level 8 oder 9 zu starten – die Anzahl der Fehler ist prohibitiv. Die Baseline löst dieses Problem: vendor/bin/phpstan analyse --generate-baseline schreibt alle aktuellen Fehler in eine phpstan-baseline.neon-Datei, die dann in der Konfiguration als bekannte Fehler registriert wird. Ab diesem Punkt schlägt PHPStan nur noch bei neuen Fehlern an – der bestehende technische Schuldenstand wird eingefroren und schrittweise abgebaut.

Extensions erweitern PHPStan um Verständnis für spezifische Frameworks. phpstan-magento kennt Magentos Dependency-Injection-Mechanismus und den Objekt-Manager und verhindert falsch-positive Fehler, die PHPStan ohne diese Extension bei Magento-spezifischen Konstrukten melden würde. phpstan-phpunit versteht PHPUnit-Mock-Objekte und prüft, ob Test-Assertions sinnvoll sind. Diese Extensions sind keine optionalen Extras, sondern Voraussetzung für eine korrekte Analyse in Framework-Projekten.


# phpstan.neon — Konfiguration für Magento-PHPUnit-Projekte

parameters:
  level: 8
  paths:
    - src/app/code/Mironsoft
  excludePaths:
    - src/app/code/Mironsoft/*/Test/Integration
  bootstrapFiles:
    - src/app/bootstrap.php
  ignoreErrors:
    # Suppress known third-party issues
    - '#Call to an undefined method Magento\\Framework\\.*#'
  checkMissingIterableValueType: false

includes:
  - phpstan-baseline.neon
  - vendor/phpstan/phpstan-phpunit/extension.neon
  - vendor/phpstan/phpstan-strict-rules/rules.neon

# Generate baseline for existing projects:
# vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon

# Run analysis:
# vendor/bin/phpstan analyse --memory-limit=512M

3. Psalm: strengere Typen, Taint-Analyse und Plugins

Psalm ist in einigen Bereichen strenger als PHPStan und bietet Features, die PHPStan nicht hat. Die Taint-Analyse verfolgt Benutzereingaben durch den Code und meldet, wenn Daten ohne Sanitisierung in SQL-Abfragen, HTML-Ausgaben oder Shell-Befehle fließen – eine Fähigkeit, die für Sicherheitsanalysen wertvoll ist. Psalm unterstützt außerdem Template-Typen (Generics) mit höherer Granularität und kann Covariance- und Contravariance-Regeln für generische Klassen prüfen.

Die Psalm-Baseline funktioniert ähnlich wie bei PHPStan: vendor/bin/psalm --set-baseline=psalm-baseline.xml schreibt den aktuellen Fehlerzustand und unterdrückt ihn in folgenden Läufen. Psalm-Levels gehen von 1 (strengste) bis 8 (toleranteste) – die umgekehrte Richtung zu PHPStan, was bei gleichzeitiger Nutzung beider Tools gelegentlich zu Verwirrung führt. Level 3 bis 4 ist für die meisten Produktionsprojekte ein realistisches Ziel. Das Psalm-Plugin für PHPUnit (psalm/plugin-phpunit) prüft, ob Mock-Methoden korrekt typisiert sind und ob Assertions die richtigen Typen erwarten.


<!-- psalm.xml — Konfiguration für PHP 8.x-Projekte -->
<?xml version="1.0"?>
<psalm
  errorLevel="4"
  resolveFromConfigFile="true"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="https://getpsalm.org/schema/config"
  xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
  findUnusedVariablesAndParams="true"
  findUnusedCode="true"
>
  <projectFiles>
    <directory name="src/app/code/Mironsoft" />
    <ignoreFiles>
      <directory name="src/app/code/Mironsoft/*/Test/Integration" />
      <directory name="vendor" />
    </ignoreFiles>
  </projectFiles>
  <issueHandlers>
    <!-- Suppress issues that are known false positives in Magento context -->
    <MixedArgumentTypeCoercion errorLevel="suppress" />
  </issueHandlers>
  <plugins>
    <pluginClass class="Psalm\PhpUnitPlugin\Plugin" />
  </plugins>
  <basefile>psalm-baseline.xml</basefile>
</psalm>

4. PHPUnit und statische Analyse: Typen in Tests prüfen

Tests sind Code und verdienen dieselbe Qualitätsprüfung wie Produktionscode. PHPStan und Psalm können Testklassen analysieren und dabei überprüfen, ob die Mocks korrekt typisiert sind, ob Rückgabewerte den erwarteten Typen entsprechen und ob assertSame()- und assertEquals()-Aufrufe typkonsistent sind. Ohne statische Analyse in Tests kann ein Mock einen Rückgabetyp von int liefern, während das getestete Objekt ein string erwartet – und der Test besteht trotzdem, weil PHP-Typen zur Laufzeit manchmal automatisch konvertiert werden.

PHPStan-Extension für PHPUnit (phpstan/phpstan-phpunit) prüft spezifisch, ob die an willReturn() übergebenen Werte dem gemockten Methodenrückgabetyp entsprechen. Das deckt eine ganze Klasse von Testfehlern auf, die durch schlecht typisierte Mocks entstehen und zur Laufzeit nicht auffallen, weil PHP keine strenge Typprüfung in der Mock-Engine durchführt.


<?php
// Example: PHPStan catches mock type errors that PHPUnit misses at runtime

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit\Service;

use Mironsoft\Catalog\Api\ProductRepositoryInterface;
use Mironsoft\Catalog\Model\Product;
use Mironsoft\Catalog\Service\PriceCalculator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

final class PriceCalculatorTest extends TestCase
{
    private MockObject&ProductRepositoryInterface $repository;
    private PriceCalculator $calculator;

    protected function setUp(): void
    {
        $this->repository = $this->createMock(ProductRepositoryInterface::class);
        $this->calculator = new PriceCalculator($this->repository);
    }

    public function testCalculatesPriceWithTax(): void
    {
        $product = new Product(basePrice: 100.0, taxRate: 0.19);

        // PHPStan checks: does willReturn(Product) match getById(): ProductInterface ?
        $this->repository
            ->method('getById')
            ->with(42)
            ->willReturn($product); // PHPStan validates this is ProductInterface

        $result = $this->calculator->calculateGross(productId: 42);

        $this->assertSame(119.0, $result); // PHPStan validates float === float
    }
}

5. Das Zusammenspiel: was PHPStan findet, was PHPUnit findet

Das Zusammenspiel zwischen statischer Analyse und Tests zeigt sich am deutlichsten bei der Fehlerklassifizierung. PHPStan findet: falsche Rückgabetypen, null-Dereferenzierungen, Aufrufe auf nicht-existente Methoden, fehlerhafte Parametertypen und ungenutzte Importe. Diese Fehler existieren unabhängig davon, welche Eingaben das System zur Laufzeit erhält – sie sind im Code verankert und statisch prüfbar. PHPUnit findet: falsches Laufzeitverhalten für spezifische Eingaben, fehlgeschlagene Assertions, nicht geworfene Exceptions und unerwartete Seiteneffekte. Diese Fehler sind eingabeabhängig und können nur durch Ausführung des Codes entdeckt werden.

Die Überlappung ist gering: Ein Typfehler, den PHPStan findet, kann theoretisch auch von einem PHPUnit-Test gefunden werden – aber nur, wenn genau der betroffene Codepfad durch einen Test abgedeckt ist und der Test auf den Rückgabewert prüft. In der Praxis hat keine realistische Test-Suite 100 Prozent Abdeckung aller Typkombinationen. PHPStan ergänzt PHPUnit dort, wo die Testabdeckung Lücken hat, und PHPUnit ergänzt PHPStan dort, wo das Laufzeitverhalten vom statisch analysierbaren Verhalten abweicht – beispielsweise bei dynamischer Typzuweisung, Reflection und Magentos Objekt-Manager.

6. Qualitätskette in der CI-Pipeline

Die Qualitätskette in der CI-Pipeline besteht aus drei aufeinanderfolgenden Stufen: Zuerst die schnelle statische Analyse (PHPStan und Psalm), dann die Unit-Tests, dann die Integrationstests. Durch diese Reihenfolge werden Typfehler bereits in der ersten Stufe abgefangen, ohne dass die langsameren Test-Suiten ausgeführt werden müssen. Ein Pull Request mit PHPStan-Fehlern schlägt in der ersten Minute fehl – nicht erst nach zehn Minuten Integrationstestlauf.

Die Stufenstruktur gibt Entwicklern außerdem klares Feedback darüber, welche Art von Fehler aufgetreten ist. Eine gescheiterte PHPStan-Stage signalisiert einen Typfehler; eine gescheiterte PHPUnit-Stage signalisiert falsches Laufzeitverhalten. Diese Unterscheidung ist diagnostisch wertvoll und beschleunigt die Fehleranalyse. In GitLab CI werden die Stufen als separate Stages konfiguriert; in GitHub Actions als separate Jobs mit expliziten Abhängigkeiten (needs:).

Tool Fehlerklasse Zeitpunkt Stärke / Schwäche
PHPStan Typfehler, null-Deref, ungültige API Statisch (vor Ausführung) Schnell, eingabeunabhängig; blind für Laufzeitverhalten
Psalm Generics, Taint, unused code Statisch (vor Ausführung) Strenger als PHPStan; Taint-Analyse für Sicherheit
PHPUnit Falsches Laufzeitverhalten Zur Laufzeit (Testausführung) Eingabeabhängig; deckt Typen nur bei Coverage ab
Kombination Typen + Verhalten + Sicherheit Stufen in CI-Pipeline Maximale Abdeckung, schnelles Feedback

7. Stufenweise Einführung in bestehende Projekte

In einem bestehenden Projekt ohne statische Analyse ist der erste Lauf von PHPStan oder Psalm oft erschreckend: Hunderte oder tausende von Fehlern, von denen die meisten legitimate Typprobleme sind, die sich über Jahre angesammelt haben. Die Baseline ist die pragmatische Lösung: Sie friert den aktuellen Fehlerzustand ein und ermöglicht es, die Qualitätskette sofort in die CI-Pipeline zu integrieren – ohne dass alle bestehenden Fehler zuerst behoben werden müssen.

Die Strategie für stufenweise Einführung: Zuerst PHPStan auf Level 4 mit Baseline einführen. In den folgenden Wochen neue Fehler (die beim Entwickeln entstehen) direkt beheben, anstatt sie zur Baseline hinzuzufügen. Gleichzeitig den Baseline-Fehlerzähler schrittweise reduzieren – zehn Fehler pro Sprint. Nach einigen Monaten ist der Baseline-Umfang so gering, dass das Level auf 6 oder 8 erhöht werden kann, wieder mit neuer Baseline. Psalm kann parallel oder danach eingeführt werden, um die Analyse zu vertiefen.

9. Zusammenfassung

PHPStan, Psalm und PHPUnit bilden eine Qualitätskette, die stärker ist als jedes Tool allein. PHPStan und Psalm finden Typfehler statisch, ohne Testabdeckung vorauszusetzen. PHPUnit prüft das tatsächliche Laufzeitverhalten für konkrete Eingaben. Die Kombination schließt die Lücken beider Ansätze: Statische Analyse findet die Fehler, die keine Tests abdecken; PHPUnit findet die Fehler, die keine statische Analyse ohne Ausführung aufdecken kann.

Die praktische Einführung beginnt mit der Baseline, die den aktuellen Fehlerzustand einfriert und einen sofortigen Start ermöglicht. PHPStan auf Level 4 bis 6 ist für die meisten PHP-8.x-Projekte ein realistisches Ziel; Psalm ergänzt mit Taint-Analyse und strikteren Generics. Die CI-Pipeline führt statische Analyse vor Unit-Tests durch – so schlagen Typfehler sofort fehl, ohne auf langsame Integrationstests zu warten. Diese Qualitätskette ist die Grundlage für nachhaltig wartbaren PHP-Code.

PHPStan + Psalm + PHPUnit — Das Wichtigste auf einen Blick

PHPStan Baseline

--generate-baseline friert bestehende Fehler ein. Ermöglicht sofortigen Start in CI ohne alle Fehler zuerst zu beheben. Level schrittweise erhöhen.

Psalm Taint-Analyse

Psalm verfolgt Benutzereingaben durch den Code und findet SQL-Injection und XSS-Schwachstellen statisch – ohne Testabdeckung.

PHPUnit Extensions

phpstan/phpstan-phpunit prüft Mock-Typen und Assertion-Konsistenz. psalm/plugin-phpunit ergänzt um PHPUnit-spezifische Typregeln.

CI-Reihenfolge

Statische Analyse zuerst (schnell), dann Unit-Tests, dann Integration. Typfehler schlagen in Minute 1 fehl – nicht nach 10 Minuten Tests.