Генераторы в php

Share

Что такое генераторы?

Генератор — это «умный список», который не хранит все элементы в памяти сразу, а создает их по одному только в тот момент, когда они вам понадобились.


Представьте, что вам нужно обработать 1 миллион коробок, которые стоят на конвейере.

  1. Обычный список (Список): Вы берете все 1 000 000 коробок сразу и складываете их в одну огромную комнату (память компьютера). Если коробок слишком много, комната переполнится (ошибка MemoryError).
  2. Генератор (Коробки на конвейере): У вас есть человек, который знает правило: «взять коробку, наклеить стикер, передать дальше». Он берет только одну коробку, обрабатывает её, отдает вам, а затем забывает про неё и берет следующую.

Практический пример: Обработка 100 000 товаров

Представь систему маркетплейса. Нужно выгрузить из базы 100 000 товаров, применить скидку и подготовить данные для API. Если загрузить их в массив разом — сервер упадет с ошибкой Memory limit exceeded.

Используем класс-процессор с генератором:

class ProductProcessor {
/**
* Имитация запроса к БД.
* Вместо создания массива на 1 ГБ, мы возвращаем генератор.
*/
private function getProductsFromDb(): \Generator {
// В реальности здесь был бы SQL запрос и цикл fetch()
for ($i = 1; $i <= 100000; $i++) {
// Функция "замирает" на yield, отдавая одну строку
yield [
'id' => $i,
'name' => "Товар #$i",
'price' => rand(100, 1000)
];
}
}

/**
* Трансформация данных "на лету".
*/
public function getDiscountedProducts(int $discountPercent): \Generator {

//Пример если нужна модификация данных полученных из бд
foreach ($this->getProductsFromDb() as $product) {
// Математика происходит только для одного товара в текущий момент
$product['price'] -= ($product['price'] * $discountPercent / 100);
$product['processed_at'] = date('Y-m-d H:i:s');

// Отдаем готовый товар и снова "засыпаем"
yield $product;
}
}
}

Использование

$processor = new ProductProcessor();
$stream = $processor->getDiscountedProducts(15); // Скидка 15%

echo "Запуск потоковой обработки...\n";

foreach ($stream as $product) {
// В памяти всегда находится только ОДИН товар, а не 100 000
echo "ID: {$product['id']} | Цена со скидкой: {$product['price']}\n";
// Мы можем остановить процесс в любой момент без потерь
if ($product['id'] >= 5) break;
}

Ключевые отличия и правила

Разница между ними — это разница между увольнением и отпуском.

1. return — «Всё, я увольняюсь!»

Когда функция встречает return, она отдает результат и полностью завершает работу. Все её внутренние переменные и состояние стираются. Если вы вызовете её снова, она начнет всё с чистого листа.

Итог: Возвращает значение (список, число, строку).

2. yield — «Я на паузе, скоро вернусь»

Когда функция встречает yield, она отдает результат, но не умирает. Она «замораживается» в этой точке, запоминая значения всех переменных. Когда вы попросите следующую порцию данных, она «просыпается» и продолжает работать с того же места.

Итог: Возвращает генератор.


  1. Экономия памяти: генератор потребляет одинаковое (минимальное) количество памяти, обрабатываешь ты 10 элементов или 10 миллиардов.
  2. Одноразовость: генератор — это поток. Его нельзя «перемотать» назад (как массив через reset()). Если данные закончились, или ты прервал цикл, нужно создавать объект генератора заново.
  3. Скорость: генератор начинает выдавать данные мгновенно. Тебе не нужно ждать, пока сформируется весь огромный список, чтобы начать его обрабатывать.