PHP-Parser使用

0x01 Introduction

PHP-Parser是基于PHP内部词法分析方法token_get_all来实现的一个抽象语法树(AST)分析工具。

  • 参考: PHP-Parser

0x02 使用

安装

使用composer 安装

1
composer require nikic/php-parser

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//加载
require_once "vendor/autoload.php";

//声明命名空间
use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpPaser\NodeDumper;

//载入内容
$code = file_get_content('target.php');
//初始化parser实例(这里使用了工厂模式)
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);

$nodeDumper = new NodeDumper;

//解析
try{
$stmts = $parser->parse($code);
echo $nodeDumper->dump($stmts),"\n";
}catch (Error $e){
echo 'Parse Error: ', $e->getMessage();
}

比如target.php的内容是:

1
<?php $a = 1;?>//要加php标签

那么输出:

1
array( 0: Stmt_Expression( expr: Expr_Assign( var: Expr_Variable( name: a ) expr: Scalar_LNumber( value: 1 ) ) ) )

可以看到输出为一个节点数组,这里只有一行,因此只有一个元素。 可以看你到每个节点都是(类型_节点名)表示,在nikic\lib\PhpParser\Node节点下定义,因为在PHP中有大约140种不同的节点[1],这里大致分为了三类:

  • PhpParser: 声明节点(statement nodes),一些声明性的内容,如声明一个类,不具有返回值。
  • PhpParser: 表达式节点(expression nodes),具有返回值,且能在其他表达式中调用的。
  • PhpParser: 纯量节点(scalar values), 包含字符串,数字,常量等。

还有一些节点不在次三类中。

节点遍历

在解析AST的时候PHP-Parser提供了四个钩子,分别在遍历前和遍历后执行beforeTraverseafterTravers在访问每一个节点时执行enterNodeleaveNode。我们需要写一个Visitor子类来描述我们要在这个步骤上做什么。

1
2
3
4
5
6
7
8
9
10
11
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

class MyNodeVisitor extends NodeVisitorAbstract
{
public function leaveNode(Node $node) {
if ($node instanceof Node\Scalar\String_) {
$node->value = 'foo';
}
}
}

这是官方的例子, 描述的是在访问每一个节点后将所有的字符串内容变为foo

我们可以根据上面描述的几类节点更改。比如我要把所有变量名改成var

1
2
3
if ($node instanceof Node\Expr\Variable){
$node->name = 'var';
}

遍历一个节点的时候enterNodeleaveNode先执行, 在使用enterNode的时候节点的子节点还没有访问,一次我们可以使用NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN等来进行访问控制。具体参照官方文档[2]

例子: 比如我们要对一个php中的函数进行切片,可以使用简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
se PhpParser\Node;
use PhpParser\NodeFinder;
use PhpParser\ParserFactory;
/**
* @param $file_name
* @return mixed
*/
function getFunctionsInfo($file_name){
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);

$code = file_get_contents($file_name);

try{
$stmts = $parser->parse($code);

$nodeFinder = new NodeFinder;

// Find all function nodes.
$functions = $nodeFinder->findInstanceOf($stmts, Node\Stmt\Function_::class);
}
catch (Error $e){
echo 'Parse Error: ', $e->getMessage();
}


foreach ($functions as $k=>$v)
{
$function_name = $v->name->name;
$function_lines= $v->getAttributes();
$functions_info[$function_name] = $function_lines;
}

return $functions_info;
}

这里使用了NodeFinder这个更简便的操作[3]