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.