Генератор — функция, которая отдаёт поток значений

Генератор в PHP — это функция с yield. Снаружи она выглядит как источник данных для foreach, но внутри не строит весь массив заранее. При каждом yield функция отдаёт очередное значение и ставит своё выполнение на паузу: локальные переменные, текущая строка и состояние цикла сохраняются до следующего шага.

<?php

declare(strict_types=1);

function ids(int $from, int $to): Generator
{
    for ($id = $from; $id <= $to; $id++) {
        yield $id;
    }
}

foreach (ids(100, 103) as $id) {
    echo $id . PHP_EOL;
}

Вызов ids(100, 103) не выполняет тело функции сразу до конца. Он возвращает объект Generator; выполнение начинается, когда код реально начинает итерацию. Это роднит генераторы с SPL, итераторы и коллекции, но писать отдельный класс Iterator для простого потока значений обычно не нужно.

flowchart TD A[Вызов generator function] --> B[Возвращается объект Generator] B --> C[foreach запрашивает первое значение] C --> D[Код выполняется до yield] D --> E[Значение отдано наружу, состояние сохранено] E --> F{Нужно следующее значение?} F -- да --> G[Продолжить после yield] G --> D F -- нет --> H[Итерация завершена] H --> I[finally освобождает ресурсы, если он есть]
flowchart TD
    A[Вызов generator function] --> B[Возвращается объект Generator]
    B --> C[foreach запрашивает первое значение]
    C --> D[Код выполняется до yield]
    D --> E[Значение отдано наружу, состояние сохранено]
    E --> F{Нужно следующее значение?}
    F -- да --> G[Продолжить после yield]
    G --> D
    F -- нет --> H[Итерация завершена]
    H --> I[finally освобождает ресурсы, если он есть]
Жизненный цикл генератора: вызов функции создаёт объект, а тело выполняется только во время итерации.
Quick recall
Почему вызов generator function вроде `ids(100, 103)` сам по себе ещё не выполняет весь цикл внутри функции?

yield вместо большого массива

Классический пример — обработка большого файла, API-выгрузки или результата постраничного запроса. Если функция возвращает массив, она должна сначала собрать всё в память. Если функция отдаёт генератор, потребитель получает строки по одной.

<?php

function readLines(string $path): Generator
{
    $handle = fopen($path, 'rb');

    if ($handle === false) {
        throw new RuntimeException('Не удалось открыть файл');
    }

    try {
        while (($line = fgets($handle)) !== false) {
            yield rtrim($line, "\r\n");
        }
    } finally {
        fclose($handle);
    }
}

foreach (readLines(__DIR__ . '/events.log') as $line) {
    if (str_contains($line, 'ERROR')) {
        echo $line . PHP_EOL;
    }
}

finally здесь важен по той же причине, что и в Ошибки, исключения и Throwable: ресурс нужно закрыть и при нормальном завершении, и при break, и при исключении. Генератор не отменяет правила работы с ресурсами; он только меняет способ доставки значений.

Quick recall
В каком случае генератор с `yield` лучше функции, которая сначала собирает и возвращает большой массив?

Ключи, yield from и getReturn()

yield может отдавать не только значение, но и ключ. Синтаксис похож на ассоциативный массив, что естественно для PHP, где массив — ordered map; это напрямую связано с Массивы как ordered map.

<?php

function statusLabels(): Generator
{
    yield 'new' => 'Новая';
    yield 'paid' => 'Оплачена';
    yield 'cancelled' => 'Отменена';
}

foreach (statusLabels() as $code => $label) {
    echo "$code: $label" . PHP_EOL;
}

yield from делегирует часть потока другому массиву, Traversable-объекту или генератору. Это удобно для композиции: один генератор собирает общий поток из нескольких маленьких источников.

<?php

function baseEvents(): Generator
{
    yield 'app.started';
    yield 'cache.warmed';
}

function allEvents(): Generator
{
    yield 'boot';
    yield from baseEvents();
    yield 'ready';
}

Ловушка: yield from сохраняет ключи внутреннего источника. Если потом вызвать iterator_to_array($generator) с настройкой по умолчанию, одинаковые ключи могут перезаписать значения. Когда нужны просто все значения подряд, используйте iterator_to_array($generator, false).

Генератор также может завершиться через return, а возвращённое значение доступно через $generator->getReturn() после полного завершения итерации. Это не замена обычным yielded values, а отдельный финальный результат: например, счётчик обработанных строк или checksum.

Quick recall
Что произойдёт с ключами при `yield from`, если внутренний источник отдаёт свои ключи?

Generator как объект

