Skip to content

Tokenizer 词法分析

概述

PHP 的 Tokenizer 扩展提供了 token_get_all() 函数,用于将 PHP 源代码分解为一系列词法单元(Tokens)。Tokenizer 是开发 PHP 解析工具、代码格式化器、静态分析器的基础组件。通过 Tokenizer,你可以在不了解完整语法的情况下,对 PHP 源代码进行粒度化的分析。

核心函数

token_get_all() 将 PHP 源代码分解为 token 数组。每个 token 要么是单字符字符串,要么是包含 token ID、文本和行号的数组。

基础概念

什么是 Tokenizer

Tokenizer(词法分析器)将源代码文本分解为一系列有意义的"词"(Tokens)。PHP 的 Tokenizer 将 PHP 源代码分解为标识符、关键字、运算符、字符串、注释等 token。

token_get_all

token_get_all(string $source) 接受 PHP 源代码字符串,返回 token 数组。

PHP_TOKEN_* 常量

每个 token 类型对应一个常量(如 T_OPEN_TAGT_VARIABLET_STRING 等)。

语法与代码

基本 token_get_all 用法

php
<?php
declare(strict_types=1);

$source = '<?php echo "Hello, World!";';
$tokens = token_get_all($source);

foreach ($tokens as $token) {
    if (is_array($token)) {
        $name = token_name($token[0]);
        $text = $token[1];
        $line = $token[2];
        echo "Line {$line}: {$name} => '{$text}'\n";
    } else {
        echo "Char: '{$token}'\n";
    }
}
// Line 1: T_OPEN_TAG => '<?php '
// Char: ' '
// Line 1: T_ECHO => 'echo'
// Char: ' '
// Line 1: T_CONSTANT_ENCAPSED_STRING => '"Hello, World!"'
// Char: ';'

解析函数定义

php
<?php
declare(strict_types=1);

$source = <<<'PHP'
<?php
function greet(string $name): string {
    return "Hello, {$name}!";
}
PHP;

$tokens = token_get_all($source);

foreach ($tokens as $token) {
    if (is_array($token)) {
        switch ($token[0]) {
            case T_FUNCTION:
                echo "发现函数定义\n";
                break;
            case T_STRING:
                echo "标识符: {$token[1]}\n";
                break;
            case T_VARIABLE:
                echo "变量: {$token[1]}\n";
                break;
        }
    }
}
// 发现函数定义
// 标识符: greet
// 变量: $name
// 标识符: string
// 标识符: string

统计 token 类型

php
<?php
declare(strict_types=1);

function countTokens(string $source): array
{
    $tokens = token_get_all($source);
    $counts = [];

    foreach ($tokens as $token) {
        if (is_array($token)) {
            $name = token_name($token[0]);
            $counts[$name] = ($counts[$name] ?? 0) + 1;
        }
    }

    arsort($counts);
    return $counts;
}

$source = file_get_contents(__FILE__);
$counts = countTokens($source);

echo "Top 10 tokens:\n";
$i = 0;
foreach ($counts as $name => $count) {
    echo "  {$name}: {$count}\n";
    if (++$i >= 10) break;
}

详细说明

