Итераторы в PHP: практическое руководство по коллекциям и foreach

Важно: iterator и iterable — разные вещи. Iterator описывает поведение итерации, iterable — тип, принимающий массивы и Traversable.
Быстрые ссылки
- Простая итерация
- Проблема коллекций
- Реализация Iterator
- IteratorAggregate
- Встроенные итераторы PHP
- Тип iterable
- Когда не использовать итераторы
- Чеклист, тесты и критерии приёмки
- Краткое резюме
Простая итерация
Чтобы пройти по массиву в PHP, используйте конструкцию foreach:
foreach (["1", "2", "3"] as $i) {
echo ($i . " ");
}Результат:
1 2 3Также можно итерировать объект с публичными свойствами:
$cls = new StdClass();
$cls->foo = "bar";
foreach ($cls as $i) {
echo $i;
}Результат:
barКлючевая мысль: foreach работает не только с массивами, но и с объектами, если они реализуют интерфейс Traversable (через Iterator или IteratorAggregate) или являются простыми объектами с публичными свойствами.
Проблема коллекций
Для классов с публичными свойствами foreach работает “из коробки”. Но часто нужны коллекции, которые гарантируют, что они содержат только объекты одного типа, и предоставляют удобные методы, например containsAnAdmin.
Пример коллекции:
class UserCollection {
protected array $items = [];
public function add(UserDomain $user) : void {
$this->items[] = $user;
}
public function containsAnAdmin() : bool {
return count(array_filter(
$this->items,
fn (UserDomain $i) : bool => $i->isAdmin()
)) > 0;
}
}Если вы попытаетесь итерировать экземпляр UserCollection, по умолчанию PHP попытается пройтись по публичным свойствам объекта, а не по логическому массиву $items. Для корректной итерации коллекции нужно добавить поведение итератора.
Реализация Iterator
Интерфейс Iterator даёт полный контроль над тем, как объект ведёт себя в foreach. Он определяет пять методов:
- current() : mixed — вернуть текущий элемент.
- key() : scalar — вернуть ключ текущей позиции.
- next() : void — перейти к следующей позиции.
- rewind() : void — сбросить позицию к началу.
- valid() : bool — вернуть, валидна ли текущая позиция.
Пример реализации:
class DemoIterator implements Iterator {
protected int $position = 0;
protected array $items = ["cloud", "savvy"];
public function rewind() : void {
echo "Rewinding\n";
$this->position = 0;
}
public function current() : string {
echo "Current\n";
return $this->items[$this->position];
}
public function key() : int {
echo "Key\n";
return $this->position;
}
public function next() : void {
echo "Next\n";
++$this->position;
}
public function valid() : bool {
echo "Valid\n";
return isset($this->items[$this->position]);
}
}При итерировании:
$i = new DemoIterator();
foreach ($i as $key => $value) {
echo "$key $value\n";
}Вывод будет напоминать состояние цикла: rewind -> valid -> current/key -> next -> valid и т.д.
Пояснение: PHP вызывает rewind() в начале, затем проверяет valid(). Если valid() возвращает true, вызываются current() и key(), затем next(), и цикл повторяется.
Советы по реализации:
- Поддерживайте минимальную и понятную ответственность — итератор должен только управлять порядком обхода.
- Не храните большие побочные состояния в итераторе; он должен быть лёгким и предсказуемым.
- valid() должен однозначно возвращать булево значение.
IteratorAggregate
Часто писать Iterator вручную утомительно: шаблон всегда одинаков. Интерфейс IteratorAggregate позволяет вернуть любой Traversable из метода getIterator(), и PHP будет использовать его при foreach.
Пример коллекции с IteratorAggregate:
class UserCollection implements IteratorAggregate {
protected array $items = [];
public function add(UserDomain $user) : void {
$this->items[] = $user;
}
public function getIterator() : Traversable {
return new ArrayIterator($this->items);
}
}
$users = new UserCollection();
$users->add(new UserDomain("James"));
$users->add(new UserDomain("Demo"));
foreach ($users as $user) {
echo $user->Name . "\n";
}Как видно, всего три строки — свойство для хранения, метод add, и getIterator, возвращающий ArrayIterator. Это наиболее распространённый подход для коллекций.
Встроенные итераторы SPL
SPL предоставляет ряд готовых итераторов, которые решают распространённые задачи без ручной реализации интерфейсов. Частые кандидаты:
- ArrayIterator — превращает массив в Iterator.
- LimitIterator — итерация по подмножности элементов.
- InfiniteIterator — зацикливает итератор (не завершается сам по себе).
- FilterIterator — абстрактный класс для фильтрации элементов.
- DirectoryIterator, FilesystemIterator, GlobIterator — итераторы для файловой системы.
LimitIterator
Позволяет пройти по подмножеству другого итератора без splice и ручной логики:
$arr = new ArrayIterator(["a", "b", "c", "d"]);
foreach (new LimitIterator($arr, 0, 2) as $val) {
echo $val . " ";
}Вывод:
a bОбратите внимание: LimitIterator принимает Iterator, а не массив.
InfiniteIterator
InfiniteIterator не завершает цикл сам — он возвращает элементы по кругу. Часто комбинируется с LimitIterator, чтобы ограничить количество возвращаемых значений:
$months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
$infinite = new InfiniteIterator(new ArrayIterator($months));
foreach (new LimitIterator($infinite, 0, 36) as $month) {
echo $month . " ";
}Это выведет месяцы за три года.
FilterIterator
FilterIterator — это абстрактный класс, где вы определяете accept(), чтобы отфильтровать ненужные элементы:
class DemoFilterIterator extends FilterIterator {
public function __construct() {
parent::__construct(new ArrayIterator([1, 10, 4, 6, 3]));
}
public function accept() : bool {
return ($this->getInnerIterator()->current() < 5);
}
}
$demo = new DemoFilterIterator();
foreach ($demo as $val) {
echo $val . " ";
}Результат:
1 4 3FilterIterator удобно комбинировать с другими итераторами (Limit, Recursive, и т.д.) для построения конвейеров обработки данных.
Тип iterable
Если функция должна работать с любым набором элементов, который поддерживает foreach, используйте тип iterable. Это псевдотип, принимающий либо массив, либо любой Traversable.
function handleBadValues(iterable $values) : void {
foreach ($values as $value) {
var_dump($value);
}
}Помните: iterable очень общий. Если вы ожидаете конкретный класс коллекции с методами, лучше указывать этот класс. Iterable подходит для утилитарных или вспомогательных функций, где важен лишь факт проходимости.
Когда не использовать итераторы
- Когда вам нужно быстро обработать небольшое количество элементов функциями массива (array_map, array_filter) — иногда это проще и читаемее.
- Когда производительность критична и вы хотите избежать объектов и методов — в горячих циклах вызов методов может быть дороже прямого обращения к массиву.
- Когда коллекция используется только локально и не требуется строгая типизация — простой массив может быть предпочтительнее.
Контрпример: если у вас поток данных из внешнего источника (бд, файл, сеть), генератор (yield) может быть более экономным по памяти и проще в реализации, чем полноценный объект-итератор.
Альтернативы и расширения
- Генераторы (yield) — быстрый способ создать итератор без полноценного класса Iterator.
- ArrayObject / ArrayIterator — если нужно поведение массива с возможностью расширения.
- Комбинированные SPL-итераторы — строят конвейеры: DirectoryIterator → RecursiveIterator → FilterIterator → LimitIterator.
Пример генератора:
function genNumbers(int $n): Generator {
for ($i = 0; $i < $n; $i++) {
yield $i;
}
}
foreach (genNumbers(5) as $num) {
echo $num . " ";
}Генераторы просты, экономны по памяти и часто удобнее вручную реализуемых итераторов.
Мини-методология добавления итерации в коллекцию
- Оцените сложность обхода: нужен ли особый порядок, фильтрация, ленивость? Если нет — используйте IteratorAggregate + ArrayIterator.
- Если нужна фильтрация или лимит — комбинируйте SPL-итераторы.
- Если требуется сложное или ленивое вычисление — рассмотрите Generator или собственный Iterator.
- Напишите тесты на поведение foreach и граничные случаи.
- Документируйте ожидания от коллекции (что хранит, какие методы доступны).
Чеклист для разработчика
- Коллекция хранит только один тип доменных объектов.
- Реализован getIterator() или Iterator с корректной логикой rewind/valid/current/next/key.
- Есть тесты на пустую коллекцию, на один элемент и на несколько элементов.
- Документация указывает, является ли коллекция ленивой.
- Производительность в горячих циклах измерена при необходимости.
Критерии приёмки
- foreach по коллекции проходит по всем логическим элементам в ожидаемом порядке.
- При пустой коллекции цикл не выполняет итераций.
- При итерировании не появляются внутренние свойства объекта вместо элементов.
- Парные методы Iterator (rewind/next) вызываются корректно и не оставляют итератор в неконсистентном состоянии.
Тесты и примеры приёмки
Тестовые сценарии:
- Пустая коллекция: foreach не вызывает тела цикла.
- Одна запись: цикл входит ровно один раз и возвращает объект.
- Несколько записей: порядок соответствует добавлению (или заданному алгоритму порядка).
- Смешение типов: при добавлении неверного типа должна быть ошибка или игнорирование, согласно спецификации коллекции.
Пример PHPUnit теста (фрагмент):
public function testForeachIteratesItems() {
$coll = new UserCollection();
$coll->add(new UserDomain('A'));
$coll->add(new UserDomain('B'));
$names = [];
foreach ($coll as $user) {
$names[] = $user->getName();
}
$this->assertSame(['A', 'B'], $names);
}Ментальная модель
Думайте об итераторе как о курсоре или движке, который управляет положением в наборе данных. IteratorAggregate — это адаптер, который говорит: “возьми внутренний набор и сделай из него итератор“.
Шпаргалка по методам Iterator
- rewind() — установить курсор в начало
- current() — получить текущий элемент
- key() — получить текущий ключ
- next() — перейти к следующему элементу
- valid() — проверить, есть ли текущая позиция
Decision flowchart
flowchart TD
A[Нужна ли кастомная логика итерации?] -->|Нет| B[Использовать IteratorAggregate + ArrayIterator]
A -->|Да, простая фильтрация| C[Использовать FilterIterator или CallbackFilterIterator]
A -->|Да, ленивое вычисление| D[Использовать Generator]
C --> E[Комбинировать с LimitIterator или InfiniteIterator при необходимости]
D --> E
B --> F[Покрыть тестами: пустая, одна, несколько записей]
E --> FКраткая таблица совместимости и миграции
- PHP 5.1+ — поддержка Iterator и IteratorAggregate, SPL-итераторов большая часть доступна.
- Generator доступен с PHP 5.5.
Если вы поддерживаете очень старые версии PHP, проверьте наличие конкретного SPL-итератора.
Риски и рекомендации по безопасности
- Не возвращайте из итератора ресурсы, которые могут быть закрыты вне цикла — документируйте владение ресурсами.
- Следите за побочными эффектами в current()/next(): лучше избегать побочных изменений состояния приложения во время итерации.
Краткое резюме
- Для коллекций используйте IteratorAggregate + ArrayIterator по умолчанию.
- Для сложной последовательной логики реализуйте Iterator или используйте генераторы для ленивости.
- SPL предоставляет мощные готовые инструменты: LimitIterator, InfiniteIterator, FilterIterator и другие.
- Покрывайте поведение коллекции тестами и документируйте ожидаемое поведение.
Ключевая вера: итераторы повышают модульность и читаемость, убирая ручную работу с указателями и концентрируя логику обхода в одном месте.
Похожие материалы
Стрелки не работают в Excel — быстрое решение
Шифрование USB‑накопителя с VeraCrypt
PowerShell: история команд — просмотр и сохранение
Nandroid — полная резервная копия Android
Ошибка 0x800f0806 в Windows 11 22H2