Entwickler, der außergewöhnliche CRM- und Laravel-Lösungen liefert

Als erfahrener Entwickler spezialisiere ich mich auf Laravel- und Vue.js-Entwicklung, die Implementierung von Vtiger CRM sowie auf vielfältige WordPress-Projekte. Meine Arbeit zeichnet sich durch kreative, dynamische und benutzerzentrierte Weblösungen aus, die individuell an die Bedürfnisse meiner Kunden angepasst werden.

PHPStan ist ein statischer Analysator für PHP. Er liest den Quellcode, ohne ihn auszuführen, und findet typische Fehler, noch bevor Tests ausgeführt oder die Anwendung bereitgestellt wird. Neben PHPStan gibt es auch andere alternative Bibliotheken, nämlich Psalm und Phan. PHPStan ist jedoch wesentlich beliebter, hat mehr Sterne auf GitHub, mehr Downloads und ist deutlich performanter.

PHPStan – Eine Anleitung zur Anwendung

  1. Was ist PHPStan und wozu braucht man es?

PHPStan ist ein statischer Analysator für PHP. Er liest den Quellcode, ohne ihn auszuführen, und findet typische Fehler, noch bevor Tests ausgeführt oder die Anwendung bereitgestellt wird. Neben PHPStan gibt es auch andere alternative Bibliotheken, nämlich Psalm und Phan. PHPStan ist jedoch wesentlich beliebter, hat mehr Sterne auf GitHub, mehr Downloads und ist deutlich performanter. Besondere Merkmale, die hervorzuheben sind:

  1. Flexible Strenge-Stufen. Zehn Stufen (0–10) ermöglichen eine schrittweise Annäherung an das „Ideal“, ohne Legacy-Code über Nacht zu zerstören.
  2. Performance. Bei einem Laravel-Projekt mit 3.000 Klassen dauert die Analyse selbst auf mittelmäßigen CI-Agenten nur zwei bis drei Minuten; Phan ist bei gleicher Codebasis normalerweise anderthalb- bis zweimal langsamer.
  3. Qualität des Ökosystems. Fertige Erweiterungen für alle gängigen Frameworks, einschließlich Laravel, Livewire, Eloquent, Doctrine. Psalm hat weniger davon, Phan wird kaum weiterentwickelt.

Zusammenfassend lässt sich sagen, dass PHPStan schneller zu implementieren ist, für jemanden, der an das Laravel-Ökosystem gewöhnt ist, einfacher zu verwenden ist und – was am wichtigsten ist – die Möglichkeit bietet, schrittweise von Stufe 0 auf 10 zu gelangen, ohne alten Code zu zerstören.

  1. Installation in einem Laravel-Projekt
    In Laravel lässt sich PHPStan am einfachsten über Composer hinzufügen:
composer require --dev phpstan/phpstan phpstan/extension-installer
composer require --dev nunomaduro/larastan

Larastan ist eine Erweiterung, die PHPStan Wissen über die „Magie“ von Eloquent, Fassaden, Blade-Direktiven und Ähnlichem vermittelt.

Die minimale Konfigurationsdatei phpstan.neon im Stammverzeichnis des Projekts:

includes:
    - vendor/nunomaduro/larastan/extension.neon

parameters:
    level: 9
    paths:
        - app
        - database
    excludePaths:
        analyse:
            - storage/*
            - bootstrap/cache/*
    checkMissingCallableSignature: true
    missingCheckedExceptionInThrows: true
    stubFiles:
        - stubs/ThirdPartyPayment.stub
    treatPhpDocTypesAsCertain: false
    tmpDir: var/cache/phpstan
    parallel:
        maxProcessCount: 8

Start der Projektanalyse:

./vendor/bin/phpstan analyse

Wenn Sie nur die geänderten Dateien prüfen möchten, können Sie den folgenden Befehl verwenden:

./vendor/bin/phpstan analyse --memory-limit=1G --paths-file=$(git diff --name-only origin/main)
  1. Was erhalten Sie als Ergebnis?
    Nach der Ausführung gibt PHPStan eine Tabelle mit allen gefundenen Problemen aus. Für Laravel gibt es zusätzlich das Format --error-format=github; Fehler werden dabei direkt im Pull Request in Anmerkungen umgewandelt.

Ausgabebeispiel:

 ------ -----------------------------------------------------------------
  Line   app/Services/LeadService.php
 ------ -----------------------------------------------------------------
  89     Method App\Services\LeadService::assignUser() should return void
         but returns Illuminate\Database\Eloquent\Model.
 129     Call to an undefined method App\Models\Lead::approove().
 ------ -----------------------------------------------------------------

[ERROR] Found 2 errors

Sie sehen sofort die Datei, die Zeile und eine verständliche Beschreibung des Problems.

  1. Zusätzliche Möglichkeiten, die PHPStan eröffnet

Typisierung von Callables

Bei der Entwicklung komplexer Projekte müssen wir häufig den Typ callable als Parameter übergeben:

final class DealService
{
    /**
     * @param callable(Deal): bool $filter
     * @return list<Deal>
     */
    public function getFiltered(callable $filter): array
    {
        return Deal::all()->filter($filter)->all();
    }
}

