🐘 PHP

المكررات والمولدات في PHP — دليل شامل

Что это такое и зачем? (аналогия из реальной жизни)

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

Способ 1 (без итератора): Вы снимаете все книги с полок, складываете их в огромную тележку, везёте к столу и только потом начинаете читать названия. Проблема: тележка может не вместить все книги, а если книг миллион — вы просто не сможете их все одновременно удержать в голове или в тележке.

Способ 2 (с итератором): Вы берёте блокнот и ручку, подходите к стеллажу, читаете первую книгу, записываете, переходите ко второй, третьей... Вы не тащите все книги сразу — вы обрабатываете их по одной, по порядку. И в любой момент вы можете остановиться, отдохнуть, а потом продолжить с того же места.

В программировании происходит то же самое. У нас есть коллекции данных: массивы, списки, результаты из базы данных, строки из файла. Иногда эти коллекции огромны — миллионы записей. Загружать их все в память сразу — это как пытаться уместить океан в стакан. Компьютер просто "захлебнётся" и упадёт.

Итераторы — это те самые "библиотекари с блокнотом". Они позволяют проходить по элементам коллекции по одному, не загружая всё сразу. Вы просто говорите: "Дай следующий элемент", и итератор его выдаёт. Когда элементы закончатся — он скажет "Стоп, больше нет".

Генераторы — это ещё более крутая штука. Представьте, что вы не просто читаете готовые книги, а сами пишете их на ходу. Генератор — это функция, которая может "заморозиться" на полуслове, выдать результат, а потом, когда её попросят, продолжить с того же места. Это как сериал: вы посмотрели серию, нажали паузу, а через неделю нажали Play и продолжили с того же момента.

Важно: Итераторы и генераторы экономят память. Если у вас есть файл на 10 гигабайт, вы можете читать его построчно с помощью генератора, и в памяти будет только одна строка, а не весь файл. Без генератора программа бы просто "упала" от нехватки памяти.

Теория: как это работает изнутри

Давайте заглянем "под капот" и разберёмся, как устроены итераторы и генераторы в PHP. Не пугайтесь — всё логично.

Итераторы: интерфейс и договорённости

В PHP есть специальный "контракт" (интерфейс) под названием Iterator. Любой класс, который хочет стать итератором, должен выполнить этот контракт — то есть реализовать пять обязательных методов. Это как правила дорожного движения: если ты водитель, ты обязан знать и выполнять эти правила.

Вот эти пять методов:

Метод Что делает Аналогия из жизни
current() Возвращает текущий элемент Вы смотрите на книгу, которая сейчас перед вами
key() Возвращает ключ (номер или имя) текущего элемента Вы смотрите на номер страницы, на которой остановились
next() Переходит к следующему элементу Вы переворачиваете страницу
rewind() Сбрасывает итератор в начало Вы закрываете книгу и открываете её снова на первой странице
valid() Проверяет, есть ли ещё элементы Вы смотрите, не закончилась ли книга

Когда вы пишете foreach ($collection as $key => $value), PHP внутри делает примерно так:

// PHP делает это за кулисами, когда вы используете foreach
$iterator->rewind();          // Встаём в начало
while ($iterator->valid()) {  // Пока есть элементы
    $key = $iterator->key();      // Берём ключ
    $value = $iterator->current(); // Берём значение
    // ... ваш код ...
    $iterator->next();        // Переходим к следующему
}

Генераторы: функции с "паузой"

Генератор — это особая функция, в которой есть ключевое слово yield. Когда PHP встречает yield, он:

  1. Запоминает всё состояние функции (все переменные, место в коде)
  2. Возвращает значение наружу
  3. Засыпает и ждёт, пока его не попросят продолжить

Когда вы снова вызываете next() у генератора, он просыпается и продолжает выполнение с того самого места, где остановился. Это называется "ленивые вычисления" — значение вычисляется только тогда, когда оно реально нужно.

Ключевое отличие: Обычная функция выполняется от начала до конца и возвращает одно значение. Генератор может "выдавать" много значений по одному, засыпая между ними. Обычная функция — это курьер, который привёз одну посылку. Генератор — это конвейер, который выдаёт детали одну за другой.

Синтаксис в PHP: все формы и варианты

В PHP есть несколько способов работать с итераторами и генераторами. Давайте разберём каждый.

