Часть 1

Когда я впервые наткнулся на термин «итерация» и увидел огромное количество классов, связанных с этим термином, я опешил. Мне показалось, что всё это слишком сложно, чтобы разобраться. Но совсем скоро я понял, что «итерация» — это всего лишь умное словечко, чтобы описать то, что программисты используют каждый день.

Если вы PHP-разработчик, вы однозначно пользовались массивами. А если вы пользовались массивами, то ещё более однозначно, что вы выполняли перебор их элементов в циклах. Взгляните на любой кусок кода и с огромной долей вероятности вы увидите там цикл foreach. Так вот, итерация — это всего лишь процесс обхода списка значений, а итератор — это объект, который выполняет сам процесс, будь то обход массива, списка файлов или даже результатов выборки из таблицы БД.

Это первая часть и двухсерийной статьи. В ней я расскажу вам об итерации и о преимуществах использования некоторых классов Standard PHP Library (SPL). В SPL присутствует огромное количество итераторов и их использование во многих случаях может сделать ваш код эффективнее и читабельнее.

Где и когда использовать итераторы SPL

Как вы скоро увидите, работа с итераторами примерно напоминает работу с массивами и отчасти по этой причине многие люди задаются вопросом: зачем вообще нужны эти итераторы, если есть массивы? Реальную выгоду от использования итераторов можно получить в случаях, когда нужно итерировать большие объёмы данных, а также данные, которые по своей структуре сложнее обычного массива.

Цикл foreach создаёт копию полученного массива. Если вы имеете дело с большими объёмами данных, такой подход не годится по очевидной причине: снижение производительности. Итератор SPL работает по другому: он обрабатывает один элемент итерируемого списка за раз, делая это куда более эффективно, нежели foreach.

При создании поставщиков данных (data providers) итераторы помогают сделать их более эффективными, предлагаю возможности ленивой загрузки (lazy loading). «Ленивая загрузка» означает то, что фактическое получение данных из источника выполняет только тогда, когда эти данные нужны. Помимо прочего, вы получаете возможность трансформации данных перед тем, как отдавать их клиенту объекта.

Однако ответ на вопрос «использовать итераторы или нет» остаётся полностью результатом вашего выбора. Итераторы имею массу полезных свойств, но в некоторых случаях (в частности, с маленькими объёмами данных) затраты на их применение будут неоправданными. В любом случае, выбор остаётся за вами. Всегда старайтесь рассмотреть максимальное количество факторов, прежде чем принимать решение.

Итерация массивов

Первый итератор, который я вам хочу представить — это ArrayIterator. Его конструктор принимает массив в качестве параметра, а сам объект предоставляет методы для выполнения итерационных операций. Вот пример работы с ArrayIterator:

<?php
// Сннтаксис создания массива, введённый в PHP 5.4
$arr = ["sitepoint", "phpmaster", "buildmobile", "rubysource",
    "designfestival", "cloudspring"];
 
// Создаём новый итератор ArrayIterator на основе массива
$iter = new ArrayIterator($arr);
 
// Итерируем
foreach($iter as $key => $value) {
    echo $key . ":  " . $value . "<br>";
}

В результате получим следующий вывод:

0: sitepoint
1: phpmaster
2: buildmobile
3: rubysource
4: designfestival
5: cloudspring

Вообще. обычно вы будете использовать ArrayObject в таких случаях, вместо непосредственного использовани ArrayIterator, поскольку первый позволяет работать с объектом как с массивом в определённых контекстах. ArrayObject создаёт ArrayIterator автоматически в случаях, когда вы используете ArrayObject в цикле foreach или вызываете ArrayObject::getIterator().

Имейте ввиду, что хотя ArrayObject и ArrayIterator ведут себя подобно массивам в контексте foreach, они всё-таки являются объектами. И если вы попытаетесь использовать с ними встроенные функции PHP вроде sort() and array_keys(), вы получите ошибку.

ArrayIterator прост в использовании, однако его возможности ограничены одномерными массивами. В случаях, когда вы имеете дело в многомерными массивами и вам необходимо обходить их рекурсивно, вам пригодится RecursiveArrayIterator.

Традиционным решением для обхода многомерных массивов можно считать вложенные циклы. Например:

<?php
// Многомерный массив
$arr = [
    ["sitepoint", "phpmaster"],
    ["buildmobile", "rubysource"],
    ["designfestival", "cloudspring"],
    "not an array"
];
 
// Первый уровень
foreach($arr as $key => $value) {
    // Второй уровень
    if (is_array($value)) {
        foreach ($value as $k => $v) {
            echo $k . ": " . $v . "<br>";
        }
    }
    else {
        echo $key . ": " . $value . "<br>";
    }
}

Результатом работы приведённого кода будет:

0: sitepoint
1: phpmaster
0: buildmobile
1: rubysource
0: designfestival
1: cloudspring
3: not an array

С использованием RecursiveArrayIterator можно добиться более элегантного решения:

<?php
...
$iter = new RecursiveArrayIterator($arr);
 
