Гид по технологиям

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

6 min read PHP Обновлено 26 Nov 2025
Итераторы в PHP: руководство по коллекциям
Итераторы в PHP: руководство по коллекциям

Логотип PHP

Важно: 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 3

FilterIterator удобно комбинировать с другими итераторами (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 . " ";
}

Генераторы просты, экономны по памяти и часто удобнее вручную реализуемых итераторов.

Мини-методология добавления итерации в коллекцию

  1. Оцените сложность обхода: нужен ли особый порядок, фильтрация, ленивость? Если нет — используйте IteratorAggregate + ArrayIterator.
  2. Если нужна фильтрация или лимит — комбинируйте SPL-итераторы.
  3. Если требуется сложное или ленивое вычисление — рассмотрите Generator или собственный Iterator.
  4. Напишите тесты на поведение foreach и граничные случаи.
  5. Документируйте ожидания от коллекции (что хранит, какие методы доступны).

Чеклист для разработчика

  • Коллекция хранит только один тип доменных объектов.
  • Реализован 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 и другие.
  • Покрывайте поведение коллекции тестами и документируйте ожидаемое поведение.

Ключевая вера: итераторы повышают модульность и читаемость, убирая ручную работу с указателями и концентрируя логику обхода в одном месте.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Стрелки не работают в Excel — быстрое решение
Excel

Стрелки не работают в Excel — быстрое решение

Шифрование USB‑накопителя с VeraCrypt
Безопасность

Шифрование USB‑накопителя с VeraCrypt

PowerShell: история команд — просмотр и сохранение
PowerShell

PowerShell: история команд — просмотр и сохранение

Nandroid — полная резервная копия Android
Android.

Nandroid — полная резервная копия Android

Ошибка 0x800f0806 в Windows 11 22H2
Windows 11

Ошибка 0x800f0806 в Windows 11 22H2

Извлечь ссылки с веб‑страницы PowerShell
Automation

Извлечь ссылки с веб‑страницы PowerShell