1. Встроенные итераторы (SPL — Standard PHP Library)

PHP уже имеет готовые итераторы для самых частых задач. Вам не нужно писать свои — просто берите и пользуйтесь.

Итератор Для чего Пример использования
ArrayIterator Превращает обычный массив в итератор Когда нужно передать массив туда, где ожидается итератор
DirectoryIterator Обходит файлы и папки в директории Список всех файлов в папке
FilesystemIterator То же, но быстрее и проще Обход файлов без лишней информации
LimitIterator Ограничивает количество элементов Взять первые 10 записей из 1000
FilterIterator Пропускает только подходящие элементы Оставить только файлы с расширением .php

2. Создание своего итератора через класс

Если готовых итераторов недостаточно, вы можете создать свой. Для этого нужно реализовать интерфейс Iterator.

// Создаём свой итератор для обхода чисел Фибоначчи
class FibonacciIterator implements Iterator {
    private int $previous = 0;  // Предыдущее число
    private int $current = 1;   // Текущее число
    private int $position = 0;  // Позиция (ключ)
    private int $maxCount;      // Сколько чисел хотим получить

    public function __construct(int $maxCount) {
        $this->maxCount = $maxCount;
    }

    // Возвращает текущее значение (число Фибоначчи)
    public function current(): mixed {
        return $this->current;
    }

    // Возвращает ключ (номер числа по порядку)
    public function key(): mixed {
        return $this->position;
    }

    // Переход к следующему числу
    public function next(): void {
        // Вычисляем следующее число Фибоначчи
        $next = $this->previous + $this->current;
        $this->previous = $this->current;
        $this->current = $next;
        $this->position++;
    }

    // Сброс в начало
    public function rewind(): void {
        $this->previous = 0;
        $this->current = 1;
        $this->position = 0;
    }

    // Проверка: есть ли ещё элементы?
    public function valid(): bool {
        return $this->position < $this->maxCount;
    }
}

// Используем наш итератор
$fib = new FibonacciIterator(10);
foreach ($fib as $key => $value) {
    echo "Число #$key: $value
";
}
// Выведет: Число #0: 1, Число #1: 1, Число #2: 2, Число #3: 3, ...

3. Генераторы — самый простой способ

Генераторы — это "ленивый" вариант итератора. Вам не нужно писать целый класс с пятью методами. Достаточно написать функцию с yield.

// Генератор чисел Фибоначчи (та же задача, но в 5 строк)
function fibonacciGenerator(int $maxCount): Generator {
    $previous = 0;
    $current = 1;
    
    for ($i = 0; $i < $maxCount; $i++) {
        yield $i => $current;  // Отдаём значение и засыпаем
        // Когда попросят — просыпаемся и продолжаем
        $next = $previous + $current;
        $previous = $current;
        $current = $next;
    }
}

// Используем генератор
foreach (fibonacciGenerator(10) as $key => $value) {
    echo "Число #$key: $value
";
}

4. Генераторы с ключами и без

Вы можете отдавать только значения (без ключей) или указывать ключи явно:

// Без ключей — ключи будут 0, 1, 2...
function simpleGenerator(): Generator {
    yield 'первый';
    yield 'второй';
    yield 'третий';
}

// С ключами
function keyedGenerator(): Generator {
    yield 'name' => 'Иван';
    yield 'age' => 25;
    yield 'city' => 'Москва';
}

// yield без значения — вернёт null
function nullGenerator(): Generator {
    yield;  // current() вернёт null
    yield 'теперь есть значение';
}

5. Генераторы с делегированием (yield from)

Можно вложить один генератор в другой с помощью yield from. Это как матрёшка:

function generatorA(): Generator {
    yield 'A1';
    yield 'A2';
}

function generatorB(): Generator {
    yield 'B1';
    yield from generatorA();  // Вставляем все значения из A
    yield 'B2';
}

foreach (generatorB() as $value) {
    echo "$value ";  // B1 A1 A2 B2
}

6. Генераторы как массивы (с помощью iterator_to_array)

Если вам всё-таки нужен обычный массив, можно "развернуть" генератор:

function rangeGenerator(int $start, int $end): Generator {
    for ($i = $start; $i <= $end; $i++) {
        yield $i;
    }
}