Generator — встроенный final-класс, который нельзя создать через new. Его создаёт PHP при вызове функции с yield. Объект реализует Iterator и имеет методы current(), key(), next(), rewind(), valid(), а также send(), throw() и getReturn().

В обычном прикладном коде чаще всего достаточно foreach. Ручное управление через методы нужно для низкоуровневых итераторов, парсеров и старого coroutine-стиля, где значение можно «отправить обратно» в генератор:

<?php

function conversation(): Generator
{
    $name = yield 'Как вас зовут?';
    yield "Привет, $name";
}

$gen = conversation();
echo $gen->current() . PHP_EOL;
echo $gen->send('Анна') . PHP_EOL;

Такой стиль существует, но в современном PHP его не стоит путать с полноценной асинхронностью. Генератор сам по себе не делает I/O неблокирующим и не запускает код параллельно. Он даёт ленивую итерацию и точку паузы на уровне yield.

Fiber — пауза всего стека вызовов

Fiber появился в PHP 8.1. Это уже не итератор, а «full-stack interruptible function»: выполнение можно приостановить внутри глубоко вложенного вызова, а затем продолжить позже. В отличие от генератора, функциям между стартом Fiber и местом Fiber::suspend() не нужно менять return type на Generator и протаскивать yield через весь стек.

<?php

$fiber = new Fiber(function (): string {
    echo "A\n";

    $value = Fiber::suspend('paused');

    echo "B: $value\n";

    return 'done';
});

$result = $fiber->start();
echo "start returned: $result\n";

$fiber->resume('resume value');
echo "return: " . $fiber->getReturn() . "\n";

Вывод будет таким:

A
start returned: paused
B: resume value
return: done

start() запускает fiber и возвращает значение, переданное в Fiber::suspend(). resume() продолжает выполнение и передаёт значение обратно в место остановки. Если нужно продолжить выполнение исключением, есть Fiber::throw(Throwable $exception). Состояние можно проверять методами isStarted(), isSuspended(), isRunning() и isTerminated().

Где Fiber нужен на практике

Сам по себе Fiber — низкоуровневый механизм. В обычном контроллере, CLI-скрипте или модели его почти никогда не создают руками. Его сила проявляется в библиотеках: event loop, HTTP-клиентах, очередях, WebSocket-серверах, runtime вроде Swoole/OpenSwoole или долгоживущих воркерах. На верхнем уровне библиотека может дать API, похожий на синхронный код, а внутри переключать fiber, пока операция ждёт сеть, таймер или другой неблокирующий источник.

Важно не переоценить механизм. Fiber не превращает блокирующий file_get_contents() или обычный PDO-запрос в неблокирующую операцию. Если внутри fiber вызвать блокирующий системный вызов, процесс всё равно будет ждать. Для реальной конкурентности нужны неблокирующие драйверы, event loop и дисциплина на границах I/O. Поэтому эта тема естественно продолжается в Асинхронный PHP и event loop, Swoole и OpenSwoole и PHP-FPM, RoadRunner и долгоживущие воркеры.

Генератор или Fiber

Генератор выбирают, когда нужен поток значений: пройти большой файл, лениво сгенерировать диапазон, составить pipeline обработки, отдать данные в foreach. Его главный контракт — iterable.

Fiber выбирают не как коллекцию, а как механизм планирования выполнения. Он нужен, когда библиотека хочет прервать стек вызовов и продолжить его позже, не заставляя каждую промежуточную функцию становиться генератором. Его главный контракт — управление состоянием выполнения.

Если коротко: yield отвечает на вопрос «какое следующее значение?», а Fiber::suspend() — «где остановить эту ветку выполнения, пока рантайм займётся чем-то другим?».

Типичные ловушки

Первая ловушка — думать, что генератор ускоряет код сам по себе. Он прежде всего экономит память и позволяет обрабатывать данные лениво. По CPU он может быть сопоставимым или даже чуть дороже простого массива на малых объёмах.

Вторая — повторно итерировать один и тот же Generator. В отличие от массива, генератор обычно одноразовый: после прохода он завершён. Если нужен новый проход, вызовите генераторную функцию заново.

Третья — забывать про ключи при yield from и iterator_to_array(). Для списков часто нужен второй аргумент false.

Четвёртая — считать Fiber потоками. Это не threads и не preemptive multitasking. Переключение кооперативное: код должен дойти до точки suspend или до операции, которую библиотека умеет оборачивать.

См. также

Sources

  1. PHP Manual: Generators overview
  2. PHP Manual: The Generator class
  3. PHP Manual: Generator syntax
  4. PHP Manual: Fibers
  5. PHP Manual: The Fiber class