PhpToken 类
概述
PhpToken 是 PHP 8.0 引入的面向对象的 Token 接口,作为传统 token_get_all() 函数的现代化替代。PhpToken 提供了更直观的 API,支持通过 tokenize() 静态方法获取 token 对象数组,每个 token 对象提供了 id、text、line、pos 等属性,以及 is()、getTokenName()、tokenId() 等方法。
PHP 版本
PhpToken 从 PHP 8.0 开始可用。建议在新代码中优先使用 PhpToken 替代 token_get_all()。
基础概念
PhpToken::tokenize()
静态方法,接受 PHP 源代码字符串,返回 PhpToken 对象数组。
token_name() / token_id()
PhpToken::getTokenName() 返回 token 名称,PhpToken::tokenId() 返回 token ID。
is() 方法
is(int ...$ids) 检查当前 token 是否为指定的类型。
语法与代码
基本 PhpToken::tokenize 用法
php
<?php
declare(strict_types=1);
$source = '<?php function hello(): void { echo "Hello"; }';
$tokens = PhpToken::tokenize($source);
foreach ($tokens as $token) {
$name = $token->getTokenName();
$text = $token->text;
$line = $token->line;
echo "[Line {$line}] {$name}: " . trim($text) . "\n";
}
// [Line 1] T_OPEN_TAG: <?php
// [Line 1] T_WHITESPACE:
// [Line 1] T_FUNCTION: function
// [Line 1] T_WHITESPACE:
// [Line 1] T_STRING: hello
// ...PhpToken 属性访问
php
<?php
declare(strict_types=1);
$tokens = PhpToken::tokenize('<?php $name = "Alice";');
foreach ($tokens as $token) {
echo "ID: {$token->id}\n";
echo "Name: {$token->getTokenName()}\n";
echo "Text: '{$token->text}'\n";
echo "Line: {$token->line}\n";
echo "Pos: {$token->pos}\n";
echo "---\n";
}使用 is() 方法过滤
php
<?php
declare(strict_types=1);
$source = '<?php
class UserController {
public function index() {}
public function show($id) {}
}
';
$tokens = PhpToken::tokenize($source);
foreach ($tokens as $token) {
if ($token->is(T_FUNCTION)) {
echo "发现函数定义 (line {$token->line})\n";
}
if ($token->is(T_STRING)) {
echo "标识符: {$token->text} (line {$token->line})\n";
}
if ($token->is(T_VARIABLE)) {
echo "变量: {$token->text}\n";
}
}获取下一个/上一个 token
php
<?php
declare(strict_types=1);
$tokens = PhpToken::tokenize('<?php echo $name;');
foreach ($tokens as $i => $token) {
if ($token->is(T_ECHO)) {
$next = $tokens[$i + 1] ?? null;
if ($next !== null) {
echo "echo 后面: " . trim($next->text) . "\n";
}
}
}与 token_get_all 对比
php
<?php
declare(strict_types=1);
$source = '<?php $x = 42;';
// 旧方式
$oldTokens = token_get_all($source);
foreach ($oldTokens as $token) {
if (is_array($token)) {
echo token_name($token[0]) . ": {$token[1]}\n";
} else {
echo "Char: {$token}\n";
}
}
// 新方式(PHP 8.0+)
$newTokens = PhpToken::tokenize($source);
foreach ($newTokens as $token) {
echo $token->getTokenName() . ": {$token->text}\n";
}详细说明
PhpToken 属性
| 属性 | 类型 | 说明 |
|---|---|---|
id | int | Token ID 常量 |
text | string | 原始文本 |
line | int | 行号(1-based) |
pos | int | 字节偏移量(PHP 8.0+) |
PhpToken 方法
| 方法 | 说明 |
|---|---|
getTokenName() | Token 名称字符串 |
is(int ...$ids) | 是否匹配指定 ID |
isIgnorable() | 是否可忽略(空白/注释) |
实战示例
实战:提取类和方法
php
<?php
declare(strict_types=1);
function extractClassesAndMethods(string $source): array
{
$tokens = PhpToken::tokenize($source);
$result = [];
$currentClass = null;
foreach ($tokens as $i => $token) {
if ($token->is(T_CLASS)) {
for ($j = $i + 1; $j < count($tokens); $j++) {
if ($tokens[$j]->is(T_STRING)) {
$currentClass = $tokens[$j]->text;
$result[$currentClass] = [];
break;
}
}
}
if ($token->is(T_FUNCTION) && $currentClass !== null) {
for ($j = $i + 1; $j < count($tokens); $j++) {
if ($tokens[$j]->is(T_STRING)) {
$result[$currentClass][] = $tokens[$j]->text;
break;
}
}
}
}
return $result;
}注意事项
使用 PhpToken 检测代码复杂度
php
<?php
declare(strict_types=1);
class ComplexityAnalyzer
{
public function analyze(string $source): array
{
$tokens = PhpToken::tokenize($source);
$metrics = [
'total_tokens' => count($tokens),
'functions' => 0,
'classes' => 0,
'control_structures' => 0,
'variables' => 0,
];
foreach ($tokens as $token) {
if ($token->is(T_FUNCTION)) $metrics['functions']++;
if ($token->is(T_CLASS)) $metrics['classes']++;
if ($token->is(T_IF, T_ELSEIF, T_FOR, T_FOREACH, T_WHILE, T_SWITCH, T_CASE)) {
$metrics['control_structures']++;
}
if ($token->is(T_VARIABLE)) $metrics['variables']++;
}
return $metrics;
}
}
$analyzer = new ComplexityAnalyzer();
$metrics = $analyzer->analyze(file_get_contents(__FILE__));
print_r($metrics);使用 isIgnorable 过滤
php
<?php
declare(strict_types=1);
$tokens = PhpToken::tokenize('<?php
// 这是一个注释
class Foo {
/** 文档注释 */
public function bar(): void {}
}
');
$nonIgnorable = array_filter($tokens, fn(PhpToken $t): bool => !$t->isIgnorable());
echo "总 token 数: " . count($tokens) . "\n";
echo "非忽略 token 数: " . count($nonIgnorable) . "\n";使用 pos 属性进行精确替换
php
<?php
declare(strict_types=1);
function removeComments(string $source): string
{
$tokens = PhpToken::tokenize($source);
$result = '';
foreach ($tokens as $token) {
if (!$token->is(T_COMMENT, T_DOC_COMMENT)) {
$result .= $token->text;
} else {
// 保留换行符(避免合并行)
if (str_contains($token->text, "\n")) {
$result .= str_repeat("\n", substr_count($token->text, "\n"));
}
}
}
return $result;
}实战:自动添加 return 类型声明
php
<?php
declare(strict_types=1);
function extractMethodSignatures(string $source): array
{
$tokens = PhpToken::tokenize($source);
$signatures = [];
$inClass = null;
$braceDepth = 0;
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if ($token->is(T_CLASS, T_TRAIT)) {
// 找到类名
for ($j = $i + 1; $j < count($tokens); $j++) {
if ($tokens[$j]->is(T_STRING)) {
$inClass = $tokens[$j]->text;
break;
}
}
}
if ($token->is(T_FUNCTION) && $inClass !== null) {
$name = '';
$params = '';
$returnType = 'void';
for ($j = $i + 1; $j < count($tokens); $j++) {
if ($tokens[$j]->is(T_STRING) && $name === '') {
$name = $tokens[$j]->text;
}
if ($tokens[$j]->text === ')' && $params === '') {
// 检查返回类型
for ($k = $j + 1; $k < count($tokens); $k++) {
if ($tokens[$k]->is(T_STRING)) {
$returnType = $tokens[$k]->text;
break;
}
}
break;
}
if ($tokens[$j]->is(T_VARIABLE) || $tokens[$j]->is(T_STRING)) {
$params .= $tokens[$j]->text;
}
}
$signatures[] = [
'class' => $inClass,
'method' => $name,
'return' => $returnType,
];
}
}
return $signatures;
}PHP 8.0 前的兼容性
PhpToken 仅在 PHP 8.0+ 中可用。需要在 PHP 7.x 中运行的代码仍需使用 token_get_all()。
PhpToken 对象是只读的
PhpToken 的属性是只读的,不能修改。
最佳实践
- PHP 8.0+ 优先使用 PhpToken:更清晰的 API。
- 使用 is() 方法过滤:比手动比较 ID 更方便。
- 利用 pos 属性定位:用于精确的文本替换。
php
<?php
declare(strict_types=1);
// 推荐:PhpToken 面向对象 API
$tokens = PhpToken::tokenize(file_get_contents($path));
$variables = array_filter($tokens, fn(PhpToken $t): bool => $t->is(T_VARIABLE));
echo count($variables) . " 个变量\n";PhpToken 面向对象分析
自定义 Token 分析器
php
<?php
declare(strict_types=1);
class TokenAnalyzer
{
/** @var array<string, int> */
private array $typeCounts = [];
/** @var array<int, PhpToken> */
private array $tokens = [];
public function analyze(string $code): void
{
$this->tokens = PhpToken::getAll($code);
$this->typeCounts = [];
foreach ($this->tokens as $token) {
$name = $token->getTokenName();
if ($name !== null) {
$this->typeCounts[$name] = ($this->typeCounts[$name] ?? 0) + 1;
}
}
}
public function getTypeCounts(): array
{
arsort($this->typeCounts);
return $this->typeCounts;
}
public function findByType(int $type): array
{
return array_filter(
$this->tokens,
fn(PhpToken $t): bool => $t->id === $type
);
}
public function getTokensByLine(int $line): array
{
return array_filter(
$this->tokens,
fn(PhpToken $t): bool => $t->line === $line
);
}
public function getFunctionSignatures(): array
{
$signatures = [];
$count = count($this->tokens);
for ($i = 0; $i < $count; $i++) {
$token = $this->tokens[$i];
if ($token->id === T_FUNCTION) {
for ($j = $i + 1; $j < $count; $j++) {
$next = $this->tokens[$j];
if ($next->id === T_STRING) {
$sig = [
'name' => $next->text,
'line' => $token->line,
];
$signatures[] = $sig;
break;
}
if (!is_array($next) && $next->text === '(') {
break; // 匿名函数
}
}
}
}
return $signatures;
}
}
$code = file_get_contents(__FILE__);
$analyzer = new TokenAnalyzer();
$analyzer->analyze($code);
echo "Token 类型统计:\n";
foreach ($analyzer->getTypeCounts() as $name => $count) {
echo " {$name}: {$count}\n";
}使用 PhpToken::tokenize 静态方法
php
<?php
declare(strict_types=1);
$code = '<?php echo "Hello, World!";';
// getAll 和 tokenize 是同一方法的别名
$tokens1 = PhpToken::getAll($code);
$tokens2 = PhpToken::tokenize($code);
// 两种调用返回相同结果
echo "getAll 返回: " . count($tokens1) . " tokens\n";
echo "tokenize 返回: " . count($tokens2) . " tokens\n";Token 范围操作
php
<?php
declare(strict_types=1);
$tokens = PhpToken::getAll('<?php class Foo { public function bar() {} }');
// 获取代码中特定范围内的 token
// 例如获取第一个函数的所有 token
$classStart = null;
$funcStart = null;
$funcEnd = null;
foreach ($tokens as $i => $token) {
if ($token->id === T_CLASS && $classStart === null) {
$classStart = $i;
}
if ($token->id === T_FUNCTION && $classStart !== null && $funcStart === null) {
$funcStart = $i;
}
if ($funcStart !== null && $token->text === '}') {
$funcEnd = $i;
break;
}
}
if ($funcStart !== null && $funcEnd !== null) {
$funcTokens = array_slice($tokens, $funcStart, $funcEnd - $funcStart + 1);
echo "函数代码:\n";
foreach ($funcTokens as $token) {
echo $token->text;
}
echo "\n";
}PhpToken 与 token_get_all 的完整对比
| 特性 | token_get_all | PhpToken (PHP 8.0+) |
|---|---|---|
| 返回类型 | 混合数组(数组+字符串) | PhpToken[] 对象数组 |
| 访问 token ID | $token[0] | $token->id |
| 访问 token 文本 | $token[1] | $token->text |
| 访问行号 | $token[2] | $token->line |
| 获取 token 名 | token_name($token[0]) | $token->getTokenName() |
| 类型安全 | 无 | 强类型(PhpToken 对象) |
| 对象方法 | 无 | is(), is(), getId(), getTokenName() |
| 推荐场景 | PHP 7.x 兼容 | PHP 8.0+ 项目 |
常见误区与 FAQ
PhpToken 对象可以序列化吗?
PhpToken 实现了 Stringable 接口,但不建议序列化。如果需要缓存 token 信息,应该提取需要的属性后存储。
如何处理语法错误的代码?
token_get_all 和 PhpToken::getAll 在遇到语法错误时会尝试继续解析,但结果可能不准确。建议先用 php -l 检查语法。
PhpToken::getAll 在大文件中性能如何?
对于非常大的文件,token 解析可能消耗较多内存。可以考虑逐块处理或使用流式解析。