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:
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.
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."
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.
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.
$this->get('name')
to constructor injection in Symfony AppIt can do many things for you, depends on how much work you put in it. Today we will try to change method name.
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);
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?
}
}
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);
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
!
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-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!