PHPMiddleCoding

Что такое generators в PHP и когда их стоит использовать?

Generator — функция с yield, возвращающая объект Generator. Используется для ленивой обработки больших наборов данных без загрузки всего в память: чтение файлов построчно, потоковая пагинация, бесконечные последовательности.

Что такое generators в PHP

Generator — специальный тип функции, содержащий оператор yield. При вызове такой функции PHP не выполняет тело, а возвращает объект класса Generator, реализующий интерфейс Iterator. Выполнение тела происходит лениво: каждый вызов ->current() / ->next() или итерация в foreach продвигает выполнение до следующего yield.

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

Базовый пример

<?php
declare(strict_types=1);

// Генератор бесконечной последовательности
function fibonacci(): Generator {
    [$a, $b] = [0, 1];
    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

$gen = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $gen->current() . "\n";
    $gen->next();
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Практический пример: чтение большого CSV

<?php
declare(strict_types=1);

/**
 * Читает CSV построчно, не загружая файл целиком в память.
 * @return Generator<int, array<string, string>>
 */
function readCsv(string $path): Generator {
    $handle = fopen($path, 'r');
    if ($handle === false) {
        throw new \RuntimeException("Cannot open file: {$path}");
    }

    try {
        $headers = fgetcsv($handle);
        if ($headers === false) {
            return;
        }

        while (($row = fgetcsv($handle)) !== false) {
            yield array_combine($headers, $row);
        }
    } finally {
        fclose($handle);
    }
}

// Использование: memory_limit не зависит от размера файла
foreach (readCsv('/data/users.csv') as $index => $row) {
    processUser($row['email'], $row['name']);
}

yield from и делегирование

<?php
declare(strict_types=1);

function innerGenerator(): Generator {
    yield 1;
    yield 2;
    return 'inner_result'; // возвращаемое значение через getReturn()
}

function outerGenerator(): Generator {
    yield 0;
    $result = yield from innerGenerator(); // делегирует + получает return value
    yield 3;
    echo "Inner returned: {$result}\n"; // 'inner_result'
}

foreach (outerGenerator() as $value) {
    echo $value . "\n"; // 0, 1, 2, 3
}

Двунаправленная коммуникация: send()

<?php
declare(strict_types=1);

function accumulator(): Generator {
    $sum = 0;
    while (true) {
        $value = yield $sum; // yield возвращает значение, переданное через send()
        if ($value === null) {
            break;
        }
        $sum += $value;
    }
}

$gen = accumulator();
$gen->current(); // инициализация до первого yield
$gen->send(10); // sum = 10
$gen->send(20); // sum = 30
echo $gen->send(5); // 35

Когда использовать generators

  • Чтение больших файлов (CSV, JSON Lines, лог-файлы) построчно.
  • Потоковая пагинация из БД: yield каждый batch, не загружая все строки.
  • Бесконечные последовательности (числа Фибоначчи, UUID, временные метки).
  • Пайплайны трансформаций: несколько generators, соединённых через yield from.
  • Кооперативная многозадачность в event loop (ReactPHP использует generators в старых API).

Подводные камни

  • Generator нельзя перемотать: rewind() вызывает Exception если генератор уже продвинулся. Для повторной итерации создавайте новый экземпляр.
  • Generator::getReturn() бросает Exception, если генератор ещё не завершён — вызывать только после того, как valid() вернул false.
  • Ресурсы (файловые дескрипторы, соединения с БД) в теле генератора не закрываются автоматически при раннем выходе из foreach — используйте try/finally.
  • Генераторы не сериализуются: нельзя передать объект Generator в очередь задач или кэш.
  • Производительность: overhead на каждый yield существенен для маленьких наборов данных. Для массива из 100 элементов обычный array_map быстрее.
  • Типизация: Generator<TKey, TValue, TSend, TReturn> — PHPStan и Psalm поддерживают generic-аннотации, без них статический анализ не проверяет типы значений.
  • yield from array работает, но ключи сохраняются из исходного массива — может сломать логику, ожидающую последовательные integer-ключи.
  • В Fibers (PHP 8.1) и generators схожая семантика приостановки, но они не взаимозаменяемы: Fiber приостанавливается через Fiber::suspend() изнутри любого стека вызовов, generator — только непосредственно в своём теле.

Common mistakes

  • Сводить generators к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: интерпретируемый runtime PHP 8.x, обычно запускаемый как отдельный запрос в FPM, CLI или worker-процессе.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет generators через конкретную точку lifecycle в PHP.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics