PHP-Parser 2.0 升级完全指南:从 1.x 迁移到 ParserFactory 时代
【免费下载链接】PHP-ParserA PHP parser written in PHP项目地址: https://gitcode.com/GitHub_Trending/ph/PHP-Parser
导读
本文以 PHP-Parser 官方升级文档 UPGRADE-2.0.md 为主体,系统梳理从 1.x 迁移到 2.0 的全部破坏性变更与应对方案。读完本文,你将掌握:通过ParserFactory创建解析器的标准姿势、PHP 5/7 双版本解析模式的选择逻辑、Parser从类到接口的演进、旧别名清理带来的命名空间影响,以及NodeTraverser、Node\Name、Scalar 节点等 API 的行为变化,从而把存量代码安全、平滑地迁移到 2.0 及后续版本。
适用前提:本文所有结论以当前仓库(PHP-Parser)源码与官方文档为准。2.0 发布于 2015 年底(见 CHANGELOG.md),仓库现已演进到支持 PHP 8.x 的版本,但 2.0 确立的工厂模式与版本化解析思想至今仍是该库的架构基石。
一、PHP 版本要求:运行环境与解析目标从此分离
2.0 的一个关键前提变化是:
- 运行 PHP-Parser 本身需要 PHP 5.4 或更新版本;
- 被解析的源码仍可以是 PHP 5.2 / 5.3 语法,只要运行环境更新即可。
也就是说,"库的运行版本"与"代码的解析版本"是两个独立维度。这个"向前解析、向后兼容"的设计在后续版本中被不断强化:查看仓库当前的 PhpVersion.php 可见,BUILTIN_TYPE_VERSIONS表中内置类型(如array要求 5.1、callable要求 5.4、string要求 7.0、mixed要求 8.0)都按版本号登记,而 getNewestSupported() 目前已支持到 8.4。这正是一脉相承的"按目标版本精确解析"机制的雏形。
二、创建解析器:从直接new到ParserFactory
2.0 之前,解析器实例通过直接实例化获得,但 2.0 中解析器类被改名,旧写法已失效:
// 旧写法(1.x,已失效) use PhpParser\Parser, PhpParser\Lexer; $parser = new Parser(new Lexer\Emulative);2.0 起必须经由ParserFactory:
use PhpParser\ParserFactory; $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);create()的第一个参数决定如何处理不同的 PHP 版本,共四个取值:
| 常量 | 行为 |
|---|---|
ParserFactory::PREFER_PHP7 | 先按 PHP 7 解析;失败再按 PHP 5 重试 |
ParserFactory::PREFER_PHP5 | 先按 PHP 5 解析;失败再按 PHP 7 重试 |
ParserFactory::ONLY_PHP7 | 只按 PHP 7 解析 |
ParserFactory::ONLY_PHP5 | 只按 PHP 5 解析 |
对绝大多数业务场景而言,PREFER_PHP7与PREFER_PHP5的差别主要体现在标量类型提示的 AST 表示上:
- 按 PHP 7 解析时,
string这类标量类型提示存为字符串字面量'string'; - 按 PHP 5 解析时,存为
new Name('string')节点对象。
例如函数function foo(string $s) {},两种模式产生的Param->type分别是一个普通字符串和一个Name节点,下游做 AST 遍历或改写时必须区分这两种形态。
自定义词法分析器(Lexer)
如需自定义 Lexer,将其作为create()的第二个参数传入:
use PhpParser\ParserFactory; $lexer = new MyLexer; $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer);默认词法分析器是Lexer\Emulative,它通过在 compatibility_tokens.php 中定义的兼容 token(如 PHP 8 的T_MATCH、T_NULLSAFE_OBJECT_OPERATOR、T_ATTRIBUTE,PHP 8.1 的T_ENUM、T_READONLY,PHP 8.4 的T_PROPERTY_C等)在旧版 PHP 上"模拟"新语法 token,从而让旧运行环境也能解析新语法代码。2.0 时这一机制只覆盖 PHP 7 特性,如今已扩展至 PHP 8.4。
源码中的工厂模式演进
当前仓库的 ParserFactory.php 已从 2.0 的"四个常量 +create()"演进为"版本对象 + 三种创建方法":
- createForVersion(PhpVersion $version):按目标版本选择
Lexer(宿主版本直接用Lexer,否则用Lexer\Emulative)与解析器实现(版本 ≥ 8.0 用Parser\Php8,否则用Parser\Php7); - createForNewestSupportedVersion():创建支持最新版本(当前为 8.4)的解析器;
- createForHostVersion():创建与运行环境版本一致的解析器,不做任何 token 模拟。
ParserFactoryTest.php 用assertInstanceOf(Php8::class, ...)验证了工厂会返回Parser\Php8实例。这印证了 2.0 引入的"工厂 + 按版本选实现"架构一直延续至今。
三、PhpParser\Parser从类到接口的演进
2.0 中PhpParser\Parser由具体类变为接口,由以下三个类实现:
Parser\Php5:PHP 5 语法解析器;Parser\Php7:PHP 7 语法解析器;Parser\Multiple:按策略在多个解析器间切换的复合解析器。
同时,解析器使用的 token 常量被移入Parser\Tokens。只要使用上文介绍的ParserFactory创建实例,这些内部变动对你完全透明。
当前仓库中该接口定义于 Parser.php,核心方法为:
parse(string $code, ?ErrorHandler $errorHandler = null): ?array——解析源码为语句节点数组;默认错误处理器为ErrorHandler\Throwing(抛异常),若传入非抛异常的错误处理器且解析失败无法恢复,则返回null;getTokens(): array——返回上次解析产生的 token 列表。
注意当前仓库已不再包含Parser\Php5与Parser\Multiple,实现类仅剩 Php7.php 与 Php8.php,"多版本多实现"的思路则由PhpVersion机制继承并发扬光大。
四、遗留别名(legacy aliases)的移除
2.0 清除了所有类名别名,包括:
- 旧的非命名空间
PHPParser_前缀类(如PHPParser_Parser、PHPParser_Lexer等 1.x 时代不带命名空间的类); - 为 PHP 7 支持而改名的类。
迁移到 2.0 时,请全局搜索代码中的PHPParser_前缀并替换为对应的PhpParser\命名空间类名,同时同步调整相关use语句。这部分没有运行时兼容层,属于硬性破坏性变更。
五、弃用:Node\Name的set系列方法
Node\Name类的以下四个方法在 2.0 中被弃用:
set()setFirst()append()prepend()
官方推荐的替代方案是静态方法Name::concat()与实例方法Name->slice()。
从当前源码 Name.php 可以确认这两个 API 的设计细节:
- slice(int $offset, ?int $length = null):语义与
array_slice()一致,偏移与长度均可为负数;返回与调用者同类型的新Name实例并保留属性;空切片返回null,且null会在concat()中被正确处理;偏移越界时抛出\OutOfBoundsException。对常见情形(offset === 1 && $length === null)还做了短路优化,直接截取首个命名空间分隔符之后的部分。 - concat($name1, $name2, array $attributes = []):拼接两个名字并返回新实例,生成实例的类型取决于调用类(如
Name\FullyQualified::concat()得到全限定名)。任一参数为null时返回另一方的副本,两者皆为null时返回null——因此Name::concat($namespace, $shortName)这种"命名空间可能为空"的写法可以放心使用。
示例:把Foo\Bar\Baz去掉第一个段并追加Quux:
use PhpParser\Node\Name; $name = new Name('Foo\Bar\Baz'); $sliced = $name->slice(1); // Bar\Baz $result = Name::concat($sliced, 'Quux'); // Bar\Baz\Quux六、杂项变更清单
2.0 还有一批不集中归类但同样影响迁移的细节变更,逐条说明如下:
1.NodeTraverser默认不再克隆节点
1.x 中遍历器默认克隆所有节点;2.0 起默认不克隆。若需恢复旧行为,向构造函数传入true:
use PhpParser\NodeTraverser; $traverser = new NodeTraverser(true); // 恢复克隆行为当前仓库的 NodeTraverser.php 构造函数签名已变为__construct(NodeVisitor ...$visitors),通过可变参数接收访问器,不再支持布尔克隆开关——可见该开关在后继版本中已被彻底移除,依赖默认克隆的代码应尽早改为显式使用 CloningVisitor 等方案。
2. 移除遗留节点格式,自定义节点需实现getSubNodeNames()
2.0 删除了旧的节点格式。若你定义了自定义节点,必须实现getSubNodeNames()方法,返回子节点名字数组,供遍历器识别其结构。仓库内置节点即以此为标准,例如 Name::getSubNodeNames() 返回['name']。
3.Scalar节点构造函数默认值被移除
Scalar系列节点的构造函数不再提供默认值。旧写法new LNumber()必须改为显式传值:
use PhpParser\Node\Scalar\LNumber; $node = new LNumber(0); // 而非 new LNumber()同理,其余标量节点(如String_、DNumber等)构造时也需显式提供值。
4. 双引号字符串(encapsed string)的parts表示变更
双引号内插字符串中,非变量片段此前以原始字符串表示,2.0 起改为使用Scalar\EncapsStringPart节点。这影响两个节点的parts子节点:
Scalar\Encaps(已演进为 InterpolatedString.php);Expr\ShellExec(shell 执行字符串)。
即类似"hello $name"的 AST 中,"hello "不再是一个普通字符串,而是一个EncapsStringPart节点。遍历或重建此类 AST 时,必须把parts里的每一项都当作节点对象处理,而不是混用字符串与节点。该变更在 CHANGELOG.md 中有对应记录,印证了它是 2.0.0 正式版的既定行为。当前仓库中 EncapsedStringPart.php 已作为兼容类存在,实际实现委托给 InterpolatedStringPart.php。
七、迁移清单速查
将 1.x 代码迁移到 2.0,按以下顺序检查即可:
- 运行环境:确认 PHP ≥ 5.4,方可运行 PHP-Parser 2.0;
- 实例化方式:将
new Parser(...)全部替换为(new ParserFactory)->create(...),并按需选择PREFER_PHP7/PREFER_PHP5/ONLY_PHP7/ONLY_PHP5四个模式之一;自定义 Lexer 作为第二参数传入; - 命名空间:删除所有
PHPParser_遗留别名引用,改用PhpParser\命名空间类; Node\Name:把set()、setFirst()、append()、prepend()改写为concat()与slice(),注意slice()空切片返回null的语义;NodeTraverser:确认是否需要new NodeTraverser(true)恢复克隆行为;- 自定义节点:补上
getSubNodeNames()方法; - 标量节点:
new LNumber()等写法补上显式初始值; - encapsed 字符串:遍历
Scalar\Encaps/Expr\ShellExec的parts时,按节点对象而非原始字符串处理EncapsStringPart。
按此清单逐项处理,即可完成从 1.x 到 2.0 的平滑迁移,并为后续 3.0、4.0、5.0 的升级(可分别参考 UPGRADE-3.0.md、UPGRADE-4.0.md、UPGRADE-5.0.md)打下基础——2.0 引入的ParserFactory与"运行/解析版本分离"思想,正是理解该库全部后续演进的关键起点。
【免费下载链接】PHP-ParserA PHP parser written in PHP项目地址: https://gitcode.com/GitHub_Trending/ph/PHP-Parser
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考