token 数组的两种格式

  1. 单字符 token:字符串(如 '('')'';'
  2. 多字符 token:数组 [int $id, string $text, int $line]

常用 Token 常量

常量说明
T_OPEN_TAG<?php
T_VARIABLE$variable
T_STRING标识符
T_FUNCTIONfunction
T_CLASSclass
T_COMMENT///* */ 注释
T_DOC_COMMENT/** */ 文档注释
T_WHITESPACE空白字符
T_CONSTANT_ENCAPSED_STRING'string'"string"

Tokenizer 的限制

Tokenizer 只做词法分析,不做语法分析。它不能告诉你代码是否合法,只是将文本分解为 token。

实战示例

实战:提取所有函数名

php
<?php
declare(strict_types=1);

function extractFunctionNames(string $source): array
{
    $tokens = token_get_all($source);
    $functions = [];
    $prevFunction = false;

    for ($i = 0; $i < count($tokens); $i++) {
        $token = $tokens[$i];

        if (is_array($token) && $token[0] === T_FUNCTION) {
            $prevFunction = true;
            continue;
        }

        if ($prevFunction && is_array($token) && $token[0] === T_STRING) {
            $functions[] = $token[1];
            $prevFunction = false;
        }

        if (is_array($token) && !in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {
            $prevFunction = false;
        }
    }

    return $functions;
}

$source = file_get_contents(__FILE__);
print_r(extractFunctionNames($source));

注意事项

提取所有变量名

php
<?php
declare(strict_types=1);

function extractVariables(string $source): array
{
    $tokens = token_get_all($source);
    $variables = [];

    foreach ($tokens as $token) {
        if (is_array($token) && $token[0] === T_VARIABLE) {
            $variables[] = $token[1];
        }
    }

    return array_unique($variables);
}

$source = '<?php $name = "Alice"; $age = 30; echo $name;';
print_r(extractVariables($source));
// Array ( [0] => $name [1] => $age )

统计代码行数(去除空白和注释)

php
<?php
declare(strict_types=1);

function countCodeLines(string $source): int
{
    $tokens = token_get_all($source);
    $codeLines = [];

    foreach ($tokens as $token) {
        if (is_array($token)) {
            $line = $token[2];
            $type = $token[0];

            if (!in_array($type, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {
                $codeLines[$line] = true;
            }
        }
    }

    return count($codeLines);
}

echo "代码行数: " . countCodeLines(file_get_contents(__FILE__)) . "\n";

检测代码中是否使用了 eval

php
<?php
declare(strict_types=1);

function containsEval(string $source): bool
{
    $tokens = token_get_all($source);

    for ($i = 0; $i < count($tokens); $i++) {
        if (is_array($tokens[$i]) && $tokens[$i][0] === T_EVAL) {
            return true;
        }
    }

    return false;
}

提取命名空间和使用声明

php
<?php
declare(strict_types=1);

function extractUseStatements(string $source): array
{
    $tokens = token_get_all($source);
    $uses = [];
    $inUse = false;
    $currentUse = '';

    foreach ($tokens as $token) {
        if (is_array($token)) {
            if ($token[0] === T_USE) {
                $inUse = true;
            } elseif ($inUse) {
                if ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR) {
                    $currentUse .= $token[1];
                } elseif ($token[0] === T_WHITESPACE) {
                    continue;
                } elseif ($token[1] === ';') {
                    if ($currentUse !== '') {
                        $uses[] = trim($currentUse);
                        $currentUse = '';
                    }
                    $inUse = false;
                }
            }
        } elseif ($token === ',' && $inUse) {
            if ($currentUse !== '') {
                $uses[] = trim($currentUse);
                $currentUse = '';
            }
        }
    }

    return $uses;
}

Tokenizer 不是解析器

Tokenizer 只做词法分析。如果需要完整的语法分析(AST),考虑使用 nikic/php-parser 等库。

短标签支持

token_get_all() 对短标签(<?)的支持取决于 short_open_tag 配置。

最佳实践

  1. 复杂分析使用 php-parser:Tokenizer 适合简单分析,复杂场景使用专门的解析器。
  2. 缓存 token 结果:Token 分析有性能开销。
  3. 使用 token_name() 输出可读名称

Tokenizer 高级应用

使用 token_get_all 分析代码结构

php
<?php
declare(strict_types=1);

function analyzeTokens(string $code): array
{
    $tokens = token_get_all($code);
    $stats = [
        'totalTokens' => count($tokens),
        'functions' => 0,
        'classes' => 0,
        'variables' => 0,
        'strings' => 0,
        'comments' => 0,
        'whitespace' => 0,
    ];

    foreach ($tokens as $token) {
        if (is_array($token)) {
            switch ($token[0]) {
                case T_FUNCTION:
                    $stats['functions']++;
                    break;
                case T_CLASS:
                    $stats['classes']++;
                    break;
                case T_VARIABLE:
                    $stats['variables']++;
                    break;
                case T_STRING:
                case T_CONSTANT_ENCAPSED_STRING:
                    $stats['strings']++;
                    break;
                case T_COMMENT:
                case T_DOC_COMMENT:
                    $stats['comments']++;
                    break;
                case T_WHITESPACE:
                    $stats['whitespace']++;
                    break;
            }
        }
    }

    return $stats;
}

$code = <<<'PHP'
<?php
/**
 * 示例类
 */
class Example {
    public function greet(string $name): string {
        return "Hello, {$name}";
    }
}
PHP;

print_r(analyzeTokens($code));

查找函数定义

php
<?php
declare(strict_types=1);

function findFunctions(string $filePath): array
{
    $code = file_get_contents($filePath);
    if ($code === false) return [];

    $tokens = token_get_all($code);
    $functions = [];
    $inClass = false;
    $className = '';

    for ($i = 0, $count = count($tokens); $i < $count; $i++) {
        $token = $tokens[$i];

        if (is_array($token)) {
            if ($token[0] === T_CLASS) {
                $inClass = true;
                // 下一个有意义 token 是类名
                for ($j = $i + 1; $j < $count; $j++) {
                    if (is_array($tokens[$j]) && $tokens[$j][0] !== T_WHITESPACE) {
                        $className = $tokens[$j][1];
                        break;
                    }
                }
            }

            if ($token[0] === T_FUNCTION) {
                // 跳过空白找函数名
                for ($j = $i + 1; $j < $count; $j++) {
                    if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {
                        $functions[] = [
                            'name' => $tokens[$j][1],
                            'class' => $inClass ? $className : null,
                            'line' => $token[2],
                        ];
                        break;
                    }
                    // 匿名函数,跳过
                    if (!is_array($tokens[$j]) && $tokens[$j] === '(') {
                        break;
                    }
                }
            }
        }
    }

    return $functions;
}

代码格式化工具基础

php
<?php
declare(strict_types=1);

function simpleFormat(string $code): string
{
    $tokens = token_get_all($code);
    $output = '';
    $indent = 0;
    $newline = true;

    foreach ($tokens as $token) {
        if (is_array($token)) {
            if ($token[0] === T_OPEN_TAG || $token[0] === T_OPEN_TAG_WITH_ECHO) {
                $output .= $token[1];
                $newline = false;
            } elseif ($token[0] === T_WHITESPACE) {
                if ($newline) {
                    $output .= str_repeat('    ', $indent);
                }
            } elseif ($token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT) {
                if ($newline) {
                    $output .= str_repeat('    ', $indent);
                }
                $output .= $token[1] . "\n";
                $newline = true;
            } else {
                if ($newline) {
                    $output .= str_repeat('    ', $indent);
                }
                $output .= $token[1];
                $newline = false;
            }
        } else {
            // 单字符 token
            if ($token === '{') {
                $output .= " {\n";
                $indent++;
                $newline = true;
            } elseif ($token === '}') {
                $indent--;
                $output .= str_repeat('    ', $indent) . "}\n";
                $newline = true;
            } elseif ($token === ';') {
                $output .= ";\n";
                $newline = true;
            } else {
                $output .= $token;
                $newline = false;
            }
        }
    }

    return $output;
}

常见误区与 FAQ

token_get_all 的返回值格式是什么?

返回值是一个混合数组,每个元素要么是:

  • 数组 [tokenId, text, lineNumber] — 有意义的 token
  • 字符串 — 单字符 token(如 (){}; 等)

如何处理 PHP 混合 HTML 的代码?

token_get_all 会将 HTML 部分识别为 T_INLINE_HTML token。可以过滤掉或特殊处理这些 token。

参考链接