Работа в PHP с Tokenizer

в 10:43, , рубрики: php, Песочница, метки:

Для справки:
Tokenizer (лексер) предоставляет интерфейс для анализа кода. Таким образом, можно писать утилиты без необходимости работы с языковой спецификацией.
Tokenizer, начиная с версии php >= 4.3 включен в сборку php по-умолчанию.

Какие задачи можно решать с помощью tokenizr'а?
Да самые разные, связанные с анализом и модификацией кода.

Удаление комментариев из кода

Самый простой пример приведен на php.net — по удалению комментариев:

<?php
function strip_comments($fileName)
{
    $source = file_get_contents($fileName);
    // получаем все метки файла
    $tokens = token_get_all($source);
    $result = '';

    foreach ($tokens as $token) {
        if (!is_array($token)) {
            // простая 1-буквенная лексема
            $result .= $token;
        } else {
            // токен-массив
            list($id, $value) = $token;

            switch ($id) {
                case T_COMMENT:
                case T_DOC_COMMENT:
                    // комментарии пропускаем
                    break;

                default:
                    // все остальное -> оставляем "как есть"
                    $result .= $value;
                    break;
            }
        }
    }
    return $result;
}
?>

Как видно из кода — мы получаем массив токенов и, в зависимости от их типа, оставляем или пропускаем.
Так можно решить задачки и поинтереснее — например на основе php-файлов сгенерировать карту классов проекта для автозагрузки.

Получение списка классов из файла

Для получения списка классов из файла я написал вот такую функцию:

<?php
function getClasses($fileName)
{
    $result = array();
    $content = file_get_contents($fileName);
    $tokens = token_get_all($content);
    $waitingClassName = false;
    $waitingNamespace = false;
    $waitingNamespaceSeparator = false;
    $namespace = array();
    for ($i = 0, $c = count($tokens); $i < $c; $i++) {
        if (is_array($tokens[$i])) {
            list($id, $value) = $tokens[$i];
            switch ($id) {
                case T_NAMESPACE:
                    $waitingNamespace = true;
                    $waitingNamespaceSeparator = false;
                    $namespace = array();
                    break;
                case T_CLASS:
                case T_INTERFACE:
                    $waitingClassName = true;
                    break;
                case T_STRING:
                    if ($waitingNamespace) {
                        $namespace[] = $value;
                        $waitingNamespace = false;
                        $waitingNamespaceSeparator = true;
                    } elseif ($waitingClassName) {
                        if (!empty($namespace)) {
                            $value = sprintf('%s\%s', implode('\', $namespace), $value);
                        }
                        $result[] = $value;
                        $waitingClassName = false;
                    }
                    break;
                case T_NS_SEPARATOR:
                    if ($waitingNamespaceSeparator && !$waitingNamespace && !empty($namespace)) {
                        $waitingNamespace = true;
                        $waitingNamespaceSeparator = false;
                    }
                    break;
            }
        } else {
            if (($waitingNamespace || $waitingNamespaceSeparator) && ($tokens[$i] == '{' || $tokens[$i] == ';')) {
                $waitingNamespace = false;
                $waitingNamespaceSeparator = false;
            }
        }
    }
    return $result;
}
?>

А потом подумал и написал небольшую утилиту, которая генерирует автозагрузчик на основании файлов проекта.
Она анализирует в указанной папке все файлы с расширением "*.php" и строит карту классов (с учетом неймспейсов, конечно), на основании которой потом генерируется автозагрузчик.
Найти ее можно на github.com

Отключение и переопределение стандартных функций

На днях вспомнил, как когда-то ковырялся с расширением runkit. Из его возможностей меня особенно заинтересовало переопределение стандартных функций и песочница, в которой можно было бы запретить использование тех или иных функций.
И сейчас я подумал, а можно ли реализовать подобный функционал, без использования этого расширения. Оказалось, что tokenizer вполне может помочь в этом деле.
Так родилась библиотечка Runtime, с помощью которой, можно во время выполнения скрипта запретить использование любых стандартных функций, или переопределить их.
Приведу примеры работы:

<?php
use DmRuntime;

$code = <<<CODE
<?php
echo str_replace( 0, 1, 100 );
?>
CODE;

// Вывалит Exception, с в котором сообщается, что использование str_replace запрещено
Runtime::code($code)
    ->disableFunction('str_replace')
    ->execute();
?>
<?php
use DmRuntime;

$code = <<<CODE
<?php
echo str_replace( 0, 1, 100 );
?>
CODE;

// Выведет 000, вместо 111
Runtime::code($code)
    ->overrideFunction('str_replace', function ($search, $replace, $subject) {
        //  меняем 1ый и 2й аргументы местами
        echo str_replace($replace, $search, $subject);
    })
    ->execute();
?>

Как использовать эти возможности — дело личное. Но использовать это нужно аккуратно.
Я провел небольшое исследование и остался доволен результатом.
Касательно runtime — сложно сказать, где его можно применить, а где нет. Но сама библиотека наглядно демонстрирует работу tokenizer'а и его возможностей.

Ссылки

  1. Tokenizer
  2. Runkit
  3. Генерация автозагрузчика на github.com
  4. Библиотечка Runtime на github.com

Автор: dmkuznetsov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js