Объединение рекурсивных результатов итератора: дети с родителями - PullRequest
5 голосов
/ 01 марта 2009

Я пытаюсь перебрать каталог, который содержит множество файлов PHP, и определить, какие классы определены в каждом файле.

Обратите внимание на следующее:

$php_files_and_content = new PhpFileAndContentIterator($dir);
foreach($php_files_and_content as $filepath => $sourceCode) {
    // echo $filepath, $sourceCode
}

Приведенная выше переменная $php_files_and_content представляет итератор, где ключ - это путь к файлу, а содержимое - это исходный код файла (как будто это не было очевидно из примера).

Затем он передается в другой итератор, который будет соответствовать всем определенным классам в исходном коде, ala:

class DefinedClassDetector extends FilterIterator implements RecursiveIterator {
    public function accept() {
        return $this->hasChildren();
    }

    public function hasChildren() {
        $classes = getDefinedClasses($this->current());
        return !empty($classes);
    }

    public function getChildren() {
        return new RecursiveArrayIterator(getDefinedClasses($this->current()));
    }
}

$defined_classes = new RecursiveIteratorIterator(new DefinedClassDetector($php_files_and_content));

foreach($defined_classes as $index => $class) {
    // print "$index => $class"; outputs:
    // 0 => Class A
    // 1 => Class B
    // 0 => Class C
}

Причина, по которой $index не является числовой последовательной, заключается в том, что во втором файле исходного кода был определен класс C, и, следовательно, возвращаемый массив снова начинается с индекса 0. Это сохраняется в RecursiveIteratorIterator, потому что каждый набор результатов представляет отдельный итератор (и, следовательно, пары ключ / значение).

В любом случае, сейчас я пытаюсь найти наилучший способ объединить их так, чтобы при выполнении итерации по новому итератору я мог получить ключ - имя класса (из итератора $defined_classes) и значение - исходный путь к файлу, ala:

foreach($classes_and_paths as $filepath => $class) {
    // print "$class => $filepath"; outputs
    // Class A => file1.php
    // Class B => file1.php
    // Class C => file2.php
}

И вот где я застрял.