// Итерируем объект.
// Для этого нам понадобится экземпляр RecursiveIteratorIterator
foreach(new RecursiveIteratorIterator($iter) as $key => $value) {
    echo $key . ": " . $value . "<br>";
}

Вывод этого кода будет таким же, как и в предыдущем случае.

Обратите внимание на создание экземпляра RecursiveIteratorIterator и передачу ему объекта RecursiveArrayIterator. В противном случае вы получите только значения массива первого уровня и ряд уведомлений об ошибках.

Вам следует использовать RecursiveArrayIterator в случаях, когда имеете дело с многомерными массивами, поскольку он отлично справляется с рекурсивным обходом вложенных массивов. Однако, если в процессе обхода массива ему встретится итерируемый объект, то его обход — это уже ваша задача. Вот для этого случая и предназначен RecursiveIteratorIterator, который по сути является декоратором. В качестве аргумента он получает RecursiveArrayIterator, итерирует его, а также любой объект, реализующий интерфейс Iterable. Для того, чтобы в процессе итерации знать, на какой глубине в данный момент находится итератор, можно использовать метод RecursiveIteratorIterator::getDepth(). Будьте внимательны, работая с RecursiveArrayIterator и RecursiveIteratorIterator, когда имеете дело с объектами. Любой объект, реализующий Iterable, будет итерироваться тоже.

Итерация каталога файловой системы

Несомненно, любой программист время от времени сталкивается с необходимостью получения списка файлов в каталоге. Существует множество способов сделать это, используя, например, встроенные PHP-функции scandir() или glob(). Помимо них, вы также можете воспользоваться классом DirectoryIterator. Он довольно мощный сам по себе, но в случае необходимости вы можете его наследовать и расширять по своему усмотрению. Рассмотрим небольшой пример:

<?php
$dir = new DirectoryIterator("/my/directory/path");
foreach ($dir as $item)
    echo $item . "<br>";

Неплохо для двух строчек кода? И не забывайте, что вдобавок вы теперь можете пользоваться исключениями:

<?php
try {
    $dir = new DirectoryIterator("/non/existent/path");
    foreach ($dir as $item) {
        echo $item . "<br>";
    }
}
catch (Exception $e) {
    echo get_class($e) . ": " . $e->getMessage();
}
UnexpectedValueException: DirectoryIterator::__construct(/non/existent/path,
/non/existent/path): The system cannot find the file specified. (code: 2)

Используя множество полезных методов вроде DirectoryIterator::isDot(), DirectoryIterator::getType() и DirectoryIterator::getSize(), вы можете получить практически всю информацию, которая только потребуется.

Если пойти дальше, то вы можете воспользоваться DirectoryIterator в связке с FilterIterator или RegexIterator, чтобы фильтровать результаты так. как вам нужно. Например:

<?php
class FileExtensionFilter extends FilterIterator
{
    // Разрешённые исключения
    protected $ext = ["php", "txt"];
 
    // Реализация абстрактного метода
    public function accept() {
        return in_array($this->getExtension(), $this->ext);
    }
}
 
// Создаём новый итератор
$dir = new FileExtensionFilter(new DirectoryIterator("./"));

SPL также предлагает RecursiveDirectoryIterator, который может быть использовать в точности, как и RecursiveArrayIterator. В отличие от встроенных функций, RecursiveDirectoryIterator делает множество дополнительной работы за вас, что позволяет создавать более чистый и понятный код. Заранее хочу вас предупредить об одной особенности: RecursiveDirectoryIterator не возвращает пустых каталогов. Даже если каталог содержит вложенные подкаталоги, но ни в одном из них не будет файла, то такой каталог считается пустым и не будет возвращён (в точности так же ведёт себя Git).

<?php
$iter = new RecursiveDirectoryIterator("/my/directory/path");
 
// Обходим содержимое каталога.
// Для этого нужен экземпляр RecursiveIteratorIterator
foreach (new RecursiveIteratorIterator($iter) as $item) {
    echo $item . "<br>";
}

Итоги

Надеюсь, те из вас, кому итераторы казались сложным и непонятным зверем, как это было в моём случае, теперь поняли, что на самом деле сложности никакой нет, более того, итераторы — это то, чем вы пользовались и будете пользоваться каждый день. В этой статье я немного рассказал вам о классах SPL, которые являются удобным и надёжным инструментом. Разумеется, то, что я показал — это лишь капля в море классов-итераторов, предоставляемых PHP SPL.

SPL — это «стандартная» библиотека. Иной раз вам может оказаться недостаточно функциональности отдельных её классов. В таких случаях вы всегда можете расширять эти классы и наделять их необходимыми вам функциями. В следующей статье я расскажу вам об использовании SPL-интерфейсов при создании ваших собственных классов, которые могут итерироваться так же. как и обычные массивы.

 

Часть 2

В реальной жизни вы можете столкнуться с ситуацией, когда функциональности SPL-итераторов вам может не хватить и вам потребуется создать свой собственный класс-итератор. К счастью, сложностей здесь никаких не существует, благодаря интерфейсам SPL.