// Превращаем генератор в массив
$numbers = iterator_to_array(rangeGenerator(1, 5));
print_r($numbers); // [1, 2, 3, 4, 5]
Предупреждение: Используйте iterator_to_array только если уверены, что данных мало. Иначе вы теряете весь смысл генератора — экономию памяти.

Примеры кода — от простого к сложному

Пример 1: Чтение большого файла построчно

Это классика. Файл на 1 ГБ, но в памяти только одна строка.

// Функция-генератор для чтения файла
function readFileLines(string $filename): Generator {
    // Открываем файл
    $handle = fopen($filename, 'r');
    
    // Если файл не открылся — ошибка
    if (!$handle) {
        throw new Exception("Не могу открыть файл: $filename");
    }
    
    // Читаем строку за строкой
    while (($line = fgets($handle)) !== false) {
        // Отдаём текущую строку (убираем пробелы по краям)
        yield trim($line);
    }
    
    // Закрываем файл
    fclose($handle);
}

// Используем генератор
foreach (readFileLines('big_log.txt') as $lineNumber => $line) {
    // Обрабатываем только строки, содержащие "ERROR"
    if (str_contains($line, 'ERROR')) {
        echo "Строка $lineNumber: $line
";
    }
    // Память занята только одной строкой, а не всем файлом!
}

Пример 2: Бесконечный генератор (числа Фибоначчи)

Генератор может работать бесконечно — он не хранит все значения.

// Бесконечный генератор чисел Фибоначчи
function infiniteFibonacci(): Generator {
    $a = 0;
    $b = 1;
    
    while (true) {  // Бесконечный цикл!
        yield $b;
        [$a, $b] = [$b, $a + $b];  // Обмен значениями (короткая запись)
    }
}

// Берём первые 20 чисел
$count = 0;
foreach (infiniteFibonacci() as $number) {
    echo "$number ";
    $count++;
    if ($count >= 20) break;  // Останавливаемся сами
}
// Выведет: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765

Пример 3: Генератор с фильтрацией

Генератор может не только отдавать данные, но и фильтровать их на лету.

// Генератор простых чисел (до N)
function primeGenerator(int $limit): Generator {
    for ($number = 2; $number <= $limit; $number++) {
        $isPrime = true;
        
        // Проверяем, делится ли число на что-то кроме 1 и себя
        for ($divisor = 2; $divisor <= sqrt($number); $divisor++) {
            if ($number % $divisor == 0) {
                $isPrime = false;
                break;
            }
        }
        
        // Если число простое — отдаём его
        if ($isPrime) {
            yield $number;
        }
    }
}

// Выводим все простые числа до 50
foreach (primeGenerator(50) as $prime) {
    echo "$prime ";  // 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
}

Пример 4: Свой итератор для обхода дерева (папки)

Представьте, что у вас есть вложенные папки. Нужно найти все файлы с расширением .php.

// Итератор для рекурсивного обхода папок
class PhpFileIterator implements Iterator {
    private array $files = [];    // Все найденные PHP-файлы
    private int $position = 0;    // Текущая позиция
    
    public function __construct(string $rootDir) {
        $this->findPhpFiles($rootDir);
    }
    
    // Рекурсивно ищем PHP-файлы
    private function findPhpFiles(string $dir): void {
        $items = scandir($dir);
        
        foreach ($items as $item) {
            // Пропускаем текущую и родительскую папку
            if ($item == '.' || $item == '..') continue;
            
            $path = $dir . DIRECTORY_SEPARATOR . $item;
            
            if (is_dir($path)) {
                // Если это папка — заходим в неё рекурсивно
                $this->findPhpFiles($path);
            } elseif (pathinfo($path, PATHINFO_EXTENSION) == 'php') {
                // Если это PHP-файл — добавляем в список
                $this->files[] = $path;
            }
        }
    }
    
    public function current(): mixed {
        return $this->files[$this->position];
    }
    
    public function key(): mixed {
        return $this->position;
    }
    
    public function next(): void {
        $this->position++;
    }
    
    public function rewind(): void {
        $this->position = 0;
    }
    
    public function valid(): bool {
        return isset($this->files[$this->position]);
    }
}

// Используем
$iterator = new PhpFileIterator('/var/www/project');
foreach ($iterator as $file) {
    echo "Найден PHP-файл: $file
";
}

Пример 5: Генератор с отправкой данных (send)

Генератор может не только отдавать, но и принимать данные. Это двухсторонняя связь!

