Programmatically analyzing and transforming PHP code
Anyone who wants to automatically rewrite, check or generate PHP code cannot avoid AST manipulation. The nikic/php-parser library turns source code into a searchable node tree that custom NodeVisitor classes can read and modify precisely, before the code is printed back out as clean, valid PHP source.
Table of Contents
- 1. What an AST is and why php-parser is the reference
- 2. Installation and the first parse run
- 3. Understanding the node tree: types, NodeDumper, positions
- 4. NodeVisitor: the visitor mechanism for walking the tree
- 5. Practical example: a refactoring visitor for deprecated calls
- 6. Practical example: writing a custom static analysis rule
- 7. Pretty printing: turning modified nodes back into source
- 8. Performance and caching on large codebases
- 9. AST manipulation compared to Reflection and the tokenizer
- 10. Summary
- 11. FAQ
1. What an AST is and why php-parser is the reference
An abstract syntax tree, or AST, is the structured representation of source code as a tree of nodes, where each node models a syntactic construct: a function call, an assignment, a class declaration. Unlike the raw output of a tokenizer, which only produces a flat list of words and symbols, an AST knows the relationships between elements. An if node knows which expression is its condition and which statements belong to its then branch and its else branch. This exact structure is what makes AST manipulation so powerful for tools that need to understand code, not just read it.
The nikic/php-parser library by Nikita Popov has established itself as the de facto standard for AST manipulation in PHP. It is used internally by PHPStan, Psalm, Rector and countless other analysis and refactoring tools, because it ships a complete, version aware parser for every supported PHP language version. Anyone who wants to build their own tools for code analysis, automated refactoring or code generation benefits from exactly the same foundation the established tools rely on, instead of writing an inevitably more error prone parser from scratch.
The decisive difference from the Reflection API lies in timing: reflection inspects code that is already loaded and executable, at runtime. AST manipulation with php-parser works on the source text itself, before it is ever executed, and can therefore analyze code that should never be loaded at all, for example third party libraries during a security audit, or code that only becomes runnable after the transformation itself.
2. Installation and the first parse run
Installation follows the usual Composer path with composer require nikic/php-parser. The entry point for any AST manipulation is a Parser instance created through the ParserFactory. This factory hides the details of which concrete parser implementation is used for the desired target PHP version, since php-parser supports several language versions at once and internally selects the matching grammar.
The parse step itself is unspectacular: a string of PHP source code goes in, an array of Node objects representing the top level code comes out. If parsing fails, for example because the source contains a syntax error, php-parser throws an Error exception with a line number and a message, so custom tools can offer the same error quality as the PHP engine itself.
<?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. Understanding the node tree: types, NodeDumper, positions
Every node in the tree is an instance of a concrete node class, such as Stmt\Class_ for a class declaration, Stmt\ClassMethod for a method, or Expr\MethodCall for a method call. This class hierarchy is the core of any AST manipulation: instead of applying string comparisons to source code fragments, you check with instanceof which concrete node type you are dealing with, then read its public properties, such as the name of a class or the arguments of a call.
For building your own tools, the NodeDumper is indispensable. It prints the entire tree structure as readable text and makes visible how deeply nested certain constructs really are, something that is rarely intuitive from reading source code alone. Every node also carries position attributes such as startLine and endLine, provided the corresponding option was enabled during parsing, which is essential for tools that need to report error locations in the original source.
<?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: the visitor mechanism for walking the tree
Manually walking the tree with nested loops quickly becomes unmanageable on realistic code, because nodes can be nested to arbitrary depth. php-parser solves this with the visitor pattern through the NodeTraverser class combined with custom NodeVisitorAbstract implementations. A visitor implements enterNode() and leaveNode(), which are called automatically for every node in the tree, no matter how deeply it is nested.
The decisive advantage of this pattern for AST manipulation is that the visitor itself never needs to know how deeply a node is nested. It simply reacts to whichever node types it cares about, and the NodeTraverser handles the entire recursion through statements, expressions and nested blocks. If enterNode() returns a new node, the traverser replaces the original node with it, which is the foundation of every code transformation. If the method returns NodeVisitor::REMOVE_NODE, the node is removed from the tree.
<?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. Practical example: a refactoring visitor for deprecated calls
A common use case for AST manipulation is automatically replacing deprecated function calls with their modern equivalent, for example when an internally used utility function is replaced by a new class and the call site has to be adjusted across hundreds of files. Instead of an error prone plain text search and replace, which would also match comments or string literals, a visitor recognizes the exact AST node of a function call and replaces it structurally.
The following visitor searches for calls to the deprecated function calculate_legacy_tax() and replaces them with a method call on a new TaxCalculator class, keeping the original arguments unchanged. This is exactly the principle that Rector also uses for automated refactoring across PHP version upgrades, only here as a strongly simplified, custom implementation tailored to a single project specific use case.
<?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. Practical example: writing a custom static analysis rule
Besides code transformation, AST manipulation is equally well suited for pure analysis without changing any code, for example custom linting rules that enforce project specific conventions for which no ready made PHPStan rule package exists. A visitor that reports every public method missing a return type declaration needs no modification at all, it simply collects findings for a subsequent report.
Such analysis visitors are faster to write than complete PHPStan extensions, because they get by without the extensive rule system and type inference of PHPStan and rely directly on php-parser instead. For focused, project specific checks, such as forbidding certain function calls inside a particular module, a custom visitor is often the more pragmatic path than building a full static analysis rule.
<?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: turning modified nodes back into source
After every transformation, the modified node tree must be turned back into runnable PHP source code. php-parser provides the Standard pretty printer for that, which generates readable, syntactically correct code from the tree. It is important to know that the standard pretty printer completely re-formats the code and does not necessarily preserve original formatting, comments at specific positions or blank lines, unless you deliberately work with php-parser's format preserving functions.
For tools where the formatting of the original matters, for example a refactoring tool that should produce minimally invasive diffs in an existing project, php-parser offers a dedicated format preserving pretty printer that only re-prints the actually changed regions and leaves the rest of the original text untouched. This mode is considerably more complex to use, but produces diffs that stay practical in code review, instead of reformatting an entire file.
<?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 and caching on large codebases
Parsing a single file is fast, but anyone who runs AST manipulation across an entire codebase with thousands of files quickly notices that repeated parsing on every CI run costs noticeable time. The usual approach is caching the parsed tree per file, tied to a hash of the file content, so an unchanged file never needs to be parsed again. This is exactly the principle PHPStan and Rector already use internally to significantly speed up repeated analysis runs.
A second performance lever is deliberately scoping the visitor: anyone interested only in class declarations should return early with return null inside enterNode() as soon as possible, rather than performing unnecessary checks for every single expression node. On very large files with deeply nested expressions, the per node overhead adds up measurably, especially when several visitors run in the same NodeTraverser and every node gets checked multiple times.
9. AST manipulation compared to Reflection and the tokenizer
AST manipulation, the Reflection API and the built in tokenizer solve different problems and complement rather than replace each other. The following table shows which approach is the right choice for which task.
| Task | Approach | Tool | Reason |
|---|---|---|---|
| Inspecting a loaded class | Runtime introspection | Reflection API |
The object already exists in memory |
| Automatically rewriting source code | Structural transformation | nikic/php-parser |
Knows the tree structure, not just text lines |
| Simple syntax highlighting output | Token stream | token_get_all() |
Lightweight, no tree construction needed |
| Custom lint rule for a project | Static analysis | NodeVisitor without modification |
Full context without executing code |
| Analyzing non executable code | Static analysis | nikic/php-parser |
Reflection requires runnable code |
The table shows a pattern: Reflection is the right choice as soon as code can actually be loaded and executed. AST manipulation with php-parser is superior whenever source code must be changed, generated or analyzed without executing it, and the tokenizer remains the lightweight solution for cases where no real tree structure is required.
Mironsoft
PHP tooling, automated refactoring and legacy migrations
Rewriting large PHP codebases automatically?
We build project specific refactoring and analysis tools on top of nikic/php-parser, from one off migration scripts to recurring lint rules inside your CI pipeline.
Migration scripts
AST based bulk rewrites instead of risky search and replace actions
Custom lint rules
Enforcing project specific conventions automatically in CI
Code generation
Generating boilerplate from configuration or schema automatically
10. Summary
AST manipulation with nikic/php-parser turns PHP source code into a searchable node tree that models every syntactic construct as its own node type. The NodeTraverser, combined with custom NodeVisitorAbstract classes, handles the complete recursion through arbitrarily nested code, so custom tools need neither their own grammar nor their own tree walking logic. This allows building both pure analysis rules and real code transformations, which are then printed back into valid PHP source through the pretty printer.
The decisive difference from the Reflection API is timing: AST manipulation works on the source code before it is executed, making it a great fit for migration scripts, project specific lint rules and code generation. For performance critical use on large codebases, caching the parsed trees per file, tied to a hash of the file content, pays off, exactly as PHPStan and Rector already implement internally.
AST Manipulation with nikic/php-parser — The Essentials at a Glance
Node tree
Every syntactic construct is modeled as its own node class, instead of a flat token stream.
NodeVisitor
The NodeTraverser handles recursion, custom visitor classes react to specific node types.
Pretty printing
The standard printer fully re-formats output, format preserving modes keep diffs minimal.
Use cases
Migration scripts, project specific lint rules and code generation without executing code.