ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP-Parser 查找特定节点时如何减少全量遍历开销?DONT_TRAVERSE_CHILDREN 与 STOP_TRAVERSAL

PHP-Parser 查找特定节点时如何减少全量遍历开销?DONT_TRAVERSE_CHILDREN 与 STOP_TRAVERSAL PHP-Parser 查找特定节点时如何减少全量遍历开销DONT_TRAVERSE_CHILDREN 与 STOP_TRAVERSAL【免费下载链接】PHP-ParserA PHP parser written in PHP项目地址: https://gitcode.com/GitHub_Trending/ph/PHP-Parser用 nikic/php-parser 分析大型 PHP 项目时常见瓶颈不是parse()本身而是之后对 AST 的全量遍历一个文件的 AST 可能包含数千个节点NodeTraverser默认会对每个节点依次调用所有 visitor 的enterNode()和leaveNode()节点数乘以 visitor 数就是方法调用总量。本文的任务是当你只需要找到所有类声明或找到某个特定节点时利用NodeVisitor::DONT_TRAVERSE_CHILDREN和NodeVisitor::STOP_TRAVERSAL两个特殊返回值提前截断遍历减少无意义的子节点访问。适用前提来自 composer.jsonPHP 7.4以及ext-tokenizer、ext-json、ext-ctype扩展。核心行为说明见 Walking the AST 的 Short-circuiting traversal 一节和 NodeVisitor 接口 的常量注释。准备拿到可遍历的节点数组按 Usage of basic components 的流程先引导 autoloader、解析出$stmtsuse PhpParser\ParserFactory; require path/to/vendor/autoload.php; // 替换为你项目中 composer 生成的 autoload.php 实际路径 $code file_get_contents($fileName); $parser (new ParserFactory())-createForHostVersion(); $stmts $parser-parse($code);两个与性能相关的注意点来自 PerformanceParser等对象设计为可复用多次解析文件时应复用同一个实例不要每个文件新建一遍parser 和 pretty printer 的初始化尤其昂贵加载 Xdebug 会让本库的代码大约慢 5 倍而且xdebug.default_enable0并不能禁用它唯一办法是不加载该扩展。如果你要做遍历耗时对比务必在没有 Xdebug 的环境下测量。方案一DONT_TRAVERSE_CHILDREN —— 找到匹配节点后不再进入它的子树语义见 lib/PhpParser/NodeVisitor.php 的常量注释enterNode()返回NodeVisitor::DONT_TRAVERSE_CHILDREN后当前节点的子节点不会为任何visitor 被遍历但对当前节点本身后续 visitor 的enterNode()和leaveNode()仍会被调用。它只能在enterNode中返回因为到leaveNode时子节点已经访问完了见 Walking the AST 文档。文档给出的典型场景查找文件中的所有类声明。因为 PHP 不允许嵌套类一旦进入一个类节点继续遍历它的子节点对找类这个目标没有任何收益。visitor 写法如下原样来自 Walking_the_AST.markdownuse PhpParser\{Node, NodeTraverser, NodeVisitorAbstract, NodeVisitor}; private $classes []; public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Class_) { $this-classes[] $node; return NodeVisitor::DONT_TRAVERSE_CHILDREN; } }注意适用边界文档说明这个优化assuming youre not interested in anonymous classes——如果你的目标包含匿名类进入Class_节点的子树就不是冗余的不能返回这个值。如果还需要阻止后续 visitor 访问当前节点本身接口提供了更强的DONT_TRAVERSE_CURRENT_AND_CHILDREN同样是enterNode返回值见 Usage of basic components 的 Node traversal 一节后续 visitor 连当前节点的enterNode()都不会被调用但已调用过enterNode()的 visitor 仍会收到该节点的leaveNode()。方案二STOP_TRAVERSAL —— 只找一个节点找到就终止整次遍历如果你只需要第一个匹配项例如找到名为Foo\Bar\Baz的类可以直接中止整个遍历use PhpParser\{Node, NodeVisitor, NodeVisitorAbstract}; private $class null; public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Class_ $node-namespacedName-toString() Foo\Bar\Baz ) { $this-class $node; return NodeVisitor::STOP_TRAVERSAL; } }与DONT_TRAVERSE_CHILDREN的关键差别STOP_TRAVERSAL在enterNode和leaveNode中都可返回返回后不再访问任何节点但afterTraverse()仍会被调用常量注释中明确说明已排队的数组级修改也会被执行遍历结束后 AST 本身保持不变traverse()的返回值与输入相同下面的测试验证会体现这一点。注意这个例子依赖$node-namespacedName子节点它是由随包提供的PhpParser\NodeVisitor\NameResolvervisitor 添加到类/函数/常量声明上的见 Usage of basic components 的 The NameResolver node visitor 一节。如果你的遍历链里没有new NameResolvernamespacedName不存在需要先加上它$traverser new NodeTraverser; $traverser-addVisitor(new NameResolver); $traverser-addVisitor(new MyLookupVisitor);文档同时提醒该场景还有更简单的写法——NodeFinder内部就是一个在找到节点后返回STOP_TRAVERSAL的 visitorFirstFindingVisitoruse PhpParser\{Node, NodeFinder}; $nodeFinder new NodeFinder; // 找第一个类节点不存在时返回 null $class $nodeFinder-findFirstInstanceOf($stmts, Node\Stmt\Class_::class); // 找第一个满足条件的节点 $class $nodeFinder-findFirst($stmts, function(Node $node) use ($name) { return $node instanceof Node\Stmt\Class_ $node-resolvedName-toString() $name; });findFirst*()方法在匹配后立即终止遍历find*()系列则返回全部匹配项。对找单个节点这类任务优先用NodeFinder自己写 visitor 只在需要更复杂的中止逻辑时用。多个 visitor 共存时的交互规则如果一次traverse()挂了多个 visitor短路返回值的作用范围比直觉更广Walking the AST 文档的 Multiple visitors 一节任意一个 visitor 返回DONT_TRAVERSE_CHILDREN子节点会被所有visitor 跳过任意一个 visitor 返回DONT_TRAVERSE_CURRENT_AND_CHILDREN当前节点对其后的 visitor 也直接跳过任意一个 visitor 返回STOP_TRAVERSAL所有visitor 的遍历都会终止。写查找类visitor 时要意识到它的短路决定会影响同一次遍历中的其他 visitor如果其他 visitor 需要看这些子节点应把查找逻辑放进单独的NodeTraverser实例或接受被跳过的副作用。验证方式用项目自带测试核对遍历轨迹仓库中的 test/PhpParser/NodeTraverserTest.php 用精确的调用轨迹断言了这两个常量的行为可以直接运行作为行为核对composer install vendor/bin/phpunit test/PhpParser/NodeTraverserTest.php测试断言的轨迹可以说明省掉了什么以下均为文档/测试中的示例轨迹不是固定输出testDontTraverseChildren()对print str; -($foo * $foo);这组节点在printNode上返回DONT_TRAVERSE_CHILDREN后轨迹为beforeTraverse→enterNode(printNode)→leaveNode(printNode)→enterNode(negNode)→enterNode(mulNode)→leaveNode(mulNode)→leaveNode(negNode)→afterTraverse——子节点String_(str)的enterNode从未出现。testStopTraversal()对[a * b, print c]这组节点在mulNode的enterNode返回STOP_TRAVERSAL后轨迹为beforeTraverse→enterNode(mulNode)→afterTraverse且traverse()的返回值与输入 AST 完全相等assertEquals($stmts, $traverser-traverse($stmts))——遍历被中止但没有修改树。你自己的 visitor 可以用同样的思路验证给 visitor 的enterNode/leaveNode记录被访问的节点类型序列返回短路值前后对比序列长度与内容即可确认哪些子树被跳过、afterTraverse是否仍被调用。边界与限制DONT_TRAVERSE_CHILDREN不能用于leaveNode这是文档明确的唯一使用位置找所有类的短路依赖 PHP 不嵌套类这一语言事实对匿名类目标不成立STOP_TRAVERSAL按文档语义只保证找到第一个匹配项若目标可能多处出现例如条件性地定义同名类两次这类exotic cases文档原文提示不要用它短路只省遍历开销不省解析开销。解析本身是计算密集步骤生产环境还应按 Performance 文档设置zend.assertions-1运行时设置为 0并避免加载 Xdebug。相关文档Walking the AST、Usage of basic components、Performance、NodeFinder 实现。【免费下载链接】PHP-ParserA PHP parser written in PHP项目地址: https://gitcode.com/GitHub_Trending/ph/PHP-Parser创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表