Для того, чтобы объект мог быть итерирован в цикле foreach, для PHP необходимо, чтобы этот объект реализовывал интерфейс Traversable. Однако, вы не можете реализовывать этот интерфейс непосредственно, и вместо этого должны использовать любой из двух дочерних интерфейсов: Iterator или IteratorAggregate.

Iterator

Интерфейс Iterator предназначен для реализации классами, объекты которых могут быть интерированы непосредственно или использоваться для создания внешних итераторов. Интерфейс определяет пять методов для реализации: rewind(), current(), key(), next() и valid().

Предположим, у нас есть библиотека книг и мы хотим иметь возможность выполнять итерацию по всей коллекции книг. Ниже приведён пример класса, реализующего Iterator, который решает подобную задачу:

<?php
class Library implements Iterator
{
    // Внутренний указатель на текущую позицию указателя в коллекции
    protected $position = 0;
 
    // Данные
    protected $books = [
        "Professional PHP Programming",
        "Programming Perl",
        "A Byte of Python",
        "The Ruby Way"
    ];
 
    // Этот метод устанавливает внутренний указатель 
    // на начало коллекции и используется для начала итерации
    public function rewind() {
        echo "rewinding <br>";
        $this->position = 0;
    }
 
    // Этот метод возвращает текущее значение указателя
    public function current() {
        echo "current <br>";
        return $this->books[$this->position];
    }
 
    // Этот метод возвращает текущую позицию указателя
    public function key() {
        echo "key <br>";
        return $this->position;
    }
 
    // Этот метод сдвигает указатель на следующий элемент
    public function next() {
        echo "next <br>";
        ++$this->position;
    }
 
    // Этот метод проверяет, есть ли данные относительно 
	// текущей позиции указателя
    public function valid() {
        echo "valid <br>";
        return isset($this->books[$this->position]);
    }
}
 
$library = new Library();
foreach ($library as $key => $value) {
    echo $key . ": " . $value . "<br>";
}

Вывод кода будет следующим:

rewinding 
valid 
current 
key 
0: Professional PHP Programming
next 
valid 
current 
key 
1: Programming Perl
next 
valid 
current 
key 
2: A Byte of Python
next 
valid 
current 
key 
3: The Ruby Way
next 
valid

Перед началом итерации PHP вызывает метод rewind(), чтобы установить указатель на начало данных. Затем при помощи вызова метода valid() он проверяет, есть ли данные по текущей позиции указателя. Если в ответ на вызов этого метода PHP получает true, то следующим шагом будет вызван метод current(), который вернёт данные, находящие относительно текущей позиции указателя. Если вы запросите ключ в конструкции foreach, то интерпретатор будет обращаться к методу key() вашего класса, чтобы получить его. Таким образом, итерация будет продолжнать до тех пор, пока объект не вернёт false в ответ на вызов метода valid().

Iterator позволяет создавать итераторы «с нуля» и отлично подходит для решения задач, вроде той, что мы только что рассмотрели. Конечно, приведённый пример очень простой, но его задачей как раз и является в простой и наглядной форме показать вам основные принципы работы класса Iterator. Однако, более лучшим решением для работы с данными вроде тех, что приведены в примере выше (обычный массив), будет другой класс — IteratorAggregate.

IteratorAggregate

IteratorAggregate требует реализации лишь одного метода, getIterator(). Этот метод должен возвращать внешний итератор, который и будет использоваться для выполнения итераций. Переписав наш пример с использованием IteratorAggregate, получим следующее:

<?php
class Library implements IteratorAggregate {
    protected $books = [
        "Professional PHP Programming",
        "Programming Perl",
        "A Byte of Python",
        "The Ruby Way"
    ];
 
    // Возвращаем Iterator
    public function getIterator() {
        echo "getIterator <br>";
        return new ArrayIterator($this->books);
    }
}
 
$library = new Library();
foreach($library as $key => $value) {
    echo $key . ": " . $value . "<br>";
}

В результате выполнения получим:

getIterator 
0: Professional PHP Programming
1: Programming Perl
2: A Byte of Python
3: The Ruby Way

Перед началом итераций интерпретатор PHP вызывает метод getIterator(), который возвращает Iterator; в нашем случае это ArrayIterator. А дальше работа происходит уже с полученным итератором таким образом, как это описано выше. То есть, вы передаёте свои данные внешнему итератору, он делает всю работу, а вы спокойно курите в сторонке.

Поскольку оба интерфейса выполняют одну и ту же задачу, иногда бывает трудно определиться, что же нужно использовать в конкретной ситуации. Правило большого пальца гласит: используйте Iterator тогда, когда вам необходимо создать сложный итератор «с нуля», с вашим собственным блэкджеком и куртизанками. В остальных случаях не морочьте голову и пользуйтесь готовыми SPL итераторами, заворачивая в них данные и реализуйте интерфейс IteratorAggregate.

Итоги

В этой статье мы рассмотрели два SPL-интерфейса. предназначенных для создания собственных классов-итераторов. Я надеюсь, что вы смогли разобраться, понять всю простоту принципов работы итераторов и вскоре начнёте применять полученные знания в ваших проектах.