个人资料
php Parser是由英俊睫毛开发的PHP抽象语法树(AST )分析工具。 PHP Parser兼具界面易用性、结构简洁、工具链完善等诸多优点。 在工程设计中,通常使用PHP Paser生成模板代码,或者使用生成的抽象语法树进行静态分析。
抽象语法树VS反射
反射率
反射在程序运行时动态分析类和方法的结构。 根据反射得到的结构,程序可以动态访问类和类的属性方法,也可以用于创建类实例。 对于抽象语法树,反射所获取的结构可以更清楚地反映类结构,因此框架常用于实现路由等功能。
抽象语法树
抽象语法树是程序语言的源代码经过句法分析和词法分析得到的分析结构。 除了反射得到的信息外,还包括注释、方法、函数的逻辑结构,抽象语法树被认为与源代码等价。
PHP解析器详细信息
功能入口
PHP parserparserfactory :3360 create (int $ kind,Lexer $lexer=null,array $parserOptions=[] ) 3360PHPparser
创建解析器,$ kind one of :3360 prefer _ PHP 7,3360 prefer _ PHP 5,3360 only _ PHP7or :3360 only _ PHP 5
PHP parserparser :3360 parse (字符串$ code,错误处理程序$ error handler=null ) :node(stmt[]|null
所有解析器都实现此方法,传递代码并返回抽象语法树。
pretty printer (标准:3360 pretty print file ($ ast ) : string
用于将抽象语法树转换为代码。
名称空间
PHP帕rsernode
包含抽象语法树的所有节点,代码中的变量声明、类引用和逻辑表达式可以用相应的Node表示。
PhpParserNodeStmt
包含表示namespace的公式节点,如namespace、class的class和类方法的ClassMethod。 此示例说明如何分析和修改表达式:
PhpParserBuilder
此命名空间下包含生成节点的工厂类,可以使用getNode方法获取相应的节点。
应用示例
一、源代码分析与生成
二、在代码中自动添加测试
要求的说明
假设在服务层中使用一个名为public static function的通用静态方法提供服务。 要确保每个方法都有单元测试,必须查找服务中存在的方法,并生成测试类和测试方法。 当然,因为每个服务都包含许多方法,所以在添加方法时,您不希望每次都手动迁移旧测试,因此必须添加测试。
输出结果(新建测试) ) ) ) ) )。
名称空间测试(单元;
使用测试测试案例;
classarticleservicetestextendstestcase
{
公共函数测试工具(
{
}
公共功能测试工具(
{
}
publicfunctiontestcreatearticle (
{
}
}
输出结果(增量添加) )。
名称空间测试(单元;
使用测试测试案例;
classarticleservicetestextendstestcase
{
公共函数测试工具(
{
//假设此测试已经存在
//生成的代码保留此注释
}
公共功能测试工具(
{
}
publicfunctiontestcreatearticle (
{
}
}
程序代码
use IlluminateConsoleCommand;
use PhpParserBuilderMethod;
use PhpParserBuilderUse_;
用户PHP parsernode;
use PhpParserNodeStmtClass_;
usephpparsernodestmtclass method;
use PhpParserNodeSt
mtNamespace_;use PhpParserParserFactory;
use PhpParserPrettyPrinter;
use Exception;
/** 略 */
public function handle()
{
// 解析需测试文件
$namespaces = $this->parseFile(app_path("Http/Services/V1/ArticleService.php"));
array_walk($namespaces, function ($namespace) {
[$namespace, $classes] = $namespace;
// 解析文件中的类
foreach ($classes as $class) {
$className = $class->name->name;
$testClassName = "{$className}Test";
$testFilePath = base_path("tests/Unit/{$testClassName}.php");
$classMethodNames = array_filter(array_map(function ($bodyPart) {
if ($bodyPart instanceof ClassMethod && $bodyPart->isPublic() && $bodyPart->isStatic()) {
$methodName = $bodyPart->name->name;
return 'test' . strtoupper($methodName[0]) . substr($methodName, 1);
}
return null;
}, $class->stmts));
if (file_exists($testFilePath)) {
// 读取已添加的测试
[$testNamespace, $testClassMethodNames, $testClass] = $this->getExistsTest($testFilePath);
$todoMethodNames = array_diff($classMethodNames, $testClassMethodNames);
} else {
// 创建空的测试类
$testNamespace = $this->prepareTestFile('TestUnit');
$todoMethodNames = $classMethodNames;
$testClass = new Class_($testClassName);
$testClass->extends = new NodeName('TestCase');
}
$testNamespace->stmts = array_filter($testNamespace->stmts, function ($stmt) {
return !($stmt instanceof Class_);
});
$testClass->stmts = array_merge($testClass->stmts, array_map(function ($methodName) {
$method = new Method($methodName);
$method->makePublic();
return $method->getNode();
}, $todoMethodNames));
$testNamespace->stmts[] = $testClass;
$prettyPrinter = new PrettyPrinterStandard;
echo $prettyPrinter->prettyPrintFile([$testNamespace]);
}
});
}
/**
* 解析已有的测试
*
* @param $testFilePath
* @return array
* @throws Exception
*/
protected function getExistsTest($testFilePath)
{
$testClasses = $this->parseFile($testFilePath);
if (count($testClasses) !== 1) {
throw new Exception('测试文件需有且仅有一个PHP片段');
}
[$testNamespace, $testClasses] = $testClasses[0];
if (count($testClasses) !== 1) {
throw new Exception('测试文件需有且仅有一个类');
}
$testClass = $testClasses[0];
$testClassMethodNames = array_filter(array_map(function ($bodyPart) {
if ($bodyPart instanceof ClassMethod && $bodyPart->isPublic()) {
return $bodyPart->name->name;
}
return null;
}, $testClass->stmts));
return [$testNamespace, $testClassMethodNames, $testClass];
}
protected function prepareTestFile($namespace)
{
$namespace = new PhpParserBuilderNamespace_($namespace);
$namespace->addStmt(new Use_('TestsTestCase', NodeStmtUse_::TYPE_NORMAL));
return $namespace->getNode();
}
protected function parseFile($path)
{
$code = file_get_contents($path);
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
if (!count($ast)) {
throw new Exception('未发现php代码');
}
$namespaces = $this->parsePHPSegments($ast);
return $namespaces;
}
protected function parsePHPSegments($segments)
{
$segments = array_filter($segments, function ($segment) {
return $segment instanceof Namespace_;
});
$segments = array_map(function (Namespace_ $segment) {
return [$segment, $this->parseNamespace($segment)];
}, $segments);
return $segments;
}
protected function parseNamespace(Namespace_ $namespace)
{
$classes = array_values(array_filter($namespace->stmts, function ($class) {
return $class instanceof Class_;
}));
return $classes;
}
结语
感谢俊秀的睫毛的杰作,我们可以通过PHP Paser便捷地解析和修改PHP代码,具备了元编程能力。在此基础上,我们可以实现静态代码分析(SCA)、模板生成代码等工具。使用这些工具开发者可以排查潜在BUG、优化项目代码,减少重复劳动。
PHP Paser算是作者最喜欢的PHP包,喜欢程度甚至高于Laravel这样的全栈框架。本文举了一个简单的自动创建单元测试的例子,除这以外还可以实现更多更好用的功能;第一次写教程,文笔不是很好,算是抛砖引玉了。
本作品采用《CC 协议》,转载必须注明作者和本文链接
为码农摸鱼事业而奋斗