Skip to content

PhpToken 类

概述

PhpToken 是 PHP 8.0 引入的面向对象的 Token 接口,作为传统 token_get_all() 函数的现代化替代。PhpToken 提供了更直观的 API,支持通过 tokenize() 静态方法获取 token 对象数组,每个 token 对象提供了 idtextlinepos 等属性,以及 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 属性

属性类型说明
idintToken ID 常量
textstring原始文本
lineint行号(1-based)
posint字节偏移量(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 的属性是只读的,不能修改。

最佳实践

  1. PHP 8.0+ 优先使用 PhpToken:更清晰的 API。
  2. 使用 is() 方法过滤:比手动比较 ID 更方便。
  3. 利用 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_allPhpToken (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_allPhpToken::getAll 在遇到语法错误时会尝试继续解析,但结果可能不准确。建议先用 php -l 检查语法。

PhpToken::getAll 在大文件中性能如何?

对于非常大的文件,token 解析可能消耗较多内存。可以考虑逐块处理或使用流式解析。

参考链接