PHP-Parser入門した

最近「PHPのコードをパースしていい感じにしたい」ということがあった。その時にPHP-Parserについて調べたので、使い方などを軽くメモしておく。

PHP-Parserとは

このライブラリのこと。

github.com

PHPのコードを静的解析して抽象構文木(AST)を生成し、そのASTに対して任意の操作を行ったり、PHPのコードに戻したりできる。ASTについてはインターネット上にいろんな記事があるのでそちらを参照してほしい。

使用例

【注意】以下のサンプルコードは、執筆時点で最新のv4.10.4を使用したものである。

PHPのコードをASTにして、何もせずPHPのコードに戻す

<?php

require_once __DIR__ . '/vendor/autoload.php';

use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter\Standard;

$code = file_get_contents('./Hoge.php');

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);

$printer = new Standard;
$result = $printer->prettyPrintFile($ast);

var_dump($result);

この例だと、空行が削除されるなどの余計なフォーマット処理が走ってしまう。v4.0からそれを回避する機能が実験的に追加されたようで、以下のように書き換えできる。

<?php

require_once __DIR__ . '/vendor/autoload.php';

use PhpParser\Lexer;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard;

$code = file_get_contents('./Hoge.php');

$lexer = new Lexer\Emulative([
    'usedAttributes' => [
        'comments',
        'startLine', 'endLine',
        'startTokenPos', 'endTokenPos',
    ],
    'phpVersion' => Lexer\Emulative::PHP_7_4,
]);
$parser = new Parser\Php7($lexer);

$oldStmts = $parser->parse($code);
$oldTokens = $lexer->getTokens();

$traverser = new NodeTraverser;
$traverser->addVisitor(new NodeVisitor\CloningVisitor);
$newStmts = $traverser->traverse($oldStmts);

$printer = new Standard;
$result = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens);

var_dump($result);

ref: https://github.com/nikic/PHP-Parser/blob/master/doc/component/Pretty_printing.markdown#formatting-preserving-pretty-printing

PHPのコードをASTにして、一部コードを書き換えてからPHPのコードに戻す

例えば、こんなコード(Hoge.php)があるとする。

<?php

class Hoge
{
    public function getMessage() : string
    {
        return 'hoge';
    }
}

このgetMessageの戻り値をhogeからpoyoに変えたい場合は、以下のようなコードを用意すれば良い。

<?php

require_once __DIR__ . '/vendor/autoload.php';

use PhpParser\Lexer;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard;

$code = file_get_contents('./Hoge.php');

$lexer = new Lexer\Emulative([
    'usedAttributes' => [
        'comments',
        'startLine', 'endLine',
        'startTokenPos', 'endTokenPos',
    ],
    'phpVersion' => Lexer\Emulative::PHP_7_4,
]);
$parser = new Parser\Php7($lexer);

$oldStmts = $parser->parse($code);
$oldTokens = $lexer->getTokens();

$traverser = new NodeTraverser;
$traverser->addVisitor(new NodeVisitor\CloningVisitor);
$newStmts = $traverser->traverse($oldStmts);

$traverser = new NodeTraverser;
$traverser->addVisitor(new class extends NodeVisitorAbstract {
    public function enterNode(Node $node)
    {
        if ($node instanceof Node\Stmt\Return_ &&
            $node->expr instanceof Node\Scalar\String_ &&
            $node->expr->value === 'hoge'
        ) {
            $node->expr->value = 'poyo';
        }
    }
});
$newStmts = $traverser->traverse($newStmts);

$printer = new Standard;
$result = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens);

var_dump($result);

本質は$traverser->addVisitor(new class ...で処理を差し込んでいる部分のみ。NodeTraverserを用いることでノードの走査中に実行したい処理を差し込むことができる。処理の実行タイミングは4種類あり、enterNodeはその一種。

ref: https://github.com/nikic/PHP-Parser/blob/master/doc/component/Walking_the_AST.markdown#node-visitors

2回に分けて走査しているが、これは1回目で各ノードの初期状態を複製しておくため。最後のコード出力のprintFormatPreservingで必要。

さいごに

PHP-Parserで最初にやりそうなことを使用例と共にメモしてみた。ここで書いた内容が何となく分かれば、あとは公式ドキュメントを見ながらいろいろ試せるようになると思う。