// Генератор-калькулятор: принимает числа и возвращает сумму
function accumulator(): Generator {
    $sum = 0;
    
    // Бесконечно принимаем числа
    while (true) {
        // yield получает значение из send() и отдаёт текущую сумму
        $number = yield $sum;
        
        if ($number === null) {
            // Если ничего не прислали — выходим
            break;
        }
        
        $sum += $number;
    }
    
    return $sum;  // Финальный результат
}

// Используем
$gen = accumulator();

// Первый вызов current() — запускаем генератор до первого yield
echo $gen->current() . "
";  // 0 (начальная сумма)

// Отправляем числа через send()
echo $gen->send(10) . "
";  // 10 (0 + 10)
echo $gen->send(20) . "
";  // 30 (10 + 20)
echo $gen->send(30) . "
";  // 60 (30 + 30)

// Завершаем генератор
$gen->send(null);
echo "Финальная сумма: " . $gen->getReturn() . "
";  // 60

Пример 6: Комбинирование нескольких генераторов

Можно объединять данные из разных источников.

// Генератор чётных чисел
function evenNumbers(int $limit): Generator {
    for ($i = 2; $i <= $limit; $i += 2) {
        yield $i;
    }
}

// Генератор нечётных чисел
function oddNumbers(int $limit): Generator {
    for ($i = 1; $i <= $limit; $i += 2) {
        yield $i;
    }
}

// Генератор, который чередует чётные и нечётные
function interleave(int $limit): Generator {
    $evens = evenNumbers($limit);
    $odds = oddNumbers($limit);
    
    // Пока есть хоть один источник — чередуем
    while ($evens->valid() || $odds->valid()) {
        if ($evens->valid()) {
            yield 'even' => $evens->current();
            $evens->next();
        }
        if ($odds->valid()) {
            yield 'odd' => $odds->current();
            $odds->next();
        }
    }
}

foreach (interleave(10) as $type => $number) {
    echo "$type: $number
";
}
// even: 2
// odd: 1
// even: 4
// odd: 3
// ...

Пример 7: Генератор для пагинации (API с постраничной выдачей)

Реальная задача: нужно получить все страницы из API, но мы не знаем, сколько их.

// Генератор для постраничного получения данных из API
function apiPaginator(string $url, int $perPage = 100): Generator {
    $page = 1;
    $hasMore = true;
    
    while ($hasMore) {
        // Формируем URL с параметрами страницы
        $requestUrl = $url . '?page=' . $page . '&per_page=' . $perPage;
        
        // Имитируем запрос к API (в реальности — curl или file_get_contents)
        $response = file_get_contents($requestUrl);
        $data = json_decode($response, true);
        
        // Если данных нет — заканчиваем
        if (empty($data['items'])) {
            $hasMore = false;
            break;
        }
        
        // Отдаём каждый элемент со страницы
        foreach ($data['items'] as $item) {
            yield $item;
        }
        
        // Проверяем, есть ли следующая страница
        $hasMore = !empty($data['next_page']);
        $page++;
    }
}

// Используем: обрабатываем элементы по одному, не загружая всё сразу
foreach (apiPaginator('https://api.example.com/users') as $user) {
    echo "Обрабатываем пользователя: " . $user['name'] . "
";
    // Здесь можно сохранить в БД, отправить письмо и т.д.
}

Сравнение с Python: чем похоже и чем отличается

Если вы изучали Python, вы уже встречались с генераторами. Давайте сравним.

Характеристика PHP Python
Ключевое слово yield yield
Тип генератора Generator (класс) generator (встроенный тип)
Создание итератора Через интерфейс Iterator (5 методов) Через методы __iter__ и __next__ (2 метода)
Отправка данных в генератор $gen->send($value) gen.send(value)
Делегирование yield from yield from
Возврат значения return $value + getReturn() return $value + StopIteration.value
Генераторные выражения Нет (только функции) (x for x in range(10))
Встроенные итераторы SPL (DirectoryIterator, LimitIterator и др.) itertools (chain, cycle, groupby и др.)

Главное отличие: В Python генераторы — это "первоклассные граждане", они используются повсеместно. В PHP генераторы появились позже (в версии 5.5) и до сих пор используются реже, чем могли бы. Многие PHP-разработчики по старинке загружают всё в массивы, хотя генераторы были бы эффективнее.