На данный момент единственное решение, которое приходит на ум, - это создать новый RecursiveIterator, который переопределяет метод current () для возврата внешнего итератора key () (который будет исходным filepath) и key () метод для возврата текущего значения iterator (). Но я не одобряю это решение, потому что:

  • Звучит сложно (что означает, что код будет выглядеть отвратительно и не будет интуитивно понятным
  • Бизнес-правила жестко запрограммированы внутри класса, тогда как я хотел бы определить некоторые общие итераторы и иметь возможность комбинировать их таким образом, чтобы получить требуемый результат.

Любые идеи или предложения с благодарностью получены.

Я также понимаю, что есть гораздо более быстрые и эффективные способы сделать это, но это также упражнение по использованию Iterators для себя и упражнение по продвижению повторного использования кода, поэтому любые новые итераторы, которые должны быть написаны, должны быть такими минимально, насколько это возможно, и попробуйте использовать существующие функции.

Спасибо

Ответы [ 2 ]

2 голосов
/ 08 марта 2009

Ладно, думаю, я наконец-то понял это. Вот примерно то, что я сделал в псевдокоде:

Шаг 1 Нам нужно составить список содержимого каталога, таким образом мы можем выполнить следующее:

// Reads through the $dir directory
// traversing children, and returns all contents
$dirIterator = new RecursiveDirectoryIterator($dir);

// Flattens the recursive iterator into a single
// dimension, so it doesn't need recursive loops
$dirContents = new RecursiveIteratorIterator($dirIterator);

Шаг 2 Нам нужно рассмотреть только файлы PHP

class PhpFileIteratorFilter {
    public function accept() {
        $current = $this->current();
        return    $current instanceof SplFileInfo
               && $current->isFile()
               && end(explode('.', $current->getBasename())) == 'php';
    }
}


// Extends FilterIterator, and accepts only .php files
$php_files = new PhpFileIteratorFilter($dirContents);

PhpFileIteratorFilter не очень подходит для повторного использования кода. Лучшим способом было бы иметь возможность предоставить расширение файла как часть конструкции и получить фильтр, соответствующий этому. Несмотря на это, я пытаюсь отойти от аргументов конструирования, где они не требуются, и больше полагаться на композицию, потому что это лучше использует шаблон стратегии. PhpFileIteratorFilter мог бы просто использовать универсальный FileExtensionIteratorFilter и установить себя полностью.

Шаг 3 Теперь мы должны прочитать содержимое файла

class SplFileInfoReader extends FilterIterator {

    public function accept() {
        // make sure we use parent, this one returns the contents
        $current = parent::current();
        return    $current instanceof SplFileInfo
               && $current->isFile()
               && $current->isReadable();
    }

    public function key() {
        return parent::current()->getRealpath();
    }

    public function current() {
        return file_get_contents($this->key());
    }    
}

// Reads the file contents of the .php files
// the key is the file path, the value is the file contents
$files_and_content = new SplFileInfoReader($php_files);

Шаг 4 Теперь мы хотим применить наш обратный вызов к каждому элементу (содержимому файла) и каким-то образом сохранить результаты. Опять же, пытаясь использовать шаблон стратегии, я убрал ненужные аргументы конструктора, например, $preserveKeys или аналогичный

/**
 * Applies $callback to each element, and only accepts values that have children
 */
class ArrayCallbackFilterIterator extends FilterIterator implements RecursiveIterator {

    public function __construct(Iterator $it, $callback) {
        if (!is_callable($callback)) {
            throw new InvalidArgumentException('$callback is not callable');
        }

        $this->callback = $callback;
        parent::__construct($it);
    }

    public function accept() {
        return $this->hasChildren();
    }

    public function hasChildren() {
        $this->results = call_user_func($this->callback, $this->current());
        return is_array($this->results) && !empty($this->results);
    }

    public function getChildren() {
        return new RecursiveArrayIterator($this->results);
    }
}


/**
 * Overrides ArrayCallbackFilterIterator to allow a fixed $key to be returned
 */
class FixedKeyArrayCallbackFilterIterator extends ArrayCallbackFilterIterator {
    public function getChildren() {
        return new RecursiveFixedKeyArrayIterator($this->key(), $this->results);
    }
}


/**
 * Extends RecursiveArrayIterator to allow a fixed $key to be set
 */
class RecursiveFixedKeyArrayIterator extends RecursiveArrayIterator {

    public function __construct($key, $array) {
        $this->key = $key;
        parent::__construct($array);
    }

    public function key() {
        return $this->key;
    }
}

Итак, здесь у меня есть основной итератор, который будет возвращать результаты $callback, которые я передал, но я также расширил его, чтобы создать версию, которая также сохранит ключи, вместо использования аргумента конструктора для это.

И, таким образом, мы имеем это:

// Returns a RecursiveIterator
// key: file path
// value: class name
$class_filter = new FixedKeyArrayCallbackFilterIterator($files_and_content, 'getDefinedClasses');

Шаг 5 Теперь нам нужно отформатировать его подходящим образом. Я хочу, чтобы пути к файлам были значением, а ключи - именем класса (то есть, чтобы обеспечить прямое сопоставление класса с файлом, в котором его можно найти для автозагрузчика)

// Reduce the multi-dimensional iterator into a single dimension
$files_and_classes = new RecursiveIteratorIterator($class_filter);

// Flip it around, so the class names are keys
$classes_and_files = new FlipIterator($files_and_classes);

И вуаля, теперь я могу перебрать $classes_and_files и получить список всех определенных классов в $ dir вместе с файлом, в котором они определены. И почти весь код, используемый для этого, повторно можно использовать и в других контекстах. Я не жестко запрограммировал что-либо в определенном Итераторе, чтобы выполнить эту задачу, а также не выполнял никакой дополнительной обработки вне итераторов

0 голосов
/ 03 марта 2009

Я думаю, что вы хотите сделать, это более или менее поменять местами ключи и значения, возвращенные из PhpFileAndContent. Указанный класс возвращает список filepath => source, и вы хотите сначала обратить отображение таким образом, чтобы оно было source => filepath, а затем развернуть source для каждого класса, определенного в source, так что это будет class1 => filepath, class2 => filepath.

Это должно быть легко, так как в вашем getChildren() вы можете просто получить доступ к $this->key(), чтобы получить текущий путь к файлу для источника, на котором вы работаете getDefinedClasses(). Вы можете написать getDefinedClasses как getDefinedClasses($path, $source), и вместо возврата индексированного массива всех классов будет возвращен словарь, в котором каждое значение из текущего индексированного массива является ключом в словаре, а значение - это путь к файлу, где этот класс был определен.

Тогда оно выйдет так, как вы хотите.

Другой вариант - отказаться от использования RecursiveArrayIterator и вместо этого написать собственный итератор, который инициализирован (в getChildren) как

return new FilePathMapperIterator($this->key,getDefinedClasses($this->current()));

и затем FilePathMapperIterator преобразует массив классов из getDefinedClasses в отображение class => filepath, которое я описал, просто перебирая массив и возвращая текущий класс в key() и всегда возвращая указанный путь к файлу в current() .

Я думаю, что последний более крутой, но определенно больше кода, поэтому маловероятно, что я бы пошел по этому пути, если бы мог адаптировать getDefinedClasses() для своих нужд.

...