Falls Sie vergessen anzugeben, dass an $filter ein Deal-Objekt übergeben wird, weist PHPStan (mit dem Flag checkMissingCallableSignature) darauf hin: „Definieren Sie die Callable-Signatur.“ Lambdas werden so selbstdokumentierend, und die IDE kann die Felder des Modells innerhalb der anonymen Funktion automatisch vervollständigen.

Unterstützung für Generics
In Laravel-Projekten werden oft Repositories-Pattern implementiert. Generics lösen hierbei das Problem des Rückgabetyps:

/**
 * @template TModel of \Illuminate\Database\Eloquent\Model
 */
final class Repository
{
    /** @var class-string<TModel> */
    private string $model;

    /** @param class-string<TModel> $model */
    public function __construct(string $model)
    {
        $this->model = $model;
    }

    /** @return TModel */
    public function find(int $id)
    {
        return $this->model::query()->findOrFail($id);
    }
}

$leadRepo = new Repository(Lead::class);
/** @var Lead $lead */
$lead = $leadRepo->find(7);   // PHPStan knows here Lead, not mixed

„Bonus“-Typen

PHPStan führt Dutzende von Utility-Pseudotypen ein. Einige der nützlichsten sind:

  • non-empty-string – praktisch, wenn das System eine erforderliche externe Kundenkennung speichert.
/** @param non-empty-string $externalId */
function pushToERP(string $externalId): void {}
  • class-string<PaymentInterface> – genauer Typ für Fabriken:
interface PaymentInterface
{
    public function charge(int $cents): void;
}

/** @param class-string<PaymentInterface> $driver */
function registerPaymentDriver(string $driver): void {}
  • positive-int – nützlich für die Angabe einer Datensatz-ID, die zwingend positiv sein muss.

Präzise Typisierung von Listen und Generatoren

Möchten Sie nicht länger raten müssen, was ein Iterator enthält?

/**
 * @return Iterator<int, Lead>
 */
function loadNewLeads(): Iterator
{
    foreach (Lead::where('status', 'new')->cursor() as $lead) {
        yield $lead->id => $lead;
    }
}

Wenn Sie statt Lead versehentlich Contact verwenden, bemerkt der Analysator dies sofort.

Strikte Deklaration von Exceptions

Der Parameter missingCheckedExceptionInThrows: true zwingt Sie dazu, mögliche Exceptions im PHPDoc zu deklarieren. Java-Entwicklern ist ein ähnlicher Ansatz vertraut.

use App\Exceptions\EmailGatewayException;

final class Mailer
{
    /**
     * @throws EmailGatewayException
     */
    public function sendInvoice(Invoice $invoice, User $user): void
    {
        if (!$this->gateway->push($invoice, $user->email)) {
            throw new EmailGatewayException('SMTP error');
        }
    }
}

Wenn innerhalb einer Methode eine RuntimeException auftritt, die nicht in @throws deklariert ist, meldet PHPStan einen Fehler.

Wir fügen unserem Projekt Unterstützung für Kovarianz hinzu.

Bei einem Standard-@template gilt der Parameter als invariant: Repository<Lead> kann nicht dort eingesetzt werden, wo Repository<Model> erwartet wird, selbst wenn Lead von Model erbt. Durch die Angabe von @template-covariant signalisieren wir PHPStan: „Ich gebe Objekte dieses Typs ausschließlich zurück, nehme sie aber niemals als Eingabe entgegen.“ Dadurch kann eine Instanz mit einem spezifischeren Typ („enger“) sicher einer Variablen mit einem allgemeineren Typ („breiter“) zugewiesen werden.
Die Grundregel lautet: Der kovariante Template-Parameter darf nicht in Eingabeparametern verwendet werden, andernfalls stellt dies eine Typverletzung dar.

Beispiel Nr. 1 — Exporter

namespace App\Export;

use Iterator;

/**
 * @template-covariant TModel of \Illuminate\Database\Eloquent\Model
 */
