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_TAG、T_VARIABLE、T_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 数组的两种格式
- 单字符 token:字符串(如
'('、')'、';') - 多字符 token:数组
[int $id, string $text, int $line]
常用 Token 常量
| 常量 | 说明 |
|---|---|
T_OPEN_TAG | <?php |
T_VARIABLE | $variable |
T_STRING | 标识符 |
T_FUNCTION | function |
T_CLASS | class |
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 配置。
最佳实践
- 复杂分析使用 php-parser:Tokenizer 适合简单分析,复杂场景使用专门的解析器。
- 缓存 token 结果:Token 分析有性能开销。
- 使用 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。