Ещё отличие: В PHP нет генераторных выражений (аналога (x*2 for x in range(10))). В PHP нужно всегда писать функцию. Но это не страшно — функция даёт больше контроля.

Типичные ошибки и подводные камни

Ошибка 1: Попытка использовать генератор как массив

function myGen(): Generator {
    yield 1;
    yield 2;
    yield 3;
}

$gen = myGen();
echo $gen[0];  // Ошибка! Нельзя обратиться по индексу
echo count($gen);  // Ошибка! Нельзя посчитать количество

Решение: Используйте foreach или iterator_to_array() (но осторожно с памятью).

Ошибка 2: Повторное использование генератора

$gen = myGen();
foreach ($gen as $v) { /* первый проход — ок */ }
foreach ($gen as $v) { /* второй проход — пусто! */ }

Генератор нельзя "перемотать" назад. После завершения он пуст. Нужно создавать новый экземпляр.

Решение: Создавайте генератор заново для каждого прохода.

Ошибка 3: Забыли про return в генераторе

function genWithReturn(): Generator {
    yield 1;
    yield 2;
    return 'done';
}

$gen = genWithReturn();
foreach ($gen as $v) {
    echo $v;  // 1 2
}
// return 'done' не попадёт в foreach!
echo $gen->getReturn();  // 'done' — нужно вызывать отдельно

Ошибка 4: Утечка ресурсов (файлы, соединения)

function badFileReader(string $file): Generator {
    $handle = fopen($file, 'r');
    while ($line = fgets($handle)) {
        yield $line;
    }
    // Если foreach прервётся досрочно (break), файл не закроется!
    fclose($handle);
}

Решение: Используйте try/finally для гарантированного закрытия:

function safeFileReader(string $file): Generator {
    $handle = fopen($file, 'r');
    try {
        while ($line = fgets($handle)) {
            yield $line;
        }
    } finally {
        fclose($handle);  // Закроется даже при break!
    }
}

Ошибка 5: Генератор внутри генератора без yield from

function inner(): Generator {
    yield 'внутри';
}

function outer(): Generator {
    yield 'снаружи';
    yield inner();  // Ошибка! Отдаст объект Generator, а не его значения
}

foreach (outer() as $v) {
    echo $v;  // 'снаружи', потом объект Generator (не то, что нужно)
}

Решение: Используйте yield from inner().

Лучшие практики и советы профессионалов

  1. Используйте генераторы для больших данных. Если вы работаете с файлами, базами данных, API — всегда думайте о генераторах. Они спасут вас от ошибок "out of memory".

  2. Не бойтесь создавать свои итераторы. Если логика обхода сложная (деревья, графы, вложенные структуры) — класс-итератор сделает код понятнее и переиспользуемее.

  3. Именуйте генераторы с суффиксом Generator. Например, getUsersGenerator() или readLogGenerator(). Это сразу говорит о том, что функция возвращает генератор, а не массив.

  4. Документируйте тип возврата. Всегда пишите : Generator в объявлении функции. Это помогает IDE и другим разработчикам.

  5. Используйте встроенные итераторы SPL. Прежде чем писать свой итератор, проверьте, нет ли готового в стандартной библиотеке. DirectoryIterator, RecursiveIteratorIterator, FilterIterator — мощные инструменты.

  6. Комбинируйте итераторы. Можно обернуть один итератор в другой. Например, LimitIterator поверх DirectoryIterator — получите первые 10 файлов.

  7. Не злоупотребляйте генераторами для маленьких массивов. Если у вас 10 элементов — обычный массив будет быстрее и проще. Генераторы хороши для больших или бесконечных последовательностей.

  8. Помните про однократность. Генератор можно обойти только один раз. Если нужно обойти дважды — сохраните результат в массив (если он небольшой) или создайте новый генератор.

Золотое правило: Если вы пишете foreach и внутри него накапливаете данные в массив "на всякий случай" — скорее всего, вам нужен генератор. Он сделает код чище, а память — свободнее.

Вопросы для самопроверки

  1. Вопрос: В чём основное отличие итератора от генератора в PHP?

    Ответ: Итератор — это класс, реализующий интерфейс Iterator (5 методов). Генератор — это функция с yield, которая автоматически становится объектом класса Generator. Генератор проще писать, но итератор даёт больше