How to change PHP code with Abstract Syntax Tree

This post was updated at October 2022 with fresh know-how.
What is new?

Updated to php-parser 5 syntax.


Today we can do amazing things with PHP. Thanks to AST and nikic/php-parser we can create very narrow artificial intelligence, which can work for us.

Let's create first its synapse!

We need to make clear what are we talking about right at the beginning. When we say "PHP AST", you can talk about 2 things:

1. php-ast

This is native extension which exports the AST internally used by PHP 7.0+. It allows read-only and is very fast, since it's native C extension. Internal AST was added to PHP 7.0 by skill-full Nikita Popov in this RFC. You can find it on GitHub under nikic/php-ast.

2. PHP AST

This is AST of PHP in Object PHP. It will take your PHP code, turn into PHP object with autocomplete in IDE and allows you to modify code. You can find it on GitHub under nikic/PHP-Parser.

Nikita explains differences between those 2 in more detailed technical way. Personally I love this human reason the most:


"Why would I want to have a PHP parser written in PHP? Well, PHP might not be a language especially suited for fast parsing, but processing the AST is much easier in PHP than it would be in other, faster languages like C. Furthermore the people most probably wanting to do programmatic PHP code analysis are incidentally PHP developers, not C developers."
Nikita Popov

Which one would you pick? If you're lazy like me and hate reading code and writing code over and over again, the 2nd one.

What work can nikic/PHP-Parser do for us?

Saying that, we skip the read-feature of this package - it's used by PHPStan or BetterReflection - and move right to the writing-feature. Btw, back in 2012, even Fabien wanted to use it in PHP CS Fixer, but it wasn't ready yet.

When we say modify and AST together, what can you brainstorm?

It can do many things for you, depends on how much work you put in it. Today we will try to change method name.

4 Steps to Changing a name

1. Parse code to Nodes

composer require nikic/php-parser

Create parser and parse the file:

use PhpParser\ParserFactory;

$parserFactory = new ParserFactory();
$parser = $parserFactory->createForNewestSupportedVersion();

$parsedFileContents = file_get_contents(__DIR__ . '/SomeClass.php');
$astNodes = $parser->parse($parsedFileContents);

2. Find Method Node

The best way to work with Nodes is to traverse them with PhpParser\NodeTraverser:

$nodeTraverser = new PhpParser\NodeTraverser;

$traversedNodes = $nodeTraverser->traverse($nodes);

Now we traversed all nodes, but nothing actually happened. Do you think we forgot to invite somebody in?


Yes, we need PhpParser\NodeVisitor - an interface with 4 methods. We can either implement all 4 of them, or use PhpParser\NodeVisitorAbstract to save some work:

use PhpParser\NodeVisitorAbstract;

final class ChangeMethodNameNodeVisitor extends NodeVisitorAbstract
{
}

We need to find a ClassMethod node. I know that, because I use this package often, but you can find all nodes here. To do that, we'll use enterNode() method:

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node\Stmt\ClassMethod;

final class ChangeMethodNameNodeVisitor extends NodeVisitorAbstract
{
    public function enterNode(Node $node)
    {
        if (! $node instanceof ClassMethod) {
            return null;
        }

        // so we got it, what now?
    }
}

3. Change Method Name

Now we find its name and change it!

use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\NodeVisitorAbstract;

final class ChangeMethodNameNodeVisitor extends NodeVisitorAbstract
{
    public function enterNode(Node $node)
    {
        if (! $node instanceof ClassMethod) {
            return null;
        }

        $node->name = new Name('newName');

        // return node to tell parser to modify it
        return $node;
    }
}

To work with class names, interface names, method names etc., we need to use PhpParser\Node\Name.


Oh, I almost forgot, we need to actually invite visitor to the NodeTraverser like this:

$nodeTraverser = new PhpParser\NodeTraverser;
$nodeTraverser->addVisitor(new ChangeMethodNameNodeVisitor());

// here we parse the file to $astNodes

$traversedAstNodes = $nodeTraverser->traverse($astNodes);

4. Save to File

Last step is saving the file (see docs). We have 2 options here:


A. Dumb Saving

use PhpParser\PrettyPrinter\Standard;

$standardPrinter = new Standard();

// here we parse the file to $astNodes
// and traverse it with node visitors

$newFileContents = $standardPrinter->prettyPrintFile($traversedNodes);

file_put_contents(__DIR__ . '/SomeClass.php', $newFileContents);

But this will actually removes spaces and comments. How to make it right?


B. Format-Preserving Printer

It requires more steps, but you will have output much more under control.

Without our code, it would look like this:

use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\PrettyPrinter\Standard;
use PhpParser\ParserFactory;


// here we create format preserving parser
$parserFactory = new ParserFactory();
$parser = $parserFactory->createForNewestSupportedVersion([
    'usedAttributes' => [
        'comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos',
    ]
]);

$originalAstNodes = $parser->parse($code);

// to keep connections with original nodes
$traverser = new NodeTraverser();
$traverser->addVisitor(new CloningVisitor());
$newStmts = $traverser->traverse($originalAstNodes);


// run our custom node visitors
$nodeTraverser = new NodeTraverser;
$nodeTraverser->addVisitor($nodeVisitor);

$traversedAstNodes = $nodeTraverser->traverse($traversedAstNodes);

$standardPrinter = new Standard();

$newFileContents = $standardPrinter->printFormatPreserving(
    $traversedAstNodes,
    $originalAstNodes,
    $parser->getLexer()->getTokens()
);

Congrats, now you've successfully renamed method to newName!

Advanced Changes? With Rector!

Do you want to see more advanced operations, like those we brainstormed in the beginning? Look at package I'm working on which should automate application upgrades - RectorPHP.


Let me know in the comments, what would you like to read about AST and its Traversing and Modification. I might inspire by your ideas.

Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!