Генераторы и yield в PHP: чек-лист перед продакшеном
Генератор экономит память, но ломается на нетривиальных сценариях: возврат значения, обработка исключений, повторный обход. Разбираем, что проверить перед выкатом.

Генератор в PHP отдаёт значения по одному через yield, не держа весь набор данных в памяти. На выгрузке миллиона строк из базы это разница между 512 МБ и парой мегабайт. Но перед тем как заменить обычный массив генератором в боевом коде, стоит пройтись по списку граничных случаев, где генератор ведёт себя не так, как массив, и молча ломает логику. Разбор рассчитан на PHP 8.x; принципиальные детали не менялись с 7.0, а yield from и возврат значения из генератора появились в 7.0.
Что генератор экономит и чем платит
Функция с yield возвращает объект класса Generator (реализует Iterator). Тело не выполняется до первого обращения. При каждой итерации выполнение доходит до очередного yield, отдаёт значение и замораживается до следующего запроса. Отсюда ключевое свойство: генератор ленив и одноразов.
Цена памяти минимальна, но у ленивости есть обратная сторона — её легко не заметить в тестах на маленьких данных и получить сюрприз в проде.
Генератор — это не «массив, который занимает мало места». Это поток, который можно пройти один раз. Любое место в коде, где вы ожидаете от него поведения массива, — потенциальный баг.
Чек-лист перед выкатом
1. Генератор нельзя перебрать дважды
Самая частая ошибка. После полного обхода генератор исчерпан, повторный foreach не даст ничего и не выбросит ошибку.
function readLines(string $path): Generator
{
$handle = fopen($path, 'r');
while (($line = fgets($handle)) !== false) {
yield rtrim($line);
}
fclose($handle);
}
$lines = readLines('data.txt');
$count = iterator_count($lines); // прошли до конца
foreach ($lines as $line) {
// сюда не зайдём ни разу: генератор уже исчерпан
}
Попытка перемотать исчерпанный или уже начатый генератор через rewind() даёт Exception: Cannot rewind a generator that was already run. Если данные нужны повторно — либо материализуйте в массив через iterator_to_array(), либо создавайте генератор заново на каждый обход.
2. count() на генераторе не работает
count($generator) выбросит TypeError в PHP 8: генератор не Countable. Чтобы посчитать элементы, используйте iterator_count() — но помните, что она проходит генератор до конца и исчерпывает его.
3. Возврат значения через return требует getReturn()
С PHP 7.0 в генераторе разрешён return со значением. Это значение не попадает в итерацию — забрать его можно только после завершения обхода через getReturn().
function parseWithStats(array $rows): Generator
{
$valid = 0;
foreach ($rows as $row) {
if ($row === null) {
continue;
}
$valid++;
yield $row;
}
return $valid; // итоговая статистика
}
$gen = parseWithStats([1, null, 2, 3]);
foreach ($gen as $item) {
// обрабатываем 1, 2, 3
}
$total = $gen->getReturn(); // 3, доступно только после foreach
Вызов getReturn() до завершения генератора выбросит исключение. Это удобный способ вернуть агрегат, но он неочевиден для того, кто читает код позже.
4. Исключения внутри генератора всплывают в месте итерации
Если yield находится внутри блока, где может произойти сбой, исключение выбросится не при создании генератора, а на той итерации, где выполнение дойдёт до проблемной строки. Оборачивать в try/catch нужно сам foreach, а не строку создания генератора.
$gen = readLines('missing.txt'); // здесь ошибки ещё нет
try {
foreach ($gen as $line) { // fopen упадёт здесь, на первой итерации
process($line);
}
} catch (Throwable $e) {
logError($e);
}
5. Ресурсы освобождаются не всегда
Если код после yield закрывает файл или соединение (как fclose в примере выше), а обход прервался через break — генератор замораживается на текущем yield и fclose не вызывается сразу. Он выполнится, когда сборщик мусора уничтожит объект генератора, но момент вы не контролируете. При работе с внешними ресурсами используйте finally:
function readLines(string $path): Generator
{
$handle = fopen($path, 'r');
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line);
}
} finally {
fclose($handle); // сработает и при break, и при исключении
}
}
6. yield from не сбрасывает числовые ключи
yield from делегирует другому итератору или массиву. Ключи он передаёт как есть, а не переиндексирует. При делегировании нескольким источникам с числовыми ключами вы получите дубликаты ключей — при материализации через iterator_to_array() без второго аргумента false поздние значения затрут ранние.
function combined(): Generator
{
yield from ['a', 'b']; // ключи 0, 1
yield from ['c', 'd']; // снова ключи 0, 1
}
// сохранить все значения: preserve_keys = false
$all = iterator_to_array(combined(), false); // ['a', 'b', 'c', 'd']
7. Передача в функции, ожидающие массив
Многие встроенные функции (array_map, array_filter, in_array, sort) принимают только массив, а не Traversable. Передача генератора в них даст TypeError. Либо материализуйте через iterator_to_array() (теряя выигрыш по памяти), либо пишите обход руками или через генераторные аналоги.
Когда генератор — правильный выбор, а когда нет
| Сценарий | Генератор | Массив |
|---|---|---|
| Построчное чтение большого файла | Да, память постоянна | Риск нехватки памяти |
| Выгрузка курсором из БД | Да, если драйвер отдаёт по строке | Держит весь результат |
| Нужен повторный обход набора | Нет, одноразов | Да |
| Нужны count, sort, произвольный доступ | Нет | Да |
| Небольшой набор (сотни элементов) | Избыточно | Проще и понятнее |
| Бесконечная или ленивая последовательность | Да, единственный вариант | Невозможно |
Практическое правило: генератор оправдан там, где набор большой или неизвестного размера и проходится ровно один раз линейно. Как только появляется потребность в случайном доступе, повторном обходе или подсчёте — выигрыш по памяти съедается неудобствами, и обычный массив честнее.
Отдельная ловушка: курсор к базе
Генератор поверх выборки из БД экономит память только если сам драйвер не буферизует результат целиком на стороне PHP. Для MySQL через PDO по умолчанию включена буферизация — весь результат уже в памяти PHP, и генератор ничего не спасает. Небуферизованный режим (атрибут PDO::MYSQL_ATTR_USE_BUFFERED_QUERY со значением false) снимает буфер, но тогда соединение занято до конца обхода и вы не можете делать параллельные запросы в том же соединении. Проверяйте поведение конкретного драйвера в его разделе документации, а не полагайтесь на то, что yield сам по себе решает вопрос памяти.
Что сделать перед деплоем
- Найдите в коде все места, где результат генератора мог бы обходиться повторно — и убедитесь, что это не так.
- Проверьте, что открытые ресурсы закрываются в
finally, а не в коде после последнегоyield. - Убедитесь, что
try/catchстоит вокругforeach, а не вокруг создания генератора. - Прогоните на объёме, близком к боевому: ленивость прячет ошибки на малых данных.
- Если генератор поверх БД — замерьте реальное потребление памяти, а не предполагайте его.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: PHP Manual: Generators overview, PHP Manual: Generator syntax (yield, yield from, return)
Частые вопросы
А если у меня PHP 5.6 — генераторы там есть?
Как посчитать элементы генератора, не потеряв их?
Генератор точно экономит память при работе с базой?
Почему foreach по генератору ничего не выдал, но ошибки нет?
Можно ли передать генератор в array_map или sort?
Что будет с открытым файлом, если прервать обход через break?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.