©Sergey Emelyanov 2025 | Alle Rechte vorbehalten
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:
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.
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)
--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.
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
PHPStan führt Dutzende von Utility-Pseudotypen ein. Einige der nützlichsten sind:
/** @param non-empty-string $externalId */
function pushToERP(string $externalId): void {}
interface PaymentInterface
{
public function charge(int $cents): void;
}
/** @param class-string<PaymentInterface> $driver */
function registerPaymentDriver(string $driver): void {}
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.
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.
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
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.
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.
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.
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
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
$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.
/** @var Repository<Deal> $repo */
$repo = resolve(Repository::class);
$deal = $repo->find(42);
\PHPStan\dumpType($deal); // Expects Deal, not mixed
/** @var Lead|false $lead */
$lead = Lead::find($id);
\PHPStan\dumpType($lead); // Lead|false
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.
PHPStan lässt sich schnell in Laravel-Projekte integrieren, insbesondere mit Larastan. Es bietet:
©Sergey Emelyanov 2025 | Alle Rechte vorbehalten