- 测试
【免费下载链接】php-token-stream
Wrapper around PHP's tokenizer extension.
本指南以 ChangeLog.md 为主线,逐版本还原 php-token-stream 从 2.0.0 到 4.0.4 的演进脉络,并结合 src/ 目录下的核心源码与 tests/ 中的回归测试,讲清楚每一个"Added / Fixed / Removed"背后的架构设计与实现原理。读完本文,你将理解该库如何把 PHP 原生token_get_all()的输出封装成面向对象的 token 流,掌握作用域追踪、匿名类/匿名函数识别、代码行统计等关键机制,并能独立解读 PHP 版本升级对静态分析类工具的影响。
一、项目定位:ChangeLog 记录的到底是什么
php-token-stream 是围绕 PHP 扩展ext-tokenizer的封装库(composer.json 中description为 "Wrapper around PHP's tokenizer extension")。它的核心价值在于:把token_get_all()返回的"数组或字符串混杂"的原始 token 序列,统一转换成一组面向对象的 token 类实例,并提供类、方法、接口、trait、include 依赖等高层结构信息的提取能力。
因此,ChangeLog.md 中每一次 "Added"(新增 token 支持)都对应 PHP 语言层面的新语法,每一次 "Fixed" 都对应 token 序列解析中的边界情况,每一次 "Removed" 都对应运行时环境的收缩。理解了这条对应关系,就能从版本历史中读出语言演进与解析器维护的双重脉络。
二、版本时间线总览
ChangeLog 共记录了 13 个版本,跨度从 2017-08 到 2020-08。下表是完整梳理:
| 版本 | 日期 | 变更类型 | 核心内容 |
|---|---|---|---|
| 4.0.4 | 2020-08-04 | Added | 支持NAME_FULLY_QUALIFIED、NAME_QUALIFIED、NAME_RELATIVEtoken |
| 4.0.3 | 2020-06-27 | Added | 支持 PHP 8 |
| 4.0.2 | 2020-06-16 | Fixed | 修复 4.0.1 引入的向后兼容性破坏 |
| 4.0.1 | 2020-05-06 | Fixed | issue #93:含匿名类方法的类处理不正确 |
| 4.0.0 | 2020-02-07 | Removed | 不再支持 PHP 7.1 与 PHP 7.2 |
| 3.1.1 | 2019-09-17 | Fixed | issue #84:名为class的方法处理不正确 |
| 3.1.0 | 2019-07-25 | Added | 支持 PHP 7.4 引入的FN与COALESCE_EQUALtoken |
| 3.0.2 | 2019-07-08 | Changed | issue #82:确保组件在 php-scoper 前缀化类名后仍可工作 |
| 3.0.1 | 2018-10-30 | Fixed | issue #78:getEndTokenId()未处理字符串美元符号插值("${var}") |
| 3.0.0 | 2018-02-01 | Removed | issue #71:移除 Hack 语言特有代码;issue #72:放弃 PHP 7.0 |
| 2.0.2 | 2017-11-27 | Fixed | issue #69:PHP_Token_USE_FUNCTION序列化不正确 |
| 2.0.1 | 2017-08-20 | Fixed | issue #68:名为empty的方法被误判为匿名函数 |
| 2.0.0 | 2017-08-03 | — | 首个 2.x 版本(无具体条目说明) |
三、4.x 系列:迈入 PHP 8 时代
3.1 4.0.4:新增 PHP 8 的 NAME_* token
PHP 8 的 tokenizer 扩展新增了三个名称类 token:T_NAME_FULLY_QUALIFIED、T_NAME_QUALIFIED、T_NAME_RELATIVE,分别对应\Foo\Bar、Foo\Bar、namespace\Foo\Bar三种名称写法。4.0.4 为此新增了三个 token 类:
- src/NameFullyQualified.php:
PHP_Token_NAME_FULLY_QUALIFIED - src/NameQualified.php:
PHP_Token_NAME_QUALIFIED - src/NameRelative.php:
PHP_Token_NAME_RELATIVE
这三个类目前都是空壳子类(仅继承PHP_Token),但它们的存在至关重要:在 src/Stream.php 的scan()方法中,token 类名是通过substr(\token_name($token[0]), 2)剥离T_前缀后拼接PHP_Token_得到的,因此只要 PHP 8 的 tokenizer 吐出了新 token,PHP_Token_命名空间下就必须有对应的类可供实例化,否则解析会直接失败。这正是 4.0.4 作为"Added"条目被记录的根本原因。
3.2 4.0.3:正式支持 PHP 8
PHP 8 对 tokenizer 的破坏性改动不止 NAME_* token。4.0.3 声明组件支持 PHP 8,其配套工作可从 composer.json 得到印证:"php": "^7.3 || ^8.0"的约束同时覆盖了 7.3+ 与 8.0。此外,src/Stream.php 中有一段专门注释@see https://github.com/sebastianbergmann/php-token-stream/issues/95的 PHP 8 兼容逻辑:当PHP_MAJOR_VERSION >= 8、当前 token 是T_WHITESPACE、前一个 token 是T_COMMENT(且注释以#或//开头,不含/*)、且该空白以换行开头时,会把换行合并进注释 token,避免 PHP 8 下//、#注释与换行的 token 切分方式变化导致的行号错乱。对应回归测试见 tests/Token/CommentTest.php 与 fixture tests/_fixture/issue_95.php。
3.3 4.0.1:修复匿名类方法处理(issue #93)
4.0.1 修复了"类中的方法声明了匿名类时处理不正确"的问题。根因在于PHP_Token_Stream::parse()依赖"遇到}就弹出类栈"的朴素逻辑,而匿名类有自己的{...}作用域,会干扰外层类的结束判断。
修复思路从源码看是双重的:
- 匿名类命名:src/Class.php 的
getName()在类名 token 之后紧跟{、extends或implements时,判定为匿名类,并生成AnonymousClass:{行号}#{tokenId}形式的稳定标识。 - 作用域栈追踪:src/Stream.php 的
parse()用$classEndLine/$class两个栈记录嵌套类,只有当前}的行号与栈顶类的endLine一致时才弹栈,从而保证外层类不会被内层匿名类的}提前关闭。
回归测试 tests/Token/ClassTest.php 对 tests/_fixture/issue93.php 断言:类PhpAnonym的endLine为 34(而不是被内部匿名类截断),且其三个方法__construct、funcOne、funcTwo的行号范围都正确;其中funcTwo(16–33 行)内恰好嵌有一个匿名类(18–30 行),是最典型的压测用例。类似的匿名类场景还覆盖了 tests/Token/ClassTest.php 中的多个 fixture(如class_with_method_that_declares_anonymous_class.php、class_with_multiple_anonymous_classes_and_functions.php)。
3.4 4.0.0:移除 PHP 7.1 / 7.2 支持
4.0.0 将最低 PHP 版本提升到 7.3。这与 composer.json 的"require": { "php": "^7.3 || ^8.0" }完全一致。对使用者而言,这意味着 PHP 7.1/7.2 项目若要继续使用该库,必须锁定在 3.x 系列。
四、3.x 系列:PHP 7.4 新语法与稳定性加固
4.1 3.1.0:新增FN与COALESCE_EQUALtoken
PHP 7.4 引入了箭头函数fn与空合并赋值运算符??=,tokenizer 相应新增T_FN、T_COALESCE_EQUAL。3.1.0 为其建立了 src/Fn.php(PHP_Token_FN)与 src/CoalesceEqual.php(PHP_Token_COALESCE_EQUAL)两个 token 类。借助scan()中"T_前缀剥离 + 类名映射"的机制,PHP 7.4 环境下这两类 token 即可自动获得面向对象封装。
这里值得注意一个细节:箭头函数本质仍是函数。在PHP_Token_FUNCTION的逻辑中,匿名函数会被命名为anonymousFunction:{行号}#{tokenId}(见 src/Function.php),而getCCN()(圈复杂度)对if / elseif / for / foreach / while / case / catch / 逻辑与或 / 三元等 token 逐一计数(src/Function.php),这套统计机制同样适用于箭头函数体内的判断结构。
4.2 3.1.1:修复名为class的方法(issue #84)
public function class()这类以保留字命名的方法在 PHP 7 中合法,但 tokenizer 会为class一词输出T_CLASS而非T_STRING,导致解析器在识别"这是方法名"还是"这是::class常量"时出现歧义。src/Stream.php 中有一段专门处理:当lastNonWhitespaceTokenWasDoubleColon为真且当前 token 名为CLASS时,改写为CLASS_NAME_CONSTANT(对应 src/ClassNameConstant.php 的PHP_Token_CLASS_NAME_CONSTANT);而在方法声明场景下class前没有::,则保持T_CLASS语义,从而正确区分Foo::class与function class()。
4.3 3.0.2:兼容 php-scoper 类名前缀化(issue #82)
php-scoper 会在打包时给所有类名加上前缀(如PHP_Token_→ScopedPHP_Token_),传统上依赖硬编码类名的代码会因此失效。3.0.2 的"Changed"正是为了让组件在类名前缀化后仍能正常工作。从源码结构看,src/Stream.php 通过protected static $customTokens数组把(、)、{、}、;、.等 28 个单字符映射到对应 token 类,并通过PHP_Token_Util::getClass()等工具方法动态获取类名,这类间接引用设计为前缀化场景留出了兼容空间。
4.4 3.0.1:修复"${var}"字符串插值(issue #78)
getEndTokenId()是PHP_TokenWithScope的核心方法(src/TokenWithScope.php),它通过花括号配对来定位类/函数/命名空间作用域的结束 token。但"${var}"这种字符串插值在 tokenizer 中会生成T_DOLLAR_OPEN_CURLY_BRACES(${)与T_CURLY_OPEN({$)等特殊 token,若只统计普通{/}就会破坏配对。修复后的getEndTokenId()把PHP_Token_OPEN_CURLY、PHP_Token_DOLLAR_OPEN_CURLY_BRACES、PHP_Token_CURLY_OPEN三类 token 都计入$block++,从而正确处理插值字符串。fixture tests/_fixture/source.php 中的buzz()方法体内恰好包含echo "${foo}";,对应的行号断言见 tests/Token/FunctionTest.php。
4.5 3.0.0:移除 Hack 语言代码与 PHP 7.0 支持
3.0.0 做了两件事:
- 移除 Hack 语言特有代码(issue #71):早期版本为兼容 HHVM 的 Hack 语法保留了专用逻辑,进入 3.0 后彻底清除。
- 放弃 PHP 7.0(issue #72):最低版本提升到 7.1(后续 4.0.0 又提升到 7.3)。
这两项"Removed"表明组件完全聚焦 PHP 官方运行时,不再为第三方方言付出维护成本。
五、2.x 系列:早期边界修复
5.1 2.0.2:修复PHP_Token_USE_FUNCTION序列化(issue #69)
use function Foo\bar;这类导入语句会被 src/Stream.php 的scan()特殊识别:当检测到USEtoken 且其后第二个 token 是T_FUNCTION时,合并生成PHP_Token_USE_FUNCTION(src/UseFunction.php)。2.0.2 修复了该 token 在序列化场景下的问题。测试 tests/Token/ClassTest.php 验证了classUsesNamespacedFunction.php这类 fixture:包含use function导入的文件不应产生顶层函数条目,类数量应为 1。
5.2 2.0.1:修复名为empty的方法(issue #68)
empty是 PHP 语言结构,tokenizer 会输出T_EMPTY(对应 src/Empty.php 的PHP_Token_EMPTY)。当类中存在public function empty()时,旧版解析器会把该方法误判为匿名函数。修复后,tests/Token/ClassTest.php 断言 tests/_fixture/class_with_method_named_empty.php 中的empty被正确收录到类的方法表中。
六、从版本历史反观核心架构
ChangeLog 中大量条目(issue #68、#78、#84、#93)都集中在"作用域识别"与"名称识别"两类问题上,这正对应着该库最核心的三大机制。
6.1 三级 token 类层次
src/Token.php 定义了抽象基类PHP_Token,持有text(原始文本)、line(起始行号)、tokenStream(所属流)、id(流内序号)四个属性,并暴露__toString()、getLine()、getId()。
其上两级抽象类体现了"按能力分层"的设计:
PHP_TokenWithScope(src/TokenWithScope.php):为具有代码块的 token(类、函数、命名空间等)提供getDocblock()(向上回溯查找紧邻的文档注释)、getEndTokenId()(花括号配对定位结束)、getEndLine()。PHP_TokenWithScopeAndVisibility(src/TokenWithScopeAndVisibility.php):在其上追加getVisibility()(向前最多 7 个 token 内查找public/protected/private,见 src/TokenWithScopeAndVisibility.php)与getKeywords()(收集static/final/abstract修饰符)。
例如PHP_Token_CLASS(src/Class.php)继承自PHP_Token_INTERFACE(src/Interface.php),后者继承自PHP_TokenWithScopeAndVisibility,从而获得getParent()、getInterfaces()、getPackage()(从 docblock 解析@category/@package/@subpackage并回溯命名空间)等类级能力。
6.2 Stream:一次扫描,多维度索引
src/Stream.php 的PHP_Token_Stream是整个库的枢纽,实现ArrayAccess、Countable、SeekableIterator接口,并支持用文件路径或源码字符串直接构造:
$stream = new PHP_Token_Stream('/path/to/source.php'); // 或 $stream = new PHP_Token_Stream($sourceCodeString);构造时scan()(src/Stream.php)调用token_get_all()完成一次词法扫描,随后建立四类索引:
tokens:顺序 token 数组,也是迭代器与数组访问的数据源;tokensByLine:行号 → token 列表的倒排索引,供注释行统计与getDocblock()使用;linesOfCode:loc(总行数)、cloc(纯注释行数)、ncloc(非注释代码行数)三要素统计,其中cloc只在整行仅含注释/空白时计入(src/Stream.php);lineToFunctionMap:由parse()通过addFunctionToMap()建立"行号 → 函数/方法名"映射,支撑getFunctionForLine($line)查询(src/Stream.php)。
parse()(src/Stream.php)是延迟执行的第二遍扫描,负责组装高层结构,通过getClasses()、getFunctions()、getInterfaces()、getTraits()暴露,返回的每个类/函数条目都包含docblock、keywords、visibility、startLine、endLine、file等元数据;对方法还会额外计算signature(src/Function.php 截取从函数名到{或;的完整签名文本)与ccn圈复杂度。
6.3 include 依赖提取
getIncludes()(src/Stream.php)专门扫描require_once、require、include_once、include四类 token(src/Includes.php 的process()会提取引号内的文件名并记录类型),支持$categorize参数按类型分组输出。测试 tests/Token/IncludeTest.php 对source3.php验证了三种调用形态:合并列表、按类型分组、按单类型过滤。这一能力在依赖分析工具中非常实用。
6.4 性能设计:CachingFactory
src/CachingFactory.php 提供静态缓存工厂PHP_Token_Stream_CachingFactory:get($filename)对同一文件只解析一次,重复调用直接返回缓存实例;clear($filename)支持清空单个文件或全部缓存。在需要反复分析同一批文件的场景(如覆盖率计算)下,可显著减少重复的token_get_all()开销。
七、安装与验证
README 给出了标准 Composer 安装方式(README.md):
# 作为项目依赖 composer require phpunit/php-token-stream # 仅作为开发期依赖(如运行测试套件) composer require --dev phpunit/php-token-stream运行环境约束以 composer.json 为准:PHP^7.3 || ^8.0,且必须启用ext-tokenizer。仓库自带 PHPUnit 测试套件(phpunit.xml 配置了tests目录与 tests/bootstrap.php 引导),可执行vendor/bin/phpunit运行全部测试,其中 tests/Token/ 下的 ClassTest、ClosureTest、CommentTest、FunctionTest、IncludeTest、InterfaceTest、NamespaceTest 恰好覆盖了 ChangeLog 中多个历史 issue 的回归场景。
八、总结:从 ChangeLog 读出维护策略
纵观 13 个版本,可以提炼出 php-token-stream 的三条维护主线:
- 跟随语言演进:每个 PHP 新版本(7.4 的
fn/??=、8.0 的 NAME_* token 与注释切分变化)都会触发对应的 token 类新增与兼容修复,这是"Added"条目的主要来源; - 死磕边界情况:方法名撞上保留字(
empty、class)、匿名类/匿名函数嵌套、"${var}"插值等词法歧义,是"Fixed"条目的高频主题,每次修复都配套了 fixture 与回归测试; - 果断收缩环境:逐步移除 PHP 7.0、7.1、7.2 与 Hack 支持,把维护成本集中到主流运行时。
对使用者而言,这份 ChangeLog 既是一份"升级前必读的兼容性清单"——例如从 3.x 升到 4.x 前必须确认 PHP ≥ 7.3——也是一份理解 tokenizer 封装库内部机制的绝佳教材:每一条看似简单的版本记录背后,都对应着 src/Stream.php 中一段具体的扫描/解析逻辑与一组可运行的测试用例。
- 测试
【免费下载链接】php-token-stream
Wrapper around PHP's tokenizer extension.
相关推荐
Leaf 框架演进全解:从 CHANGELOG 到源码的版本脉络与技术要点
Leaf 框架演进全解:从 CHANGELOG 到源码的版本脉络与技术要点 本文以 Leaf(Rust 编写的开源机器学习框架)仓库中的 CHANGELOG.m
机器学习深度学习LOOT 版本历史深度解析:从 v0.5 到 v0.29 的功能演进、架构重构与修复脉络
LOOT 版本历史深度解析:从 v0.5 到 v0.29 的功能演进、架构重构与修复脉络 LOOT(Load Order Optimisation Tool)是
游戏开发开发工具Expo Web Browser 演进全解析:从 CHANGELOG 到源码的 57 版功能脉络
Expo Web Browser 演进全解析:从 CHANGELOG 到源码的 57 版功能脉络 Expo Web Browser( expo web brow
移动开发前端跨平台原生移动
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考