AST Manipulation with nikic/php-parser: Programmatically Analyzing and Transforming PHP Code
AI generated
<?php
8.4
PHP · AST · Metaprogramming
AST Manipulation with nikic/php-parser
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.

19 min read NodeVisitor · NodeTraverser · PrettyPrinter nikic/php-parser 5.x · PHP 8.4

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.

11. FAQ: AST Manipulation with nikic/php-parser

1What is an AST in PHP?
The structured tree representation of source code where each node models a syntactic construct including its relationships to other elements.
2Why nikic/php-parser?
De facto standard, used by PHPStan, Psalm and Rector, supports multiple PHP versions and is robustly tested.
3AST vs. Reflection API?
Reflection inspects loaded, executable code. AST manipulation works on the source before that, even for non executable code.
4What does a NodeVisitor do?
Implements enterNode()/leaveNode(), called automatically for every node. Return values replace or remove nodes.
5Does formatting survive?
Standard printer re-formats everything. Format preserving printers keep minimally invasive diffs for unchanged regions.
6Only analyze, no change?
Yes, a visitor can exclusively collect findings and always return null, ideal for custom lint rules.
7Parsing large codebases?
Caching the tree per file based on a content hash avoids reparsing files that have not changed.
8Which PHP versions supported?
Multiple language versions at once through configurable lexer and parser variants, independent of the runtime version.
9What about syntax errors?
An Error exception with line number and message, at the same quality as errors from the PHP engine itself.
10What do PHPStan/Rector use it for?
PHPStan for type inference and rule checking, Rector for automated refactoring across version upgrades, both on the same node classes.