Токенизация и синтаксический анализ PHP кода
Парсинг PHP файла: основные подходы
Встроенный токенизатор (token_get_all)
Наиболее эффективное решение для базового разбора PHP файла - использование функции token_get_all(). Она разбивает исходный код на лексические токены, возвращая массив, каждый элемент которого содержит идентификатор токена, его текст и номер строки. Это позволяет анализировать файл без его выполнения.
Пример базового использования:
$code = file_get_contents('example.php');
$tokens = token_get_all($code);
foreach ($tokens as $token) {
if (is_array($token)) {
echo token_name($token[0]) . ': ' . $token[1] . "\n";
}
}
Parse php file (парсинг php файла)
Результат выводит типы токенов, например T_OPEN_TAG, T_ECHO, T_STRING и т.д.
Возможные проблемы:
- Токенизация даёт плоский поток, сложно обрабатывать вложенные структуры (например, найти все операторы внутри определённой функции).
- Не учитывается контекст - одинаковые строки могут быть именами функций, переменных или классов.
- Высокая сложность при построении собственного парсера для полноценного анализа.
Решение: для сложных задач переходить к AST (см. вариант с PHP-Parser).
Цели и случаи использования:
- Подсчёт вызовов определённых функций в проекте.
- Проверка наличия запрещённых конструкций (например, eval).
- Извлечение строковых литералов для перевода.
Как извлечь имена функций и классов без полной токенизации?
Вариант с регулярными выражениями. Используя preg_match_all, можно найти паттерны для function, class, но метод не учитывает комментарии и строки, что приводит к ложным срабатываниям.
$code = file_get_contents('example.php');
preg_match_all('/\bfunction\s+([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\(/', $code, $matches);
$functionNames = $matches[1];
print_r($functionNames);
Php run file (запуск файла php)
Типичные ошибки:
- Пропуск функций, объявленных анонимно или с возвращаемыми типами.
- Ложное обнаружение слова 'function' внутри строк или комментариев.
- Неверная обработка многострочных объявлений.
Решение: перед регэкспами удалять комментарии и строки, но это усложняет код. Лучше использовать token_get_all.
Целесообразно применять только для быстрых одноразовых скриптов или при отсутствии возможности установки сторонних библиотек.
Как получить структурированное синтаксическое дерево (AST) PHP файла?
Сторонняя библиотека nikic/php-parser (установка через Composer) строит абстрактное синтаксическое дерево, позволяя рекурсивно обходить все конструкции: объявления, вызовы, выражения. Это самый мощный вариант для глубокого анализа.
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
$code = file_get_contents('example.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
// Обход всех узлов
$traverser = new NodeTraverser();
$traverser->addVisitor(new class extends NodeVisitorAbstract {
public function enterNode(Node $node) {
if ($node instanceof \PhpParser\Node\Expr\FuncCall) {
echo 'Вызов функции: ' . $node->name->toString() . "\n";
}
}
});
$traverser->traverse($ast);
Возможные сложности:
- Зависимость от внешней библиотеки и версии PHP.
- Необходимость разбираться в классах узлов (Node) для написания посетителей.
- Синтаксические ошибки в исходном файле приводят к исключениям, которые нужно обрабатывать.
Решение: комбинировать с try-catch и валидацией кода перед парсингом.
Используется для рефакторинга, статического анализа, автозамены кода, извлечения метаданных.
Как получить информацию о классах и методах без разбора текста?
Reflection API (классы ReflectionClass, ReflectionMethod) позволяет анализировать уже загруженные классы. Для этого файл сначала подключается (include/require), а затем создаются объекты отражения. Этот метод не требует парсинга как такового, но приводит к выполнению кода из файла.
require_once 'example.php';
$reflection = new ReflectionClass('MyClass');
$methods = $reflection->getMethods();
foreach ($methods as $method) {
echo $method->getName() . ' : ' . $method->getStartLine() . "\n";
}
Проблемы и ограничения:
- Файл должен быть синтаксически корректен и не должен содержать фатальных ошибок.
- Выполнение кода может иметь побочные эффекты (например, вызовы функций, запись в БД).
- Невозможно анализировать неисполняемые участки (например, мертвый код).
Решение: использовать только для доверенных файлов или в средах с изоляцией.
Подходит для анализа автозагруженных классов в приложении, построения документации, проверки сигнатур методов.
Расширенные примеры парсинга PHP файлов
Извлечение всех имён пользовательских функций и их параметров с помощью token_get_all.
$code = file_get_contents('example.php');
$tokens = token_get_all($code);
$functions = [];
$count = count($tokens);
for ($i = 0; $i < $count; $i++) {
if (is_array($tokens[$i]) && $tokens[$i][0] === T_FUNCTION) {
// Следующий токен - имя функции
$next = $tokens[$i + 1];
if (is_array($next) && $next[0] === T_STRING) {
$name = $next[1];
// Собираем параметры до '{'
$params = [];
$j = $i + 2;
while ($j < $count) {
if (is_array($tokens[$j]) && $tokens[$j][0] === T_VARIABLE) {
$params[] = $tokens[$j][1];
} elseif ($tokens[$j] === '{') {
break;
}
$j++;
}
$functions[$name] = $params;
}
}
}
print_r($functions);
Array
(
[myFunction] => Array
(
[0] => $arg1
[1] => $arg2
)
[anotherFunc] => Array
(
[0] => $data
)
)
Пример 2: Использование PHP-Parser для поиска всех вызовов методов с цепочками
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node\Expr\MethodCall;
$code = file_get_contents('example.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
$traverser = new NodeTraverser();
$traverser->addVisitor(new class extends NodeVisitorAbstract {
public function enterNode(Node $node) {
if ($node instanceof MethodCall) {
// Выводим цепочку: var->method()
$var = $node->var;
$varName = ($var instanceof \PhpParser\Node\Expr\Variable) ? $var->name : 'expr';
echo "\${$varName}->{$node->name->toString()}\n";
}
}
});
$traverser->traverse($ast);
$object->doSomething
$object->getConfig
$db->query('...')
Пример 3: Извлечение всех use-import из файла с помощью регулярных выражений (с предварительной очисткой)
$code = file_get_contents('example.php');
// Удаляем многострочные комментарии и строки, содержащие 'use'
$cleaned = preg_replace('/\/\*.*?\*\/|\/\/[^\n]*/', '', $code);
// Удаляем строки в кавычках (упрощённо)
$cleaned = preg_replace('/["\'][^"\']*["\']/', '', $cleaned);
preg_match_all('/\buse\s+([^;]+);/im', $cleaned, $matches);
$uses = array_map('trim', $matches[1]);
print_r($uses);
Array
(
[0] => App\Models\User
[1] => App\Helpers\StringHelper
)
// Файл Config.php содержит класс с константами
require_once 'Config.php';
$ref = new ReflectionClass('Config');
$constants = $ref->getConstants();
echo "Константы класса Config:\n";
foreach ($constants as $name => $value) {
echo " $name = $value\n";
}
Константы класса Config: DEBUG_MODE = 1 VERSION = '2.0.1'
Пример 5: Проверка на использование eval с token_get_all
$code = file_get_contents('example.php');
$tokens = token_get_all($code);
$hasEval = false;
foreach ($tokens as $token) {
if (is_array($token) && $token[0] === T_EVAL) {
$hasEval = true;
break;
}
}
echo $hasEval ? "Найден eval" : "eval не используется";
eval не используется