Генераторы и yield в PHP: что выбрать на практике
Массивы загружают весь результат в память, Iterator требует больше кода, а Generator отдаёт значения по одному. Сравниваем три подхода в PHP 8.3+ и разбираем, где yield ускоряет обработку, а где только усложняет код.

Если PHP-скрипт читает 2 млн строк CSV, обычный массив пытается удержать весь результат в памяти, а генератор отдаёт строки по одной. Но yield не делает любой код быстрее: он меняет момент вычисления данных и добавляет ограничения на повторный проход. В PHP 8.3+ выбор между массивом, собственным Iterator и Generator стоит делать по объёму данных, числу проходов и цене усложнения кода.
Три способа вернуть набор данных
В PHP один и тот же источник можно представить тремя способами. Массив сразу содержит все значения. Объект-итератор хранит состояние обхода явно. Генератор описывает, как получить следующее значение, а PHP сам управляет состоянием функции между вызовами yield.
| Подход | Когда формируются данные | Повторный проход | Цена |
|---|---|---|---|
| Массив | До передачи результата вызывающему коду | Да, пока массив существует | Память под весь набор |
Iterator | По правилам, которые задаёт класс | Зависит от реализации | Больше кода и состояний |
Generator | При переходе к очередному элементу | Нет, генератор одноразовый | Низкое потребление памяти, но поток нельзя свободно перематывать |
Главное преимущество генератора — не «магическое ускорение PHP», а возможность не создавать результат целиком до того, как понадобится его первый элемент.
Генератор — объект, который PHP создаёт для функции с yield. Вызов такой функции возвращает не готовый набор данных, а управляемый поток значений.
Подход первый: массив для небольшого и повторно используемого результата
Массив остаётся лучшим вариантом, когда данных немного, их нужно сортировать или к ним обращаются по индексу. Он прост для отладки: результат можно вывести, посчитать, передать нескольким потребителям и пройти в цикле несколько раз.
<?php
declare(strict_types=1);
function activeStatuses(): array
{
return ['new', 'paid', 'shipped'];
}
$statuses = activeStatuses();
foreach ($statuses as $status) {
echo $status . PHP_EOL;
}
echo 'Количество: ' . count($statuses) . PHP_EOL;
Проблема появляется не из-за самого цикла, а в момент построения массива. Если источник — большой файл, результат SQL-запроса или последовательность вычислений, вся коллекция уже должна находиться в памяти до первого foreach.
Подход второй: свой Iterator, когда состояние нужно контролировать
Класс, реализующий Iterator, полезен, когда обход — часть самостоятельной модели: ему нужны сложные правила перехода, явный сброс, несколько связанных полей состояния или повторный запуск. За это вы платите шаблонными методами current, key, next, rewind и valid.
<?php
declare(strict_types=1);
final class NumberRange implements Iterator
{
private int $current;
public function __construct(
private readonly int $start,
private readonly int $end,
) {
$this->current = $start;
}
public function current(): int
{
return $this->current;
}
public function key(): int
{
return $this->current - $this->start;
}
public function next(): void
{
$this->current++;
}
public function rewind(): void
{
$this->current = $this->start;
}
public function valid(): bool
{
return $this->current <= $this->end;
}
}
$numbers = new NumberRange(3, 5);
foreach ($numbers as $number) {
echo $number . PHP_EOL;
}
В этом примере состояние обхода видно явно. Но для простого последовательного чтения класс из пяти обязательных методов создаёт больше поверхности для ошибок: неверный rewind или valid может проявиться только на втором проходе.
Подход третий: Generator и yield
Генератор подходит для однонаправленного потока: прочитать файл, обработать строки, передать записи в API или собрать статистику за один проход. Функция начинает выполнять тело не в момент вызова, а при первой попытке получить значение.
<?php
declare(strict_types=1);
/**
* PHP 8.3+: отдаёт строки файла по одной.
*/
function readNonEmptyLines(string $path): iterable
{
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException('Не удалось открыть файл: ' . $path);
}
try {
while (($line = fgets($handle)) !== false) {
$line = trim($line);
if ($line !== '') {
yield $line;
}
}
} finally {
fclose($handle);
}
}
foreach (readNonEmptyLines(__DIR__ . '/input.txt') as $line) {
echo $line . PHP_EOL;
}
Здесь в памяти находится текущая строка и состояние функции, а не весь файл. Блок finally закрывает дескриптор и при нормальном завершении, и при исключении во время обработки.
Почему yield from не заменяет обычный yield
yield from передаёт значения из другого итерируемого источника. Он удобен, когда генератор собирает поток из нескольких частей, но не превращает источник в массив и не позволяет повторно пройти уже использованный генератор.
<?php
declare(strict_types=1);
function numbersFrom(int $start, int $end): iterable
{
for ($number = $start; $number <= $end; $number++) {
yield $number;
}
}
function allNumbers(): iterable
{
yield from numbersFrom(1, 3);
yield from numbersFrom(7, 8);
}
foreach (allNumbers() as $number) {
echo $number . PHP_EOL;
}
Как выбрать подход
Начинайте не с вопроса «можно ли поставить yield», а с контракта данных. Важны размер результата, количество проходов и операции, которые понадобятся после чтения.
- Выбирайте массив, если результат небольшой, нужен по индексу, должен сортироваться или будет использован несколько раз.
- Выбирайте Generator, если источник большой или потенциально бесконечный, обработка идёт последовательно, а второй проход не нужен.
- Выбирайте Iterator, если объект должен иметь самостоятельный контракт обхода, явное состояние и поведение, которое стоит тестировать отдельно от функции.
Сравнение на типичной задаче
| Задача | Предпочтительный вариант | Почему |
|---|---|---|
| Список из десятков настроек | Массив | Простота важнее экономии на небольшом объёме |
| Обработка большого CSV один раз | Generator | Строки можно обработать без загрузки всего файла |
| Повторный обход вычисляемого диапазона | Iterator или фабрика нового Generator | Один Generator нельзя перемотать и запустить заново |
| Публичный объект с несколькими правилами обхода | Iterator | Состояние и контракт видны в классе |
Что ломается в реальном коде
Генератор используют второй раз
После полного прохода объект Generator нельзя просто перемотать. Повторный вызов rewind после начала выполнения приводит к ошибке. Если данные нужны дважды, создайте новый генератор из фабричной функции или сохраните результат в массив, если его размер приемлем.
В генератор передают ресурс с коротким сроком жизни
Открытый файл, курсор базы данных или сетевое соединение должны существовать всё время обхода. Нельзя открыть ресурс в вызывающем коде, закрыть его до foreach, а потом ожидать, что генератор продолжит работу.
Пытаются вызвать count или взять элемент по индексу
Тип iterable не означает массив. Для генератора нельзя напрямую применить count и нельзя обратиться к элементу через квадратные скобки. Если эти операции обязательны, материализуйте поток в массив — осознанно принимая расход памяти.
Скрывают побочный эффект за ленивым вычислением
Функция с yield не открывает файл и не выполняет запрос в базу при вызове. Ошибка может возникнуть только внутри foreach. Поэтому границы ресурса, обработку исключений и логирование проверяйте именно на полном проходе.
Считают yield заменой пакетной обработки
Генератор уменьшает объём данных в памяти, но не отменяет стоимость каждого SQL-запроса, сетевого вызова или преобразования. Если обработка медленная, измеряйте отдельно время чтения, преобразования и записи. Без таких замеров нельзя утверждать, что переход на yield ускорил приложение.
Следующий шаг
Возьмите один существующий метод, который возвращает большой массив, и зафиксируйте три показателя: объём входных данных, пиковое потребление памяти и время полного прохода. Затем перепишите только слой чтения на Generator, не меняя обработчик, и сравните результат на одинаковом наборе данных. Отдельно проверьте повторный запуск, исключение в середине потока и закрытие файла или курсора.
Инвалидация кеша, пакетная загрузка в базу и параллельная обработка — отдельные задачи. Сам по себе yield решает только вопрос последовательной выдачи значений.
Генератор изображений: Иллюстрация, обложка, баннер, схема — по текстовому описанию, без исходного фото.
Спросить за 20 ₽Источники: Официальная документация PHP: генераторы, Официальная документация PHP: Iterator
Частые вопросы
Генератор всегда использует меньше памяти, чем массив?
Можно ли пройти один Generator два раза?
Что выбрать для чтения большого CSV: Iterator или Generator?
Можно ли вызвать count() для Generator?
Почему ошибка внутри генератора появляется не при вызове функции?
Поддерживается ли yield в старых версиях PHP?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.