Глава 111 · От основ к практике
Tokenizer: понять PHP-исходник, не выполняя его
Полная категория Tokenizer: функции, PhpToken, позиции, TOKEN_PARSE, каталог T_*, анализ шаблонов и осторожное преобразование исходников.
Зачем разбирать код как данные
Исходный PHP-код становится данными анализатора
Анализируем запись программы, не выполняя её.
- <?php $price = 7;
Строка исходника — входные данные.
- PhpToken::tokenize
Разбивает текст на элементы с ID, текстом, строкой и байтовой позицией.
- $price / T_VARIABLE / pos 6
Точный фрагмент с понятным видом и координатами.
- Подсветка или отчёт
Правило анализатора выбирает нужные элементы.
Лексер не знает бизнес-смысл и не запускает анализируемый код. Для сложного преобразования требуется разбор структуры.
Вы сопровождаете сайт и хотите найти места, где разработчик подавил ошибку оператором @. Простой поиск текста захватит адреса электронной почты и комментарии. Tokenizer помогает отличить запись программы от текста внутри неё. На таком же механизме строятся подсветка, отчёты о стиле и часть инструментов преобразования исходников.
- Строка: <?php @readfile("missing.txt");
- Сканер видит отдельный знак @
- Анализатор запоминает строку и позицию
- Отчёт предлагает проверить обработку ошибки
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
<?php
declare(strict_types=1);
$source = '<?php $price = 7;';
$tokens = PhpToken::tokenize($source);
echo $tokens[1]->text, "\n";
echo $tokens[1]->getTokenName(), "\n";
Ожидаемый результат
$price
T_VARIABLE
Переменная $source содержит текст другой программы. [1] берёт второй элемент списка: первый — открывающий тег. ->text читает исходную запись; getTokenName() возвращает её вид. Для произвольного файла фиксированный индекс не подходит — дальше используем перебор.
Аналогия — разложить предложение на слова и знаки с подписями. Она ограничена: PHP имеет состояния сканера, интерполяцию, теги и составные конструкции. Токены ещё не объясняют, какую задачу решает программа, и не составляют полноценное дерево выражений.
Установка, версии и границы инструмента
Tokenizer использует лексический сканер Zend Engine. Расширение включено по умолчанию; при собственной сборке его можно отключить параметром --disable-tokenizer. На Windows поддержка встроена: отдельную DLL tokenizer подключать не требуется. Отдельных конфигурационных директив для этой категории нет. Объект PhpToken доступен с PHP 8.0, две процедурные функции — с PHP 4.2.
<?php
declare(strict_types=1);
var_dump(extension_loaded('tokenizer'));
var_dump(function_exists('token_get_all'));
var_dump(class_exists(PhpToken::class));
Ожидаемый результат
bool(true)
bool(true)
bool(true)
Это результат стенда PHP 8.5.11. Если tokenizer отключён в сборке, вызов отсутствующей функции не станет запасным режимом; приложение должно сообщить об отсутствии зависимости.
Анализатор следует версии того PHP, где запущен, а не версии, указанной в комментарии исходного файла. PHP 8.2 не научится оператору из PHP 8.5 от добавления константы в пользовательский код. Числовые значения T_* меняются; сравнивайте именованные константы. Для анализа нескольких версий нужен инструмент с явно выбранной грамматикой.
| Потребность | Подход |
|---|---|
| Подписи элементов, подсветка, ограниченный отчёт | Tokenizer с исходными текстами и координатами. |
| Найти имена/выражения с учётом вложенности и namespace | Парсер AST; правила преобразования и сохранения форматирования. |
| Проверить синтаксис файла целевой версией | php -l на нужной версии; это не полноценное выполнение. |
| Проверить бизнес-результат программы | Тесты в её окружении; Tokenizer не знает правильный остаток товара. |
Токенизация не выполняет исходный код. Это не повод принимать неограниченный вход: исходная строка и массив/объекты занимают память, а разбор требует времени. Сначала задайте лимит размера. Не обещайте фиксированный выигрыш одного API над другим — измеряйте на своих файлах.
PhpToken: четыре поля и единый интерфейс
| Свойство | Содержание |
|---|---|
| public int $id | Вид токена; для одиночного знака — его байтовое значение. |
| public string $text | Точный фрагмент исходника; не результат вычисления. |
| public int $line | Начальная строка, начиная с 1; вручную по умолчанию -1. |
| public int $pos | Байтовое смещение с 0; вручную по умолчанию -1. |
PhpToken реализует Stringable и допускает наследование. Его конструктор final, а tokenize возвращает объекты класса, у которого вызван. Поля доступны для изменения, но изменение text не пересчитывает id, pos и line. Эта структура описывает результат разбора, а не автоматически поддерживаемое дерево программы.
<?php
declare(strict_types=1);
$source = '<?php /* я */ $price = 7;';
foreach (PhpToken::tokenize($source) as $token) {
if ($token->is(T_VARIABLE)) {
var_dump(substr($source, $token->pos, strlen($token->text)) === '$price');
echo 'byte offset: ', $token->pos, "\n";
}
}
Ожидаемый результат
bool(true)
byte offset: 15
Русская буква я занимает два байта UTF-8, поэтому байтовая позиция отличается от числа видимых символов перед ней. substr и strlen здесь работают с байтами, что соответствует pos. Это не правило для обрезки пользовательского текста на экране.
TOKEN_PARSE: лексический вид зависит от грамматического места
Единственный описанный флаг этой категории — TOKEN_PARSE. Он позволяет корректно распознавать зарезервированные слова там, где язык разрешает их использовать как имена. Флаг появился в PHP 7.0. Разница показана в token_get_all: PUBLIC внутри const может иметь вид T_STRING. При неверной грамматике получают ParseError; это надо отличать от запуска программы.
<?php
declare(strict_types=1);
$tokens = PhpToken::tokenize('<?php missingShopFunction();', TOKEN_PARSE);
echo 'parsed tokens: ', count($tokens), "\n";
Ожидаемый результат
parsed tokens: 5
Анализатор принял форму вызова. Он не пытался вызвать missingShopFunction и не доказал, что она существует. Проверка синтаксиса, разрешение имён и выполнение — разные этапы.
Без TOKEN_PARSE сканер удобен для промежуточного текста в редакторе. Это не обещание принять любую последовательность байтов: некоторые ошибки лексического уровня всё равно возможны. Внешние исходники разбирайте с обработкой исключений и ограничением размера.
Полный каталог T_*: запись, назначение и версии
Таблица охватывает имена из официального приложения «List of Parser Tokens», включая исторические. Наличие строки в таблице не означает наличие константы в каждой версии PHP. Проверяют defined("T_PIPE"), если анализатор работает на нескольких версиях. Не задавайте вымышленные числовые ID отсутствующим токенам: сканер старого PHP всё равно не выучит новую грамматику.
Запись в средней колонке показывает характерный синтаксис, а не обещает точные границы text во всех контекстах. Для знаков ;, +, скобок и многих других односимвольных элементов T_*-константы нет. У token_get_all это строка, у PhpToken — объект с соответствующим однобайтовым ID.
| Токен | Характерная запись | Назначение и важная граница |
|---|---|---|
| T_ABSTRACT | abstract | Абстрактный класс или метод: определяет обязательства для наследника. |
| T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG | & | PHP 8.1+: амперсанд перед переменной или ...; нужен, например, для передачи по ссылке. Роль определяется грамматикой. |
| T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG | & | PHP 8.1+: другой контекст амперсанда, например пересечение типов. Нельзя определять смысл только по тексту &. |
| T_AND_EQUAL | &= | Присвоение результата побитового И. |
| T_ARRAY | array | Длинная запись массива либо слово типа array. Круглые скобки — отдельные элементы. |
| T_ARRAY_CAST | (array) | Явное преобразование значения в массив. |
| T_AS | as | Связывает элемент с переменной в foreach; встречается и при задании псевдонима. |
| T_ATTRIBUTE | #[ | PHP 8.0+: начало атрибута. Это уже не обычный комментарий #. |
| T_BAD_CHARACTER | управляющий байт | PHP 7.4+: недопустимые управляющие байты ASCII ниже 32, кроме табуляции, LF и CR. Это не любой неправильный символ UTF-8. |
| T_BOOLEAN_AND | && | Логическое И с коротким замыканием. |
| T_BOOLEAN_OR | || | Логическое ИЛИ с коротким замыканием. |
| T_BOOL_CAST | (bool), (boolean) | Явное преобразование в логическое значение. |
| T_BREAK | break | Выход из цикла или switch; уровень вложенности разбирается отдельно. |
| T_CALLABLE | callable | Объявление вызываемого типа; не обозначает конкретную функцию. |
| T_CASE | case | Ветка switch или case перечисления: нужен контекст. |
| T_CATCH | catch | Обработчик выброшенного исключения. |
| T_CLASS | class | Объявление класса либо часть ::class; один токен не доказывает наличие объявления. |
| T_CLASS_C | __CLASS__ | Магическая константа имени класса; значение появляется при обработке программы. |
| T_CLONE | clone | Создание копии объекта с правилами клонирования. |
| T_CLOSE_TAG | ?> | Закрывающий PHP-тег; исторический %> больше не поддерживается в PHP 7+. |
| T_COALESCE | ?? | Выбор запасного значения при null или отсутствующем значении. |
| T_COALESCE_EQUAL | ??= | PHP 7.4+: присваивание, если значение отсутствует или равно null. |
| T_COMMENT | //, #, /* ... */ | Обычный комментарий вместе с его текстом. Строковый литерал с // не становится комментарием. |
| T_CONCAT_EQUAL | .= | Дописать строку к переменной. |
| T_CONST | const | Объявить константу; область объявления определяется окружением. |
| T_CONSTANT_ENCAPSED_STRING | 'price', "price" | Заключённая в кавычки строка без разбиваемой интерполяции. Содержимое ещё хранит исходную запись, а не вычисленное PHP-значение. |
| T_CONTINUE | continue | Перейти к следующему шагу цикла; правила switch внутри цикла требуют отдельного внимания. |
| T_CURLY_OPEN | {$ | Начало сложной интерполяции внутри строки; фактический текст токена и соседние токены смотрите в результате сканера. |
| T_DEC | -- | Уменьшение; положение до или после переменной влияет на значение выражения. |
| T_DECLARE | declare | Директива выполнения или компиляции для участка/файла. |
| T_DEFAULT | default | Запасная ветвь switch/match. Само слово не задаёт выполняемое действие. |
| T_DIR | __DIR__ | Магическая константа каталога файла. |
| T_DIV_EQUAL | /= | Деление с присваиванием результата. |
| T_DNUMBER | 0.12, 1e3 | Запись числа с плавающей точкой; исходная запись остаётся текстом. |
| T_DO | do | Начало цикла с проверкой условия после тела. |
| T_DOC_COMMENT | /** ... */ | Документационный комментарий. Парсер его пропускает, но IDE и библиотеки могут читать. |
| T_DOLLAR_OPEN_CURLY_BRACES | ${ | Историческая форма интерполяции ${...}; соответствующие формы устаревают с PHP 8.2. Не советуем её для нового кода. |
| T_DOUBLE_ARROW | => | Связь ключа со значением, ветвь match или тело стрелочной функции — нужен контекст. |
| T_DOUBLE_CAST | (double), (float) | Приведение к float. Историческое (real) удалено в PHP 8.0. |
| T_DOUBLE_COLON | :: | Псевдоним T_PAAMAYIM_NEKUDOTAYIM: обращение к члену класса или ::class. |
| T_ECHO | echo | Вывод значения. Не является обычной пользовательской функцией. |
| T_ELLIPSIS | ... | Распаковка, вариативный параметр или создание first-class callable — роль зависит от места. |
| T_ELSE | else | Запасная ветвь условия. |
| T_ELSEIF | elseif | Следующая проверка после неуспешного if. |
| T_EMPTY | empty | Проверка на «пустое» значение по правилам PHP. |
| T_ENCAPSED_AND_WHITESPACE | текст внутри интерполяции | Литеральная часть строки с подстановками; несмотря на имя, это не только пробелы. |
| T_ENDDECLARE | enddeclare | Закрывает альтернативную форму declare: ... enddeclare. |
| T_ENDFOR | endfor | Закрывает альтернативную форму for. |
| T_ENDFOREACH | endforeach | Закрывает альтернативную форму foreach. |
| T_ENDIF | endif | Закрывает альтернативную форму if. |
| T_ENDSWITCH | endswitch | Закрывает альтернативную форму switch. |
| T_ENDWHILE | endwhile | Закрывает альтернативную форму while. |
| T_ENUM | enum | PHP 8.1+: начало объявления перечисления. |
| T_END_HEREDOC | закрывающая метка | Конец heredoc/nowdoc; содержит выбранное в исходнике имя метки. |
| T_EVAL | eval | Конструкция исполнения PHP-текста. Токенизация сама eval не вызывает. |
| T_EXIT | exit, die | Завершение выполнения; с PHP 8.4 семантика вызова расширена, вид токена сохраняется. |
| T_EXTENDS | extends | Указывает наследование класса или интерфейса. |
| T_FILE | __FILE__ | Магическая константа пути файла. |
| T_FINAL | final | Ограничение переопределения/наследования в соответствующем контексте. |
| T_FINALLY | finally | Завершающий блок try: освобождение ресурса независимо от результата. |
| T_FN | fn | PHP 7.4+: стрелочная функция. |
| T_FOR | for | Цикл с явными инициализацией, проверкой и изменением. |
| T_FOREACH | foreach | Последовательный перебор массива/итерируемого объекта. |
| T_FUNCTION | function | Объявление именованной или анонимной функции. |
| T_FUNC_C | __FUNCTION__ | Магическая константа имени функции. |
| T_GLOBAL | global | Связывает локальное имя с глобальной переменной. |
| T_GOTO | goto | Переход к метке с ограничениями области действия. |
| T_HALT_COMPILER | __halt_compiler | Остановка компиляции оставшегося текста файла; после неё может находиться полезная нагрузка. |
| T_IF | if | Условный выбор исполнения. |
| T_IMPLEMENTS | implements | Класс объявляет реализуемые интерфейсы. |
| T_INC | ++ | Увеличение; префиксная и постфиксная формы отличаются результатом выражения. |
| T_INCLUDE | include | Подключение и выполнение другого PHP-файла. |
| T_INCLUDE_ONCE | include_once | Подключение с защитой от повторного включения того же файла. |
| T_INLINE_HTML | текст вне PHP | Обычный текст/разметка за пределами PHP-тегов; даже похожий на PHP комментарий здесь остаётся текстом. |
| T_INSTANCEOF | instanceof | Проверка принадлежности объекта классу/интерфейсу. |
| T_INSTEADOF | insteadof | Выбор метода при конфликте подключённых трейтов. |
| T_INTERFACE | interface | Объявление интерфейса. |
| T_INT_CAST | (int), (integer) | Явное преобразование к целому; не полноценная проверка внешнего ввода. |
| T_ISSET | isset | Проверка существования значения, отличного от null. |
| T_IS_EQUAL | == | Нестрогое сравнение с правилами преобразования типов. |
| T_IS_GREATER_OR_EQUAL | >= | Сравнение «больше либо равно». |
| T_IS_IDENTICAL | === | Строгое сравнение значения и типа. |
| T_IS_NOT_EQUAL | !=, <> | Нестрогое неравенство. |
| T_IS_NOT_IDENTICAL | !== | Отрицание строгой идентичности. |
| T_IS_SMALLER_OR_EQUAL | <= | Сравнение «меньше либо равно». |
| T_LINE | __LINE__ | Магическая константа номера строки. |
| T_LIST | list | Деструктуризация массива в переменные. |
| T_LNUMBER | 123, 012, 0x1ac | Целочисленная запись разных оснований; отрицательный знак обычно отдельный токен. |
| T_LOGICAL_AND | and | Логическое И с приоритетом, отличающимся от &&. |
| T_LOGICAL_OR | or | Логическое ИЛИ с приоритетом, отличающимся от двойного вертикального штриха. |
| T_LOGICAL_XOR | xor | Логическое исключающее ИЛИ. |
| T_MATCH | match | PHP 8.0+: выражение выбора со строгим сравнением. |
| T_METHOD_C | __METHOD__ | Магическая константа имени метода вместе с классом. |
| T_MINUS_EQUAL | -= | Вычитание с присваиванием. |
| T_MOD_EQUAL | %= | Остаток от деления с присваиванием. |
| T_MUL_EQUAL | *= | Умножение с присваиванием. |
| T_NAMESPACE | namespace | Объявление пространства имён либо часть относительного обращения. |
| T_NAME_FULLY_QUALIFIED | \App\Order | PHP 8.0+: полное имя с ведущим обратным слешем. |
| T_NAME_QUALIFIED | App\Order | PHP 8.0+: составное имя без ведущего обратного слеша. |
| T_NAME_RELATIVE | namespace\Order | PHP 8.0+: имя относительно текущего namespace. |
| T_NEW | new | Создание объекта, в том числе анонимного класса. |
| T_NS_C | __NAMESPACE__ | Магическая константа текущего пространства имён. |
| T_NS_SEPARATOR | обратный слеш | Разделитель пространства имён в контекстах, где не поглощён составным токеном имени. |
| T_NUM_STRING | индекс в "$items[0]" | Числовой индекс внутри интерполируемой строки. Это не любой числовой литерал программы. |
| T_OBJECT_CAST | (object) | Преобразование значения в объект. |
| T_OBJECT_OPERATOR | -> | Доступ к свойству/методу объекта. |
| T_NULLSAFE_OBJECT_OPERATOR | ?-> | PHP 8.0+: доступ с остановкой цепочки при null. |
| T_OPEN_TAG | <?php, <? | Открывающий тег; короткий <? зависит от short_open_tag. Исторический <% удалён в PHP 7.0. Текст токена может включать следующий пробел/перевод строки. |
| T_OPEN_TAG_WITH_ECHO | <?= | Открывающий тег с выводом; исторический <%= удалён в PHP 7.0. Отличается от обычного открывающего тега. |
| T_OR_EQUAL | |= | Побитовое ИЛИ с присваиванием. |
| T_PAAMAYIM_NEKUDOTAYIM | :: | Историческое имя токена разрешения области; T_DOUBLE_COLON обозначает то же значение. |
| T_PIPE | |> | PHP 8.5+: передача значения в вызываемое выражение оператором pipe. |
| T_PLUS_EQUAL | += | Сложение с присваиванием; для массивов действуют правила объединения ключей. |
| T_POW | ** | Возведение в степень. |
| T_POW_EQUAL | **= | Возведение в степень с присваиванием. |
| T_PRINT | Вывод одного выражения с возвращаемым значением 1. | |
| T_PRIVATE | private | Закрытая видимость члена класса. |
| T_PRIVATE_SET | private(set) | PHP 8.4+: закрытая видимость записи свойства при асимметричной видимости. |
| T_PROPERTY_C | __PROPERTY__ | PHP 8.4+: имя свойства в контексте hook свойства. |
| T_PROTECTED | protected | Видимость члена для класса и связанных с ним наследников. |
| T_PROTECTED_SET | protected(set) | PHP 8.4+: ограничение записи свойства иерархией классов. |
| T_PUBLIC | public | Публичная видимость члена. |
| T_PUBLIC_SET | public(set) | PHP 8.4+: публичная видимость записи свойства. |
| T_READONLY | readonly | PHP 8.1+: неизменяемость свойств по правилам readonly; для классов доступна с PHP 8.2. |
| T_REQUIRE | require | Обязательное подключение файла. |
| T_REQUIRE_ONCE | require_once | Обязательное подключение один раз. |
| T_RETURN | return | Вернуть результат и закончить текущий вызов/подключаемый файл. |
| T_SL | << | Сдвиг битов влево. |
| T_SL_EQUAL | <<= | Сдвиг влево с присваиванием. |
| T_SPACESHIP | <=> | Трёхзначное сравнение: отрицательный, нулевой или положительный результат. |
| T_SR | >> | Сдвиг битов вправо. |
| T_SR_EQUAL | >>= | Сдвиг вправо с присваиванием. |
| T_START_HEREDOC | <<<METKA | Начало heredoc/nowdoc; открывающая запись включает метку и перевод строки. |
| T_STATIC | static | Статическое состояние/член либо позднее статическое связывание; нужен контекст. |
| T_STRING | price, Order, self | Идентификатор: имя функции, класса, константы и другие роли. Несмотря на название, не означает литерал строки. |
| T_STRING_CAST | (string) | Явное преобразование к строке. |
| T_STRING_VARNAME | имя в ${...} | Имя переменной в старой форме строковой интерполяции; не обычный T_VARIABLE с долларом. |
| T_SWITCH | switch | Выбор ветви с семантикой switch. |
| T_THROW | throw | Выброс исключения. |
| T_TRAIT | trait | Объявление набора повторно используемых методов/членов. |
| T_TRAIT_C | __TRAIT__ | Магическая константа имени трейта. |
| T_TRY | try | Начало области обработки исключений. |
| T_UNSET | unset | Удаление переменной/элемента, а не универсальная очистка всех копий данных. |
| T_UNSET_CAST | (unset) | Историческое приведение, удалено в PHP 8.0. Сохраняем в справочнике для понимания старого кода, не используем в новых примерах. |
| T_USE | use | Импорт имени, подключение трейта или захват переменной замыканием: роли различаются окружением. |
| T_VAR | var | Историческая запись объявления публичного свойства; для нового кода предпочтительна явная видимость. |
| T_VARIABLE | $price | Запись имени переменной вместе со знаком доллара. |
| T_VOID_CAST | (void) | PHP 8.5+: вычислить выражение и отбросить его значение. Это не возвращаемый тип функции void. |
| T_WHILE | while | Условие цикла while либо завершающая проверка do...while. |
| T_WHITESPACE | пробел, табуляция, CR/LF | Пробельный фрагмент исходника. Отступ не равен незначимости при склеивании соседних слов. |
| T_XOR_EQUAL | ^= | Побитовое исключающее ИЛИ с присваиванием. |
| T_YIELD | yield | Передать значение потребителю генератора и приостановиться. |
| T_YIELD_FROM | yield from | Делегировать перебор другому итерируемому источнику. |
К смыслу конструкций: операторы, условия и циклы, строки и интерполяция, функции, объекты. Таблица лексера помогает распознать запись; эти главы учат применять её.
Убрать комментарий, сохранив границы слов
Нужен служебный просмотр исходника без обычных комментариев. Regex легко перепутает // внутри URL со строковым комментарием. Сканер уже различает эти состояния. Однако вырезать комментарий в пустоту опасно: return/* note */7 превратится в return7. В учебном решении сохраняем пробелы на месте непробельных байтов комментария и все переводы строк.
- return/* note */7
- T_RETURN, T_COMMENT, T_LNUMBER
- Только T_COMMENT заменяется пробелами
- Слова не склеились, номера строк сохранились
<?php
declare(strict_types=1);
$source = '<?php /** API */ echo "https://shop.test"; /* note */';
$result = '';
foreach (PhpToken::tokenize($source, TOKEN_PARSE) as $token) {
if ($token->is(T_COMMENT)) {
$result .= preg_replace('/[^\r\n\t ]/', ' ', $token->text);
} else {
$result .= $token->text;
}
}
var_dump(str_contains($result, 'https://shop.test'));
var_dump(str_contains($result, '/** API */'));
var_dump(!str_contains($result, 'note'));
var_dump(strlen($source) === strlen($result));
Ожидаемый результат
bool(true)
bool(true)
bool(true)
bool(true)
Регулярное выражение здесь работает только внутри уже найденного комментария; оно не пытается разобрать весь PHP. Байтовая длина и переводы строк сохраняются. PHPDoc оставлен намеренно. Это ограниченное преобразование для просмотра, не обещание универсального production-минификатора.
В проекте комментарии могут содержать директивы статического анализатора, лицензию, настройки сборки и аннотации. Их удаление может менять работу инструментов, даже если грамматика PHP осталась корректной. PhpToken::isIgnorable нельзя использовать как кнопку «безопасно удалить всё лишнее».
Практика: отчёт о подавлении ошибок в исходнике
В шаблоне встретился адрес support@shop.test, комментарий с @ и настоящий оператор подавления. Задача — показать только операцию, которую стоит заменить понятной обработкой отказа. Сначала разбираем, затем просматриваем отдельные знаки; ничего не выполняем и не исправляем автоматически.
<?php
declare(strict_types=1);
$source = <<<'PHP'
<?php
$email = 'support@shop.test';
// @ is discussed here
@readfile('missing.txt');
PHP;
foreach (PhpToken::tokenize($source, TOKEN_PARSE) as $token) {
if ($token->is('@')) { echo 'check error handling at line ', $token->line, "\n"; }
}
Ожидаемый результат
check error handling at line 4
Отчёт показывает точное место. Файл missing.txt не читается: это текст для сканера. Следующее действие человека — понять, какой отказ ожидался и как корректно обработать его, а не механически удалить @.
Упражнение: добавьте в исходник строку "@readfile" и ещё один реальный оператор @. Ожидается одно дополнительное сообщение только для оператора. Затем сохраните отчёт с именем файла и номером строки. При показе имени/текста в HTML используйте экранирование; исходник может содержать произвольные теги.
Дальше: ошибки и исключения, чтение файлов, проверка поведения программы.
Все функции и методы Tokenizer
| Инструмент | Полезное действие |
|---|---|
| token_get_all | Разобрать PHP-текст на массив токенов, не выполняя его. |
| token_name | Получить понятное имя T_* по числовому идентификатору. |
| PhpToken::__construct | Создать один токен вручную для своего преобразователя или отчёта. |
| PhpToken::getTokenName | Прочитать имя вида токена либо сам одиночный знак. |
| PhpToken::is | Проверить вид или точный текст токена, в том числе по нескольким альтернативам. |
| PhpToken::isIgnorable | Узнать, пропускает ли синтаксический анализатор этот вид токена. |
| PhpToken::__toString | Получить исходный текст объекта при преобразовании в строку. |
| PhpToken::tokenize | Разобрать исходник в однородный список объектов с текстом и позицией. |
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
token_get_allРазобрать PHP-текст на массив токенов, не выполняя его.
token_get_all(string $code, int $flags = 0): arrayРазобрать PHP-текст на массив токенов, не выполняя его.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| code | Исходный текст, обычно вместе с открывающим тегом <?php. Без тега текст считается находящимся вне PHP. |
| flags | 0: лексический разбор; TOKEN_PARSE: учитывать синтаксический контекст, в том числе допустимые зарезервированные слова. Некорректная грамматика в этом режиме вызывает ParseError. |
| Результат | Массив. Элемент — либо строка с отдельным знаком, либо [идентификатор, исходный текст, номер начальной строки]. Строки считаются с 1. |
| Границы | Это не AST, не выполнение и не проверка безопасности программы. Сканер использует грамматику установленной версии PHP. |
- <?php echo 7; — входная строка
- [T_OPEN_TAG, "<?php ", 1]
- [T_ECHO, "echo", 1], пробел, число
- Знак ; остаётся отдельной строкой
<?php
declare(strict_types=1);
$tokens = token_get_all('<?php echo 7;');
foreach ($tokens as $token) {
echo is_array($token) ? token_name($token[0]) : $token;
echo "\n";
}
Ожидаемый результат
T_OPEN_TAG
T_ECHO
T_WHITESPACE
T_LNUMBER
;
Сначала проверяем is_array: обращение к [0] имеет разный смысл у массива и строки. Имена T_* позволяют читать результат без запоминания числовых кодов. echo внутри анализируемой строки не выполнялся.
<?php
declare(strict_types=1);
$template = '<h1>Shop</h1><?php echo 7; ?>Done';
foreach (token_get_all($template) as $token) {
if (is_array($token) && $token[0] === T_INLINE_HTML) {
echo $token[1], "\n";
}
}
Ожидаемый результат
<h1>Shop</h1>
Done
Так редактор шаблона отделяет HTML-части от PHP. Это не HTML-парсер: найденный текст ещё можно разобрать соответствующим инструментом. При показе исходника на сайте его экранируют, иначе браузер воспримет теги.
<?php
declare(strict_types=1);
$source = '<?php class Delivery { const PUBLIC = 1; }';
foreach ([0, TOKEN_PARSE] as $flags) {
foreach (token_get_all($source, $flags) as $token) {
if (is_array($token) && $token[1] === 'PUBLIC') {
echo token_name($token[0]), "\n";
}
}
}
Ожидаемый результат
T_PUBLIC
T_STRING
PUBLIC здесь имя константы, хотя отдельно public является ключевым словом. Грамматический контекст помогает понять разрешённую роль слова; сам текст остаётся прежним.
Частая ошибка — передать /* note */ без открывающего тега и ждать T_COMMENT: получится T_INLINE_HTML. Пробел или перевод строки после открывающего тега может входить в текст T_OPEN_TAG. Нельзя восстанавливать исходник по одним именам токенов: нужны их исходные тексты. Для больших файлов список занимает дополнительную память; ограничьте размер анализируемых входов.
Связанные инструменты: PhpToken::tokenize; token_name.
token_nameПолучить понятное имя T_* по числовому идентификатору.
token_name(int $id): stringПолучить понятное имя T_* по числовому идентификатору.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| id | Числовой ID из токена или именованная константа вроде T_VARIABLE. Не сохраняйте «магическое» число из чужой версии PHP. |
| Результат | Имя известного токена. Для неизвестного ID возвращается строка UNKNOWN, а не false/null. |
| Обычные знаки | У ; или + нет такого T_*-имени. Эта функция не заменяет chr; объектный getTokenName умеет вернуть одиночный знак. |
- ID токена T_VARIABLE
- token_name(ID)
- Строка T_VARIABLE
- В отчёте видно назначение элемента
<?php
declare(strict_types=1);
echo token_name(T_VARIABLE), "\n";
echo token_name(-1), "\n";
Ожидаемый результат
T_VARIABLE
UNKNOWN
UNKNOWN — нормальный ответ для неизвестного числа. Значение ID не обязано сохраняться между версиями PHP, поэтому в правилах анализатора используют T_VARIABLE, а в переносимом отчёте сохраняют его имя.
<?php
declare(strict_types=1);
$source = "<?php\necho 7;\nprint 8;";
foreach (token_get_all($source) as $token) {
if (is_array($token) && in_array($token[0], [T_ECHO, T_PRINT], true)) {
echo 'line ', $token[2], ': ', token_name($token[0]), "\n";
}
}
Ожидаемый результат
line 2: T_ECHO
line 3: T_PRINT
Так можно составить список мест, где шаблон пишет ответ. Наличие echo не является ошибкой само по себе: правило зависит от роли файла. Лексер не знает, должен ли данный файл быть шаблоном или сервисом.
Связанные инструменты: PhpToken::getTokenName; token_get_all.
PhpToken::__constructСоздать один токен вручную для своего преобразователя или отчёта.
final public PhpToken::__construct(int $id, string $text, int $line = -1, int $pos = -1)Создать один токен вручную для своего преобразователя или отчёта.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| id | Именованная константа T_* либо код отдельного знака (например ord(";")). |
| text | Буквальный текст токена. Он не исполняется и не проверяется на соответствие ID. |
| line | Начальная строка с 1, если известна. -1 обозначает неизвестное положение. |
| pos | Смещение в байтах от начала исходника с 0. -1 — неизвестно; это не номер Unicode-символа. |
| Возврат | new возвращает созданный объект. Конструктор final: наследник не может его переопределить. |
- T_VARIABLE задаёт вид
- Текст $price задаёт содержимое
- Строка 2, байт 6 задают положение
- Объект хранит четыре независимых значения
<?php
declare(strict_types=1);
$token = new PhpToken(T_VARIABLE, '$price', 2, 6);
echo $token->getTokenName(), ': ', $token->text, "\n";
echo $token->line, '/', $token->pos, "\n";
Ожидаемый результат
T_VARIABLE: $price
2/6
new создаёт объект; параметры описывают его поля. $price находится внутри одинарных кавычек, поэтому это текст имени, а не чтение переменной программы.
<?php
declare(strict_types=1);
$inserted = [new PhpToken(ord(';'), ';'), new PhpToken(T_WHITESPACE, "\n")];
foreach ($inserted as $token) {
echo $token->text;
}
echo $inserted[0]->line, '/', $inserted[0]->pos, "\n";
Ожидаемый результат
;
-1/-1
У новых элементов нет исходной позиции. Если меняете исходник, прежние позиции остальных токенов автоматически не пересчитываются; после изменения можно снова токенизировать результат.
Конструктор разрешит несогласованные поля, например ID T_ECHO и текст price. Это описание, которое передал автор, а не доказательство корректного синтаксиса. Для чтения настоящего исходника используйте PhpToken::tokenize.
Связанные инструменты: PhpToken::tokenize; PhpToken::getTokenName.
PhpToken::getTokenNameПрочитать имя вида токена либо сам одиночный знак.
public PhpToken::getTokenName(): ?stringПрочитать имя вида токена либо сам одиночный знак.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| Параметры | Нет: метод читает id текущего объекта. |
| Результат | Имя T_*, одиночный символ для ID меньше 256 или null для неизвестного ID от 256. Передавайте настоящие ID сканера: отрицательный вручную заданный ID тоже попадает в байтовую ветку реализации. |
| Отличие от token_name | Процедурная функция отдаёт UNKNOWN для неизвестного ID; этот метод отдаёт null. Текст токена читается отдельно через text. |
- ID T_ECHO → T_ECHO
- ID ord(";") → ;
- Неизвестный ID → null
- text всё ещё содержит конкретный исходный текст
<?php
declare(strict_types=1);
$token = new PhpToken(T_VARIABLE, '$quantity');
echo $token->getTokenName(), "\n", $token->text, "\n";
Ожидаемый результат
T_VARIABLE
$quantity
Первое значение сообщает, что перед нами переменная; второе — какая именно запись встретилась. Это полезно для списка переменных в редакторе.
<?php
declare(strict_types=1);
foreach ([new PhpToken(ord(';'), ';'), new PhpToken(PHP_INT_MAX, '?')] as $token) {
echo $token->getTokenName() ?? 'unknown token', "\n";
}
Ожидаемый результат
;
unknown token
Оператор ?? выбирает подпись, когда слева null. Он не изменяет сам объект и не делает неизвестный элемент корректным PHP-кодом.
Связанные инструменты: token_name; PhpToken::is.
PhpToken::isПроверить вид или точный текст токена, в том числе по нескольким альтернативам.
public PhpToken::is(int|string|array $kind): boolПроверить вид или точный текст токена, в том числе по нескольким альтернативам.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| kind как int | Сравнить с id, например T_FUNCTION. |
| kind как string | Сравнить буквально с text, с учётом регистра. Строка "T_FUNCTION" не обозначает константу. |
| kind как array | Список int/string: достаточно первого совпадения. Пустой список даёт false; неверный тип достигнутого элемента приводит к TypeError. |
| Результат | true/false. Метод ничего не переименовывает и не изменяет. |
- Токен: ID T_ECHO, text ECHO
- is(T_ECHO) → true
- is("echo") → false
- is([T_PRINT, T_ECHO]) → true
<?php
declare(strict_types=1);
$token = PhpToken::tokenize('<?php ECHO 7;')[1];
var_dump($token->is(T_ECHO), $token->is('echo'), $token->is([T_PRINT, T_ECHO]));
Ожидаемый результат
bool(true)
bool(false)
bool(true)
PHP распознаёт ECHO как ключевое слово, но сохраняет исходные заглавные буквы. Проверка вида подходит для анализа; проверка текста — для точной правки.
<?php
declare(strict_types=1);
$source = '<?php interface Clock {} trait Logged {} class Order {}';
foreach (PhpToken::tokenize($source, TOKEN_PARSE) as $token) {
if ($token->is([T_INTERFACE, T_TRAIT, T_CLASS])) {
echo $token->text, "\n";
}
}
Ожидаемый результат
interface
trait
class
Это поиск слов объявления. Для полноценного каталога имён надо учитывать контекст: ::class, анонимные классы, namespace и вложенные конструкции. Одно условие не превращается в полный синтаксический анализатор.
Массив альтернатив должен целиком состоять из int/string. Не полагайтесь на короткое замыкание, при котором ошибочный элемент после совпадения случайно не рассматривается. Валидация собственного набора правил выполняется при его создании.
Связанные инструменты: PhpToken::getTokenName; PhpToken::isIgnorable.
PhpToken::isIgnorableУзнать, пропускает ли синтаксический анализатор этот вид токена.
public PhpToken::isIgnorable(): boolУзнать, пропускает ли синтаксический анализатор этот вид токена.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| Параметры | Нет. Решение принимается по id. |
| true | T_WHITESPACE, T_COMMENT, T_DOC_COMMENT и T_OPEN_TAG в PHP 8.5.11. |
| false | В том числе T_CLOSE_TAG, T_OPEN_TAG_WITH_ECHO и T_INLINE_HTML: эти элементы имеют иной смысл. |
| Ограничение | «Пропускается парсером» не означает «можно безопасно вырезать из файла». Пробел разделяет слова; открывающий тег включает PHP; PHPDoc могут читать инструменты. |
- Сканер сохранил тег, комментарий и пробелы
- isIgnorable задаёт фильтр отчёта
- echo, число и ; остаются видимыми
- Оригинальный файл сохраняем целиком
<?php
declare(strict_types=1);
foreach (PhpToken::tokenize('<?php /* price */ echo 7;') as $token) {
if (!$token->isIgnorable()) { echo $token->text, "\n"; }
}
Ожидаемый результат
echo
7
;
Такой список помогает объяснить структуру выражения. Его нельзя склеивать и использовать как исходный файл: без тега PHP и разделителей поведение изменится.
<?php
declare(strict_types=1);
$source = '<?php /** Price in kopecks. */ function price() { return 7; }';
$documents = 0;
foreach (PhpToken::tokenize($source) as $token) {
if ($token->is(T_DOC_COMMENT)) { $documents++; }
}
echo 'documentation blocks: ', $documents, "\n";
var_dump((new PhpToken(T_OPEN_TAG, '<?php '))->isIgnorable());
Ожидаемый результат
documentation blocks: 1
bool(true)
Документационные комментарии нужны IDE, генераторам справки и некоторым библиотекам. Их удаление — отдельное решение для конкретного проекта, а не автоматическая оптимизация.
Связанные инструменты: PhpToken::is; PhpToken::__toString.
PhpToken::__toStringПолучить исходный текст объекта при преобразовании в строку.
public PhpToken::__toString(): stringПолучить исходный текст объекта при преобразовании в строку.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| Вызов | Обычно (string) $token, echo $token или конкатенация; напрямую вызывать метод обычно незачем. |
| Результат | Содержимое публичного свойства text. Ничего не исполняется, не переводится в HTML и не очищается. |
| Stringable | PhpToken реализует Stringable. Возможность получить строку не означает автоматическое принятие объекта всеми строгими string-параметрами; явное приведение убирает двусмысленность. |
- Объекты сохранили каждый кусочек текста
- (string) $token читает text
- Склеиваем в первоначальном порядке
- Получаем ту же строку исходного кода
<?php
declare(strict_types=1);
$token = new PhpToken(T_VARIABLE, '$total');
echo (string) $token, "\n";
Ожидаемый результат
$total
Выводится запись $total, а не значение переменной с таким именем. Токен не ссылается на переменные исполняющей программы.
<?php
declare(strict_types=1);
$source = "<h1>Shop</h1><?php\n// note\necho 7; ?>";
$restored = '';
foreach (PhpToken::tokenize($source) as $token) { $restored .= (string) $token; }
var_dump($restored === $source);
Ожидаемый результат
bool(true)
Сохраняются не только команды, но и оформление. Позиции имеют смысл относительно этой исходной строки. Для подсветки на веб-странице дополнительно используйте htmlspecialchars, чтобы исходник не стал исполняемой HTML-разметкой.
Связанные инструменты: PhpToken::tokenize; PhpToken::isIgnorable.
PhpToken::tokenizeРазобрать исходник в однородный список объектов с текстом и позицией.
public static PhpToken::tokenize(string $code, int $flags = 0): arrayРазобрать исходник в однородный список объектов с текстом и позицией.
Токен — кусочек исходного текста с назначением: имя переменной, число, слово echo или знак ;. Лексический анализатор разбивает текст, но не запускает этот код. В примерах строка '<?php echo 7;' — данные для анализатора. Само число 7 он не выводит. $tokens хранит полученный список; foreach по очереди рассматривает его элементы.
Вариант с объектами хранит каждый кусочек в PhpToken: ->text читает текст, ->is(...) задаёт вопрос о его виде. ::tokenize(...) вызывается у класса и создаёт список объектов. Основа синтаксиса: переменные, массивы, перебор, классы и объекты.
| Часть вызова | Что она означает |
|---|---|
| code | Исходный PHP/HTML-текст, включая теги. Без <?php анализ начинается вне PHP. |
| flags | 0 либо TOKEN_PARSE для учёта грамматики. При синтаксической ошибке TOKEN_PARSE может выбросить ParseError. |
| Результат | Список PhpToken; при вызове у наследника — объекты этого наследника (static[]). Все элементы имеют id, text, line и pos, в том числе знаки пунктуации. |
| Стоимость | Весь список хранится в памяти. Не загружайте неограниченные присланные файлы; лексер не анализирует семантику приложения. |
- Вход <?php $price = 7;
- Токен $price имеет T_VARIABLE
- line = 1, pos = 6
- Редактор отмечает байты этой переменной
<?php
declare(strict_types=1);
foreach (PhpToken::tokenize('<?php $price = 7;') as $token) {
if ($token->is(T_VARIABLE)) {
echo $token->text, ': line ', $token->line, ', byte ', $token->pos, "\n";
}
}
Ожидаемый результат
$price: line 1, byte 6
pos начинается с нуля и измеряется байтами. После русских букв он не совпадёт с номером видимого символа. При работе с редактором надо явно согласовать систему координат.
<?php
declare(strict_types=1);
try {
PhpToken::tokenize('<?php if (', TOKEN_PARSE);
echo "accepted\n";
} catch (ParseError $error) {
echo "syntax error\n";
}
Ожидаемый результат
syntax error
В интерактивном редакторе пользователь часто ещё не дописал строку: это ожидаемый случай. Режим без TOKEN_PARSE полезен для промежуточного лексического разбора, но тоже не стоит считать универсальным валидатором.
<?php
declare(strict_types=1);
class ProjectToken extends PhpToken {
public function isOutput(): bool { return $this->is([T_ECHO, T_PRINT]); }
}
$count = 0;
foreach (ProjectToken::tokenize('<?php echo 7; print 8;') as $token) {
if ($token->isOutput()) { $count++; }
}
echo $count, "\n";
Ожидаемый результат
2
Объекты результата — ProjectToken. Метод прячет повторяющийся вопрос в одном месте. Конструктор PhpToken final; дополнительные свойства наследника требуют понятных значений по умолчанию, а не расчёта в переопределённом конструкторе.
T_STRING — вид идентификатора, а не гарантия, что найден вызов функции. Класс, имя метода, константа и другие конструкции тоже могут иметь такой токен. Для разрешения имён, типов и безопасного изменения программы нужен анализ контекста, а для сложных преобразований — AST и соответствующая библиотека.
Связанные инструменты: token_get_all; PhpToken::__construct; PhpToken::is.