interface Exporter
{
    /**
     * @return Iterator<int, TModel>
     */
    public function export(): Iterator;
}

Der Exporter gibt Modelle aus, nimmt sie jedoch nicht zurück. Daher ist Kovarianz zulässig.

Konkrete Implementierung:

namespace App\Export;

use App\Models\Lead;
use Generator;

final class LeadCsvExporter implements Exporter
{
    /**
     * @return Generator<int, Lead>
     */
    public function export(): Generator
    {
        foreach (Lead::cursor() as $lead) {
            yield $lead;  // PHPStan знает, что тут Lead
        }
    }
}

Nutzung:

/** @var Exporter<\Illuminate\Database\Eloquent\Model> $exp */
$exp = new LeadCsvExporter();   // ОК: Lead is Model

Ohne template-covariant würde eine solche Zuweisung einen Fehler auslösen: Die Typen sind „zu spezifisch“.

Beispiel №2 — ReadonlyCollection

/**
 * @template-covariant T of object
 * @implements \IteratorAggregate<int, T>
 */
final class ReadonlyCollection implements \IteratorAggregate
{
    /** @var list<T> */
    private array $items;

    /** @param list<T> $items */
    public function __construct(array $items)
    {
        $this->items = $items;
    }

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

Die Kollektion liefert nur Elemente und bietet keine add()- oder set()-Methoden an. Deshalb ist die Kovarianz sicher. Nun:

$leads = new ReadonlyCollection([new Lead(), new Lead()]);
$models = new ReadonlyCollection([new Lead()]);

/** @var ReadonlyCollection<\Illuminate\Database\Eloquent\Model> $anyModels */
$anyModels = $leads;    // Valid thanks to covariance

Beispiel Nr. 3: Paginator für Berichte

/**
 * @template-covariant TReport of \App\DTO\Report
 */
interface Paginator
{
    /**
     * @return list<TReport>
     */
    public function page(int $page, int $perPage = 20): array;
}

LeadReportPaginator implementiert Paginator<LeadReport>, aber wir übergeben es problemlos an eine Methode, die Paginator<Report> erwartet.

So vermeiden Sie Fehler

  1. Stellen Sie sicher, dass der Parameter in den Eingabeargumenten nicht vorkommt.
  2. Fügen Sie ihn nicht in Eigenschaften ein, wenn diese von außen überschrieben werden können.
  3. Geben Sie klare Einschränkungen an (of \BaseClass) – das hilft sowohl Entwicklern als auch PHPStan.
  4. Das Schwierigste – die Einführung in ein bestehendes Projekt

Die meisten Legacy-Projekte haben Zehntausende von Zeilen ohne Type-Hints. „PHPStan auf höchster Stufe aktivieren“ bedeutet, Tausende von Fehlern zu erhalten und das Vorhaben aufzugeben. Daher gibt es nur einen Weg – die schrittweise Einführung.

  1. Setzen Sie das Level auf 0 und fügen Sie eine baseline hinzu:
./vendor/bin/phpstan analyse --generate-baseline

Die Datei phpstan-baseline.neon enthält alle aktuellen Fehler und ignoriert sie. Neuer Code wird jetzt von Grund auf fehlerfrei geschrieben, während der alte Code schrittweise bereinigt wird: Entfernen Sie pro Release ein paar Zeilen aus der Baseline und beheben Sie die entsprechenden Stellen.

  1. Alle paar Wochen erhöhen wir das Level (level: 2, dann 3 …). Die wichtigste Regel: Der Build bleibt immer grün.
  2. Das Wichtigste – vergessen Sie nicht die Implementierung von
    Stub-Dateien – Ihr Schutzschild gegen Bibliotheken, über die Sie keine Kontrolle haben.

Manchmal können wir Code von Drittanbietern nicht ändern: zum Beispiel das SDK einer Bank, das alle denkbaren Regeln verletzt. Die Lösung: eine Stub-Datei.

stubs/ThirdPartyPayment.stub:

<?php declare(strict_types=1);

namespace Bank\SDK;

/**
 * @phpstan-type Currency string  # упрощённый alias
 */
interface Gateway
{
    /**
     * @param non-empty-string $clientId
     * @param positive-int     $amountCents
     * @param Currency         $currency
     *
     * @throws \Bank\SDK\Exception\TransferFailed
     */
    public function transfer(string $clientId, int $amountCents, string $currency): void;
}

In unserem Code stützt sich PHPStan auf Stubs statt auf die tatsächliche, „unsaubere“ Implementierung. Fehler in Stubs dürfen nicht ignoriert werden, sodass die Typen konsistent bleiben.

