PHP-Code programmatisch analysieren und transformieren
Wer PHP-Code automatisiert umbauen, prüfen oder generieren will, kommt an der AST-Manipulation nicht vorbei. Die Bibliothek nikic/php-parser verwandelt Quelltext in einen durchsuchbaren Node-Baum, den eigene NodeVisitor-Klassen gezielt lesen und verändern können, bevor der Code als sauberer PHP-Quelltext wieder ausgegeben wird.
Inhaltsverzeichnis
- 1. Was ein AST ist und warum php-parser die Referenz ist
- 2. Installation und der erste Parse-Durchlauf
- 3. Den Node-Baum verstehen: Typen, NodeDumper, Positionen
- 4. NodeVisitor: der Besucher-Mechanismus zum Durchlaufen
- 5. Praxisbeispiel: ein Refactoring-Visitor für veraltete Aufrufe
- 6. Praxisbeispiel: eine eigene statische Analyse-Regel schreiben
- 7. Pretty-Printing: veränderten Code zurück in Quelltext wandeln
- 8. Performance und Caching bei großen Codebasen
- 9. AST-Manipulation im Vergleich zu Reflection und Tokenizer
- 10. Zusammenfassung
- 11. FAQ
1. Was ein AST ist und warum php-parser die Referenz ist
Ein abstrakter Syntaxbaum, kurz AST, ist die strukturierte Repräsentation von Quellcode als Baum aus Knoten, bei dem jeder Knoten eine syntaktische Konstruktion abbildet: ein Funktionsaufruf, eine Zuweisung, eine Klassendeklaration. Anders als beim rohen Tokenizer-Ergebnis, das nur eine flache Liste von Wörtern und Symbolen liefert, kennt ein AST die Beziehungen zwischen den Elementen. Ein if-Knoten weiß, welcher Ausdruck seine Bedingung ist und welche Anweisungen zu seinem Then- und Else-Zweig gehören. Genau diese Struktur macht AST-Manipulation so mächtig für Werkzeuge, die Code nicht nur lesen, sondern verstehen müssen.
Die Bibliothek nikic/php-parser von Nikita Popov hat sich als De-facto-Standard für AST-Manipulation in PHP etabliert. Sie wird intern von PHPStan, Psalm, Rector und zahllosen weiteren Analyse- und Refactoring-Werkzeugen eingesetzt, weil sie einen vollständigen, versionsbewussten Parser für alle unterstützten PHP-Sprachversionen mitbringt. Wer eigene Werkzeuge für Codeanalyse, automatisiertes Refactoring oder Codegenerierung bauen will, profitiert von genau derselben Grundlage, auf der auch die etablierten Tools stehen, statt einen eigenen, zwangsläufig fehleranfälligeren Parser zu schreiben.
Der entscheidende Unterschied zur Reflection API liegt im Zeitpunkt: Reflection untersucht bereits geladenen, ausführbaren Code zur Laufzeit. AST-Manipulation mit php-parser arbeitet auf dem Quelltext selbst, bevor er überhaupt ausgeführt wird, und kann daher auch Code analysieren, der niemals geladen werden soll, etwa fremde Bibliotheken bei einem Sicherheits-Audit oder Code, der erst durch die Transformation lauffähig wird.
2. Installation und der erste Parse-Durchlauf
Die Installation erfolgt klassisch über Composer mit composer require nikic/php-parser. Der Einstiegspunkt für jede AST-Manipulation ist eine Parser-Instanz, die über die ParserFactory erzeugt wird. Diese Factory kapselt die Details, welche konkrete Parser-Implementierung für die gewünschte PHP-Zielversion verwendet wird, denn php-parser unterstützt mehrere Sprachversionen gleichzeitig und wählt intern die passende Grammatik.
Der Parse-Vorgang selbst ist unspektakulär: Ein String mit PHP-Quellcode geht hinein, ein Array von Node-Objekten kommt heraus, die den Wurzelebenen-Code repräsentieren. Schlägt der Parse-Vorgang fehl, etwa weil der Quelltext einen Syntaxfehler enthält, wirft php-parser eine Error-Exception mit Zeilennummer und Fehlermeldung, sodass eigene Werkzeuge dieselbe Fehlerqualität liefern können wie die PHP-Engine selbst.
<?php
declare(strict_types=1);
use PhpParser\ParserFactory;
use PhpParser\Error;
require __DIR__ . '/vendor/autoload.php';
$code = <<<'PHP'
<?php
final class InvoiceCalculator
{
public function total(array $items): float
{
return array_sum(array_column($items, 'price'));
}
}
PHP;
$parser = (new ParserFactory())->createForNewestSupportedVersion();
try {
// Parsing the source turns it into an array of AST nodes
$ast = $parser->parse($code);
} catch (Error $error) {
echo 'Parse error: ' . $error->getMessage() . PHP_EOL;
exit(1);
}
echo 'Top-level nodes: ' . count($ast) . PHP_EOL; // 1 (the class declaration)
3. Den Node-Baum verstehen: Typen, NodeDumper, Positionen
Jeder Knoten im Baum ist eine Instanz einer konkreten Node-Klasse, etwa Stmt\Class_ für eine Klassendeklaration, Stmt\ClassMethod für eine Methode oder Expr\MethodCall für einen Methodenaufruf. Diese Klassenhierarchie ist der Kern jeder AST-Manipulation: Statt String-Vergleiche auf Quellcode-Fragmenten anzuwenden, prüft man mit instanceof, um welchen konkreten Knotentyp es sich handelt, und liest anschließend dessen öffentliche Properties aus, etwa den Namen einer Klasse oder die Argumente eines Aufrufs.
Für die Entwicklung eigener Werkzeuge ist der NodeDumper unverzichtbar. Er gibt die komplette Baumstruktur als lesbaren Text aus und macht sichtbar, wie tief verschachtelt bestimmte Konstruktionen tatsächlich sind, etwas, das aus reiner Quellcode-Betrachtung selten intuitiv ist. Jeder Knoten trägt außerdem Positionsattribute wie startLine und endLine, sofern beim Parsen die entsprechende Option aktiviert wurde, was für Werkzeuge, die Fehlerorte im Original-Quelltext melden müssen, unverzichtbar ist.
<?php
declare(strict_types=1);
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Class_;
$parser = (new ParserFactory())->createForNewestSupportedVersion();
$ast = $parser->parse(file_get_contents('InvoiceCalculator.php'));
// Dump the raw node structure for exploration during development
$dumper = new NodeDumper(['dumpPositions' => true]);
echo $dumper->dump($ast) . PHP_EOL;
// Walk the tree manually to find every method inside every class
foreach ($ast as $node) {
if (!$node instanceof Class_) {
continue;
}
foreach ($node->getMethods() as $method) {
assert($method instanceof ClassMethod);
printf('%s::%s() at line %d%s', $node->name, $method->name, $method->getStartLine(), PHP_EOL);
}
}
4. NodeVisitor: der Besucher-Mechanismus zum Durchlaufen
Manuelles Durchlaufen des Baums mit verschachtelten Schleifen wird bei realistischem Code schnell unübersichtlich, weil Knoten beliebig tief verschachtelt sein können. Php-parser löst das mit dem Visitor-Pattern über die Klasse NodeTraverser in Kombination mit eigenen NodeVisitorAbstract-Implementierungen. Ein Visitor implementiert enterNode() und leaveNode(), die für jeden Knoten im Baum automatisch aufgerufen werden, egal auf welcher Verschachtelungstiefe er liegt.
Der entscheidende Vorteil dieses Musters für die AST-Manipulation: Der Visitor muss selbst nicht wissen, wie tief ein Knoten verschachtelt ist. Er reagiert einfach auf jeden Knotentyp, der ihn interessiert, und der NodeTraverser übernimmt die komplette Rekursion durch Statements, Ausdrücke und verschachtelte Blöcke. Gibt enterNode() einen neuen Node zurück, ersetzt der Traverser den ursprünglichen Knoten durch diesen, was die Grundlage jeder Code-Transformation ist. Gibt die Methode NodeVisitor::REMOVE_NODE zurück, wird der Knoten aus dem Baum entfernt.
<?php
declare(strict_types=1);
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
final class MethodCallCounterVisitor extends NodeVisitorAbstract
{
private array $calls = [];
public function enterNode(Node $node): ?int
{
if ($node instanceof Node\Expr\MethodCall && $node->name instanceof Node\Identifier) {
$name = $node->name->toString();
$this->calls[$name] = ($this->calls[$name] ?? 0) + 1;
}
return null; // do not replace the node
}
public function getCalls(): array
{
return $this->calls;
}
}
5. Praxisbeispiel: ein Refactoring-Visitor für veraltete Aufrufe
Ein alltäglicher Anwendungsfall für AST-Manipulation ist das automatisierte Ersetzen veralteter Funktionsaufrufe durch ihr modernes Äquivalent, etwa wenn eine intern genutzte Utility-Funktion durch eine neue Klasse ersetzt wird und der Aufruf in hunderten Dateien angepasst werden muss. Statt eines fehleranfälligen Suchen-und-Ersetzen über reinen Text, das auch Kommentare oder String-Literale träfe, erkennt ein Visitor den exakten AST-Knoten eines Funktionsaufrufs und ersetzt ihn strukturell.
Der folgende Visitor sucht nach Aufrufen der veralteten Funktion calculate_legacy_tax() und ersetzt sie durch einen Methodenaufruf auf eine neue TaxCalculator-Klasse, wobei die ursprünglichen Argumente unverändert übernommen werden. Diese Art von Transformation ist exakt das Prinzip, das auch Rector für automatisiertes Refactoring bei PHP-Versionswechseln nutzt, nur in einer stark vereinfachten Eigenimplementierung, die sich gezielt auf einen einzigen projektspezifischen Anwendungsfall zuschneiden lässt.
<?php
declare(strict_types=1);
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\NodeVisitorAbstract;
final class LegacyTaxCallReplacer extends NodeVisitorAbstract
{
public function enterNode(Node $node): ?Node
{
if (!$node instanceof FuncCall || !$node->name instanceof Name) {
return null;
}
if ($node->name->toString() !== 'calculate_legacy_tax') {
return null;
}
// Replace the deprecated function call with a method call
// on a freshly instantiated TaxCalculator, keeping all arguments.
return new MethodCall(
new New_(new Name('TaxCalculator')),
new Identifier('calculate'),
$node->args
);
}
}
6. Praxisbeispiel: eine eigene statische Analyse-Regel schreiben
Neben Code-Transformation eignet sich AST-Manipulation ebenso gut für reine Analyse ohne Veränderung des Codes, etwa für eigene Linting-Regeln, die spezifische Konventionen eines Projekts durchsetzen, für die es kein fertiges PHPStan-Rule-Paket gibt. Ein Visitor, der beispielsweise jede öffentliche Methode ohne Typdeklaration für den Rückgabewert meldet, benötigt keine Modifikation, sondern sammelt lediglich Fundstellen für einen anschließenden Bericht.
Solche Analyse-Visitors sind schneller zu schreiben als vollständige PHPStan-Extensions, weil sie ohne das umfangreiche Regelsystem und die Typinferenz von PHPStan auskommen und sich direkt auf php-parser stützen. Für punktuelle, projektspezifische Prüfungen, etwa das Verbot bestimmter Funktionsaufrufe in einem Modul, ist ein eigener Visitor oft der pragmatischere Weg als eine vollwertige Static-Analysis-Regel zu entwickeln.
<?php
declare(strict_types=1);
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\NodeVisitorAbstract;
final class MissingReturnTypeRule extends NodeVisitorAbstract
{
/** @var array<int, array{method: string, line: int}> */
private array $violations = [];
public function enterNode(Node $node): ?int
{
if (!$node instanceof ClassMethod || !$node->isPublic()) {
return null;
}
if ($node->returnType === null) {
$this->violations[] = [
'method' => $node->name->toString(),
'line' => $node->getStartLine(),
];
}
return null;
}
/** @return array<int, array{method: string, line: int}> */
public function getViolations(): array
{
return $this->violations;
}
}
7. Pretty-Printing: veränderten Code zurück in Quelltext wandeln
Nach jeder Transformation muss der veränderte Node-Baum wieder in ausführbaren PHP-Quelltext zurückverwandelt werden. Php-parser liefert dafür den Standard-PrettyPrinter, der aus dem Baum lesbaren, syntaktisch korrekten Code erzeugt. Wichtig zu wissen ist, dass der Standard-PrettyPrinter den Code komplett neu formatiert und dabei ursprüngliche Formatierung, Kommentare an bestimmten Positionen und Leerzeilen nicht zwingend originalgetreu erhält, sofern man nicht gezielt mit den Format-Preserving-Funktionen von php-parser arbeitet.
Für Werkzeuge, bei denen die Formatierung des Originals wichtig ist, etwa ein Refactoring-Tool, das in einem bestehenden Projekt minimal-invasive Diffs erzeugen soll, bietet php-parser einen speziellen format-preserving PrettyPrinter, der nur die tatsächlich veränderten Bereiche neu ausgibt und den Rest des Originaltexts unverändert lässt. Dieser Modus ist deutlich komplexer in der Anwendung, liefert aber Diffs, die in Code-Reviews praktikabel bleiben, statt eine komplette Datei umzuformatieren.
<?php
declare(strict_types=1);
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
$code = file_get_contents('LegacyBilling.php');
$parser = (new ParserFactory())->createForNewestSupportedVersion();
$originalAst = $parser->parse($code);
$traverser = new NodeTraverser();
$traverser->addVisitor(new CloningVisitor()); // keeps original attributes for diffing
$traverser->addVisitor(new LegacyTaxCallReplacer());
$newAst = $traverser->traverse($originalAst);
// Standard printer: fully re-formats the output
$printer = new PrettyPrinter\Standard();
$newCode = $printer->prettyPrintFile($newAst);
file_put_contents('LegacyBilling.php', $newCode);
echo 'Rewritten ' . substr_count($newCode, 'TaxCalculator') . ' occurrence(s)' . PHP_EOL;
8. Performance und Caching bei großen Codebasen
Das Parsen einer einzelnen Datei ist schnell, aber wer AST-Manipulation über eine gesamte Codebasis mit mehreren tausend Dateien laufen lässt, spürt schnell, dass das wiederholte Parsen bei jedem CI-Lauf spürbar Zeit kostet. Der übliche Ansatz ist Caching des geparsten Baums pro Datei, verknüpft mit einem Hash des Dateiinhalts, sodass eine unveränderte Datei nicht erneut geparst werden muss. Genau dieses Prinzip nutzen PHPStan und Rector intern, um wiederholte Analyseläufe deutlich zu beschleunigen.
Ein zweiter Performance-Hebel ist die gezielte Beschränkung des Visitors: Wer nur an Klassendeklarationen interessiert ist, sollte in enterNode() so früh wie möglich mit return null abbrechen, statt für jeden einzelnen Ausdrucksknoten unnötige Prüfungen durchzuführen. Bei sehr großen Dateien mit tief verschachtelten Ausdrücken summiert sich der Overhead pro Knoten messbar, insbesondere wenn mehrere Visitors gleichzeitig im selben NodeTraverser registriert sind und jeder Knoten mehrfach geprüft wird.
9. AST-Manipulation im Vergleich zu Reflection und Tokenizer
AST-Manipulation, Reflection API und der eingebaute Tokenizer lösen unterschiedliche Probleme und ergänzen sich, statt sich gegenseitig zu ersetzen. Die folgende Tabelle ordnet ein, wann welcher Ansatz die richtige Wahl ist.
| Aufgabe | Ansatz | Werkzeug | Grund |
|---|---|---|---|
| Geladene Klasse untersuchen | Laufzeit-Introspektion | Reflection API |
Objekt existiert bereits im Speicher |
| Quelltext automatisiert umschreiben | Strukturelle Transformation | nikic/php-parser |
Kennt Baumstruktur statt reiner Textzeilen |
| Einfache Syntax-Highlighting-Ausgabe | Token-Stream | token_get_all() |
Leichtgewichtig, keine Baumkonstruktion nötig |
| Custom Lint-Regel für ein Projekt | Statische Analyse | NodeVisitor ohne Modifikation |
Vollständiger Kontext ohne Code-Ausführung |
| Nicht ausführbaren Code analysieren | Statische Analyse | nikic/php-parser |
Reflection benötigt lauffähigen Code |
Die Tabelle zeigt: Reflection ist die richtige Wahl, sobald Code tatsächlich geladen und ausgeführt werden kann. AST-Manipulation mit php-parser ist überlegen, wenn Quelltext verändert, generiert oder analysiert werden muss, ohne ihn auszuführen, und der Tokenizer bleibt die leichtgewichtige Lösung für Fälle, in denen keine echte Baumstruktur nötig ist.
Mironsoft
PHP-Tooling, automatisiertes Refactoring und Legacy-Migrationen
Große PHP-Codebasen automatisiert umbauen?
Wir entwickeln projektspezifische Refactoring- und Analyse-Werkzeuge auf Basis von nikic/php-parser, von einmaligen Migrationsskripten bis zu wiederkehrenden Lint-Regeln in eurer CI-Pipeline.
Migrationsskripte
AST-basierte Massenumbauten statt riskanter Suchen-und-Ersetzen-Aktionen
Custom Lint-Regeln
Projektspezifische Konventionen automatisiert in der CI durchsetzen
Codegenerierung
Boilerplate aus Konfiguration oder Schema automatisiert erzeugen
10. Zusammenfassung
AST-Manipulation mit nikic/php-parser verwandelt PHP-Quelltext in einen durchsuchbaren Node-Baum, der jede syntaktische Konstruktion als eigenen Knotentyp abbildet. Der NodeTraverser übernimmt zusammen mit eigenen NodeVisitorAbstract-Klassen die vollständige Rekursion durch beliebig verschachtelten Code, sodass eigene Werkzeuge weder eine eigene Grammatik noch eine eigene Baumdurchlaufslogik implementieren müssen. Damit lassen sich sowohl reine Analyse-Regeln als auch echte Code-Transformationen bauen, die anschließend über den PrettyPrinter wieder in gültigen PHP-Quelltext zurückverwandelt werden.
Der entscheidende Unterschied zur Reflection API ist der Zeitpunkt: AST-Manipulation arbeitet auf dem Quelltext, bevor er ausgeführt wird, und eignet sich damit für Migrationsskripte, projektspezifische Lint-Regeln und Codegenerierung. Für Performance-kritische Anwendungen auf großen Codebasen lohnt sich Caching der geparsten Bäume pro Datei, verknüpft mit einem Hash des Dateiinhalts, genau wie es PHPStan und Rector intern bereits umsetzen.
AST-Manipulation mit nikic/php-parser — Das Wichtigste auf einen Blick
Node-Baum
Jede syntaktische Konstruktion wird als eigene Node-Klasse abgebildet, statt als flacher Token-Strom.
NodeVisitor
Der NodeTraverser übernimmt die Rekursion, eigene Visitor-Klassen reagieren gezielt auf Knotentypen.
Pretty-Printing
Der Standard-Printer formatiert komplett neu, format-preserving Modi erhalten minimale Diffs.
Einsatzgebiet
Migrationsskripte, projektspezifische Lint-Regeln und Codegenerierung ohne Code-Ausführung.