Генераторы и yield в PHP: рабочие приёмы из практики
Когда обычный массив съедает всю память на большом CSV, а построчное чтение выглядит громоздко. Разбираем, где yield реально помогает в PHP 8.x, а где создаёт скрытые проблемы.

Задача простая: выгрузить в отчёт 800 тысяч строк из базы, отдать их в CSV и закинуть в файл. Наивный код собирает всё в массив через fetchAll(), PHP упирается в memory_limit, и скрипт падает с Fatal error: Allowed memory size exhausted. Поднять лимит до гигабайта — костыль, который перестанет работать на следующем объёме данных. Генераторы решают именно эту проблему: они позволяют перебирать последовательность, не держа её целиком в памяти. Всё, что ниже, проверено на PHP 8.2 и 8.3.
Что делает yield и чем платит
Функция с ключевым словом yield в теле перестаёт быть обычной функцией и возвращает объект Generator. При вызове её тело не выполняется — до тех пор, пока по результату не начнут итерироваться. На каждой итерации код доходит до очередного yield, отдаёт значение и замораживается, сохраняя своё состояние: локальные переменные, позицию в цикле. Следующая итерация продолжает ровно с этого места.
Ключевое следствие: в памяти живёт одно значение за раз, а не вся коллекция. Цена — генератор одноразовый. Пройти по нему второй раз нельзя, попытка перебора уже исчерпанного генератора просто ничего не даст, а вызов rewind() после начала итерации бросит исключение.
Генератор — это не коллекция, это рецепт получения значений по одному. Если вам нужен массив, к которому обращаются по индексу и перебирают дважды, генератор не подходит.
Простейший пример: ленивое чтение файла
Чтение большого файла построчно — классический случай. Функция file() загружает весь файл в массив, генератор отдаёт строки по одной:
<?php
// PHP 8.x
function readLines(string $path): Generator
{
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException("Не удалось открыть файл: $path");
}
try {
while (($line = fgets($handle)) !== false) {
// отдаём строку без завершающего перевода строки
yield rtrim($line, "\r\n");
}
} finally {
// finally гарантирует закрытие даже при прерывании перебора
fclose($handle);
}
}
foreach (readLines('/var/log/app/access.log') as $line) {
// обрабатываем по одной строке, память не растёт
echo strlen($line), PHP_EOL;
}
Блок finally здесь не для красоты. Если перебор прервать через break, генератор уничтожается сборщиком мусора, и finally отработает — файл закроется. Без него дескриптор повиснет до конца скрипта.
Приём первый: ключи через yield key => value
Генератор умеет отдавать не только значения, но и ключи. Синтаксис тот же, что в массивах:
<?php
function parseCsvWithHeader(string $path): Generator
{
$handle = fopen($path, 'rb');
$header = fgetcsv($handle);
try {
$rowNumber = 0;
while (($cells = fgetcsv($handle)) !== false) {
// ключ — номер строки, значение — ассоциативный массив
yield $rowNumber++ => array_combine($header, $cells);
}
} finally {
fclose($handle);
}
}
foreach (parseCsvWithHeader('orders.csv') as $num => $row) {
printf("Строка %d: заказ %s\n", $num, $row['order_id']);
}
Важная деталь: ключи генератора не обязаны быть уникальными и последовательными в отличие от массива. Если вы отдаёте один и тот же ключ дважды, оба значения дойдут до цикла — конфликта не будет, потому что генератор ничего не индексирует.
Приём второй: делегирование через yield from
Когда один генератор должен отдать все значения другого, ручной цикл-проброс избыточен. Конструкция yield from делегирует итерацию вложенному генератору или обходимой сущности:
<?php
function readAllLogs(array $paths): Generator
{
foreach ($paths as $path) {
// отдаём все строки каждого файла подряд
yield from readLines($path);
}
}
$logs = readAllLogs([
'/var/log/app/day1.log',
'/var/log/app/day2.log',
]);
foreach ($logs as $line) {
// строки из обоих файлов идут единым потоком
}
Подводный камень с ключами: yield from сохраняет ключи вложенного генератора. Если во внешнем и внутреннем генераторах ключи пересекаются, при переборе через iterator_to_array() без второго аргумента false вы потеряете часть данных — одинаковые ключи затрут друг друга при сборке в массив. В обычном foreach проблемы нет, значения просто идут по порядку.
Приём третий: обратная связь через send
Генератор — двунаправленный канал. Метод send() передаёт значение внутрь, оно становится результатом выражения yield. Это редкий приём, но он полезен для корутин и конвейеров обработки:
<?php
function accumulator(): Generator
{
$sum = 0;
while (true) {
// yield возвращает то, что пришло через send()
$value = yield $sum;
$sum += $value;
}
}
$acc = accumulator();
$acc->current(); // запускаем генератор до первого yield
echo $acc->send(10); // 10
echo $acc->send(5); // 15
echo $acc->send(100); // 115
Тонкость: перед первым send() генератор нужно "завести" через current() или rewind(), иначе первое переданное значение потеряется — генератор ещё не дошёл до yield. Это частая ошибка, из-за которой первый элемент молча пропадает.
Где генераторы ломаются
Ленивость даёт экономию памяти, но забирает свойства обычной коллекции. Что перестаёт работать:
- count() не работает. Генератор не знает, сколько элементов отдаст, пока не отдаст их все. Считать придётся вручную в цикле или через
iterator_count(), но последний исчерпает генератор. - Нет доступа по индексу.
$gen[5]вызовет ошибку. Генератор — это поток, а не массив. - Повторный перебор невозможен. Отдали значения один раз — всё. Нужен второй проход — либо создавайте генератор заново вызовом функции, либо соберите в массив (и тогда экономия памяти теряется).
- Исключение внутри рвёт весь поток. Если генератор бросил исключение на 400-тысячной строке, всё, что не успели обработать, потеряно. Обрабатывайте ошибки внутри тела генератора, если хотите продолжить перебор.
- return в генераторе. В PHP 7+
return $value;в теле генератора задаёт финальное значение, доступное черезgetReturn()после завершения перебора. Внутриforeachоно не появляется — легко забыть и потерять данные.
Когда брать генератор, а когда массив
Простое правило: генератор для одноразового потока, массив для коллекции, к которой обращаются повторно.
| Критерий | Генератор | Массив |
|---|---|---|
| Объём данных | Большой, не влезает в память | Помещается в память |
| Число проходов | Один | Любое |
| Доступ по индексу | Нет | Есть |
| Известно количество заранее | Нет | Да, через count() |
| Источник данных | Файл, курсор БД, API постранично | Готовая структура в коде |
| Функции массива (sort, array_map) | Не применимы напрямую | Работают |
Отдельный частый вопрос — выгрузка из базы. PDO по умолчанию буферизует весь результат в памяти. Чтобы генератор реально экономил память при чтении из БД, нужен небуферизованный режим для MySQL: атрибут PDO::MYSQL_ATTR_USE_BUFFERED_QUERY в false. Иначе весь результат уже в памяти, и yield по строкам ничего не сэкономит — вы просто отдаёте по одной строке из уже загруженного набора. Точное поведение драйверов сверяйте в документации PDO.
Что делать дальше
Проверьте на своём проекте: возьмите скрипт, который собирает большой массив через fetchAll() или file(), замерьте memory_get_peak_usage(true) до и после перевода на генератор. На выгрузках в сотни тысяч строк разница обычно кратная. Дальше посмотрите на связку генераторов с Fibers (PHP 8.1+) для асинхронных сценариев и на компонент Symfony\Component\HttpFoundation\StreamedResponse, если отдаёте большие ответы прямо в HTTP-поток — это естественное продолжение ленивой обработки данных.
Генератор планировок: Набросок комнаты карандашом → 3D-рендер дизайна.
Спросить за 20 ₽Источники: PHP Manual: Generators, PHP Manual: PDO::setAttribute
Частые вопросы
У меня PHP 7.4, будут ли работать эти примеры?
Почему у меня генератор отдаёт только один элемент, а дальше пусто?
Как посчитать количество элементов в генераторе?
Генератор реально снизит память при чтении из MySQL?
Что будет с открытым файлом, если я прерву foreach через break?
Можно ли применить array_map или sort к генератору?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.