  1. Wie man Typen nachvollzieht und debuggt?

Sie haben die Level konfiguriert, Generics eingeführt, checkMissingCallableSignature aktiviert, und schon erscheint folgende Meldung in der Konsole:

Parameter #1 $lead of method App\Services\LeadNotifier::notify()
expects App\Models\Lead, Iterator<int, Lead>&non-empty-string given.

Man sieht diesen Hybridtypen Iterator<int, Lead>&non-empty-string und versteht nicht, woher „non‑empty‑string“ kommt. In solchen Fällen hilft die interaktive Ausgabe des tatsächlichen, bereits inferierten Typs direkt im Code.

Wie \PHPStan\dumpType() funktioniert

  1. Die Funktion existiert nur für den Analysator; im Runtime‑Code gibt es sie nicht.
  2. PHPStan ersetzt während der Analyse den Aufruf durch eine „virtuelle“ Funktion und gibt den Typ der Variable oder des Ausdrucks aus.
  3. Sobald Sie alles verstanden haben, wird der Aufruf entfernt oder in if (false) eingeschlossen, damit er später nicht stört.

Ein einfaches Beispiel

namespace App\Services;

use App\Models\Lead;

final class LeadExporter
{
    /**
     * @return list<Lead>
     */
    public function export(): array
    {
        $leads = Lead::query()
            ->where('status', 'new')
            ->pluck('id')    // ❌ typo: pluck returns Collection<string>
            ->all();

        \PHPStan\dumpType($leads); // We will see: array<int, non-empty-string>

        return $leads; // Now we understand problemm
    }
}

Sie starten:

vendor/bin/phpstan analyse app/Services/LeadExporter.php

Und in der Ausgabe taucht zwischen den üblichen Fehlern eine Zeile auf:

Dump of $leads:
array<int, non-empty-string>

Man sieht sofort: Statt Lead haben wir non-empty-string erhalten, was bedeutet, dass die Methode in der ORM-Kette verwechselt wurde.

Nützliche Tricks und Kniffe

  1. Vergleichen, wie sich der Typ Schritt für Schritt ändert
$result = $query->get();
\PHPStan\dumpType($result);         // Collection<int, Lead>
$result = $result->take(10);
\PHPStan\dumpType($result);         // Collection<int, Lead>
$result = $result->toArray();
\PHPStan\dumpType($result);         // array<int, Lead>

So leicht lässt sich die Stelle ausmachen, an der Informationen über Schlüssel oder generische Parameter verloren gegangen sind.

  1. Komplexe Generics debuggen
/** @var Repository<Deal> $repo */
$repo = resolve(Repository::class);
$deal = $repo->find(42);
\PHPStan\dumpType($deal); // Expects Deal, not mixed
  1. Union-Typen in Runtime-Flags überprüfen
/** @var Lead|false $lead */
$lead = Lead::find($id);
\PHPStan\dumpType($lead);            // Lead|false
  1. Verwendung in Tests oder Fixtures
    Manchmal ist es praktisch, „Telemetrie“ im Testcode zu belassen:
if (false) {           // do not executes
    \PHPStan\dumpType(Lead::factory()->make());
}

Dieser Code ist nur für den Analysator sichtbar; die Produktionsumgebung ignoriert ihn.

Die Option --debug ist nicht dasselbe wie dumpType(), aber ebenfalls nützlich
Der Kommandozeilen-Parameter --debug gibt eine Liste der Dateien, die genaue Verarbeitungszeit und den maximalen Speicherverbrauch aus. Das hilft zu verstehen, welcher Service die Analyse verlangsamt, zeigt aber nicht die Variablentypen an. Um die Typen selbst anzuzeigen, verwenden Sie dumpType().

Wenn das Projekt wächst und Generics mit class-string<...> zu Ketten aus drei bis vier Intersection Types (Typüberschneidungen) werden, kann man nicht mehr die gesamte Ausgabe im Kopf behalten. \PHPStan\dumpType() ist eine schnelle Möglichkeit, den Analysator zu fragen: „Wie siehst du das?“ und so innerhalb einer Minute einen Fehler zu finden, für dessen Suche man andernfalls eine Stunde benötigen würde.

Fazit

PHPStan lässt sich schnell in Laravel-Projekte integrieren, insbesondere mit Larastan. Es bietet:

  • sofortiges Feedback zu typischen Fehlern;
  • Unterstützung für Generics, Callable-Signaturen und nützliche Pseudo-Typen;
  • die Möglichkeit, Exceptions wie in Java zu dokumentieren;
  • flexible Stufen für die Strenge der Analyse und eine Baseline für Legacy-Code.