Токенизация и синтаксический анализ PHP кода

Раздел: Обработка данных в 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 файлов

Пример 1: Токенизация с фильтрацией и группировкой

Извлечение всех имён пользовательских функций и их параметров с помощью 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
)
Пример 4: Анализ констант класса через Reflection
Пример

// Файл 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 не используется

Парсинг PHP файла - comments

En
Parse php file (php)