Глава 52 · От основ к архитектуре
Итераторы, генераторы, SPL и Fibers: обработка больших наборов
Читаем товары по одному, различаем iterable и массив, бережём память и понимаем, почему Fiber сам не превращает блокирующую сеть в параллельную.
Выдавать товары по одному, когда их просит foreach
Каталог может состоять из большого файла или многих страниц API. Хотим обрабатывать следующую запись, не собирая весь каталог заранее. Генератор — функция, которая приостанавливается на yield и выдаёт очередное значение.
Что получится. Цикл получит Чай, затем Кофе. По сообщениям будет видно, когда выполняется каждая часть функции.
- products() создаёт объект генератора
- foreach начинает его исполнение
- yield отдаёт Чай и приостанавливает функцию
- После обработки foreach просит следующее
- Функция продолжается и отдаёт Кофе
<?php
declare(strict_types=1);
function products(): Generator
{
echo "Готовим Чай\n";
yield 'Чай';
echo "Готовим Кофе\n";
yield 'Кофе';
}
$products = products();
echo "Начинаем каталог\n";
foreach ($products as $title) {
echo 'Показали: ', $title, "\n";
}
Ожидаемый результат
Начинаем каталог
Готовим Чай
Показали: Чай
Готовим Кофе
Показали: Кофе
Сам вызов products ещё не выполнил тело до первого yield. Выполнение начинает потребитель. yield выдаёт элемент, а не завершает функцию как return. Generator — встроенный объект управления этой последовательностью.
| Запись в примере | Что делает каждый элемент |
|---|---|
yield 'Чай'; | Выдать одно значение и сохранить место продолжения. |
foreach ($products as $title) | Попросить очередной элемент и выполнить тело цикла для него. |
: Generator | Вызов возвращает объект генератора, не готовый массив строк. |
Экономия возникает, когда источник тоже читается постепенно
Генератор отдаёт запись, потребитель решает продолжать
Нужно просуммировать количество товаров, не собирая ещё один массив всех записей из страниц интеграции.
- Источник: две страницы
В учебном массиве лежат TEA × 2, COFFEE × 3 и CUP × 1.
- productRows проверяет очередную запись
Генератор проходит страницы и требует строковый sku и целое quantity.
- yield передаёт запись и ставит паузу
Потребитель получает одну строку, а позиция генератора сохраняется.
- foreach прибавляет quantity
После обработки цикл запрашивает следующую запись; генератор продолжает после yield.
- После последней записи: units = 6
Отдельный массив всех строк не создавался. Накопилось только итоговое число.
Сам источник примера уже находится в памяти. Генератор не отменяет эту загрузку: для большого импорта ещё и источник должен читать порциями. Fiber и yield сами по себе не делают блокирующий HTTP параллельным.
Если сначала file_get_contents загрузит гигантский файл целиком, yield не отменит эту затрату. Если источник — API, держите разумную страницу данных, выдавайте её записи, затем запрашивайте следующую. Генератор не даёт произвольный доступ по индексу; для маленького списка, который нужно сортировать и обходить многократно, массив может быть удобнее.
Прочитать подготовленный файл строка за строкой
<?php
declare(strict_types=1);
function productLines(string $path): Generator
{
$handle = fopen($path, 'r');
if ($handle === false) { throw new RuntimeException('Каталог недоступен'); }
try {
while (($line = fgets($handle)) !== false) {
$title = trim($line);
if ($title !== '') { yield $title; }
}
} finally {
fclose($handle);
}
}
foreach (productLines(__DIR__ . '/products.txt') as $title) {
echo 'Товар: ', $title, "\n";
}
Ожидаемый результат
Товар: Чай
Товар: Кофе
Создайте рядом products.txt с двумя строками Чай и Кофе; файл включён в учебную фикстуру. fopen открывает чтение, fgets берёт следующую строку, trim убирает перевод строки. finally освобождает поток при завершении генератора. Ограничивайте размер одной строки: поэлементный обход не делает любую отдельную запись маленькой.
Для повторного прохода создавать новый итератор
<?php
declare(strict_types=1);
class Catalog implements IteratorAggregate
{
public function getIterator(): Traversable
{
yield 'Чай';
yield 'Кофе';
}
}
$catalog = new Catalog();
foreach ($catalog as $title) { echo 'Список: ', $title, "\n"; }
foreach ($catalog as $title) { echo 'Этикетка: ', $title, "\n"; }
Ожидаемый результат
Список: Чай
Список: Кофе
Этикетка: Чай
Этикетка: Кофе
IteratorAggregate обещает getIterator. Каждый foreach просит новый обход; каждый вызов генераторного getIterator создаёт новый Generator. Если сохранить один завершённый генератор в поле и возвращать его снова, повторяемость исчезнет. Traversable — общий встроенный договор обхода, его обычно реализуют через Iterator или IteratorAggregate.
Структура данных выбирается по операции
<?php
declare(strict_types=1);
$queue = new SplQueue();
$queue->enqueue('Заказ 42 принят');
$queue->enqueue('Заказ 43 принят');
while (!$queue->isEmpty()) {
echo 'Отправить: ', $queue->dequeue(), "\n";
}
Ожидаемый результат
Отправить: Заказ 42 принят
Отправить: Заказ 43 принят
enqueue добавляет в конец, dequeue забирает из начала. Это очередь в памяти текущего процесса; слово «Отправить» обозначает полученную задачу, сеть здесь не вызывается. Для сохранения задач после сбоя нужен внешний надёжный брокер или таблица, а не просто SplQueue.
SPL предоставляет также стеки, кучи, итераторы каталогов и обёртки фильтрации. Генератор описывает последовательное вычисление, а очередь хранит ожидающие элементы. Это разные задачи; стоимость операций оценивайте для выбранной структуры и объёма.
Ошибка источника возникает в момент потребления
<?php
declare(strict_types=1);
function importedTitles(): Generator
{
yield 'Чай';
throw new RuntimeException('Вторая страница каталога недоступна');
}
$titles = importedTitles();
try {
foreach ($titles as $title) { echo 'Получен: ', $title, "\n"; }
} catch (RuntimeException $error) {
echo 'Импорт не завершён: ', $error->getMessage(), "\n";
}
Ожидаемый результат
Получен: Чай
Импорт не завершён: Вторая страница каталога недоступна
try охватывает foreach, где фактически продолжается генератор. Обернуть только importedTitles() было бы недостаточно. Уже обработанный Чай не исчезает при последующей ошибке: импорт должен выбрать атомарную запись либо сохраняемую закладку продолжения.
Ранний break останавливает потребителя, но сохранённый объект генератора может удерживать открытый ресурс. Освобождайте ссылки или задавайте явный жизненный цикл обёрткой. Преобразование iterator_to_array материализует результат и может вернуть прежнюю проблему памяти.
Fiber приостанавливает стек, но не ускоряет блокирующую сеть
Генератор естественно выдаёт последовательность. Fiber нужен, когда приостановить и продолжить требуется цепочку обычных вызовов. Он не является отдельным потоком ОС и сам не создаёт event loop. Блокирующий HTTP-запрос внутри Fiber останется блокирующим без подходящего неблокирующего транспорта и планировщика.
Сначала закончите последовательный импорт. Затем отдельный маршрут Fiber покажет start, suspend, resume, результат и ошибки состояния на точных шагах.
Попробуйте. Добавьте к Catalog третий товар Сахар. Останутся ли два прохода независимыми?
Разобрать решение
Добавьте ещё один yield в getIterator. Оба foreach снова вызовут getIterator и получат все три записи. Не заменяйте его полем с единственным заранее созданным Generator.
Дальше: полный договор языка и специальные случаи
Далее сохранены ключи yield, yield from, getReturn, send/throw, ссылки и полный Iterator-протокол. Это разные стороны управления потоком; каждый специальный шаг опирается на наблюдаемую паузу первого примера.
Разделы ниже сохраняют специальные правила официального PHP Manual: Обычный foreach по объекту перебирает доступные свойства, Генератор выдаёт ключи, но сборка массива может перезаписать запись, Элементы потока и итог работы: yield from и getReturn, Send и throw: передать ответ в приостановленный генератор, Генератор по ссылке: когда потребитель действительно меняет источник, Пять методов Iterator: как foreach получает очередную запись, Yield без значения: элемент null ещё не означает конец, Исключение проходит через yield from к приостановленному шагу.
Обычный foreach по объекту перебирает доступные свойства
Разработчик передал в foreach объект клиента и ожидал список заявок. Но объект без Iterator/IteratorAggregate не превращается в результат API. По умолчанию PHP перечисляет доступные из текущей области свойства. Это следует отличать от собственной коллекции.
- CatalogCard хранит title, warehouse, internalNote
- Внешний foreach видит только public title
- Внутренний foreach видит все три поля
- Список товаров отдельно от свойств одной карточки
<?php
declare(strict_types=1);
class CatalogCard
{
public string $title = 'Чай';
protected string $warehouse = 'Склад A';
private string $internalNote = 'Учебная заметка';
public function visibleNamesInside(): array
{
$names = [];
foreach ($this as $name => $value) {
$names[] = $name;
}
return $names;
}
}
$card = new CatalogCard();
foreach ($card as $name => $value) {
echo $name, ': ', $value, "\n";
}
echo implode(', ', $card->visibleNamesInside()), "\n";
Ожидаемый результат
title: Чай
title, warehouse, internalNote
Первый цикл выполняется снаружи: виден только title. Второй находится в методе самого класса: там доступны все три объявленных поля. Метод намеренно возвращает только их имена, а не внутренние значения. Изменение области выполнения меняет доступность.
Для публичного API перечисляйте поля явно или используйте JsonSerializable с определённым договором. Добавление нового public-поля иначе может незаметно поменять экспорт. Для обхода списка товаров создайте коллекцию с IteratorAggregate или перебирайте массив, полученный listMethod; foreach по самому API-клиенту не вызывает сеть.
Генератор выдаёт ключи, но сборка массива может перезаписать запись
Интеграция получила две страницы товаров. Каждая страница локально пронумерована от нуля. Если делегировать их через yield from, цикл увидит все товары, однако массив с повторными ключами вместит только последнее значение каждого ключа. Перед этим разберите первый yield и ключи массива.
Запись yield $key => $value выдаёт пару ключ/значение. Yield from $items делегирует перебор другому массиву или Traversable: это не автоматическая перенумерация. Сохранение всех значений или исходных ключей — отдельное решение вызывающего кода.
- Первая страница: 0 Чай, 1 Кофе
- Вторая страница: 0 Сок, 1 Вода
- foreach получает все четыре выдачи
- Массив с сохранением ключей оставляет последние 0 и 1
<?php
declare(strict_types=1);
function catalogPages(): Generator
{
yield from [0 => 'Чай', 1 => 'Кофе'];
yield from [0 => 'Сок', 1 => 'Вода'];
}
$seen = [];
foreach (catalogPages() as $key => $title) {
$seen[] = $key . ':' . $title;
}
echo implode(', ', $seen), "\n";
echo implode(', ', iterator_to_array(catalogPages())), "\n";
echo implode(', ', iterator_to_array(catalogPages(), false)), "\n";
Ожидаемый результат
0:Чай, 1:Кофе, 0:Сок, 1:Вода
Сок, Вода
Чай, Кофе, Сок, Вода
Каждый вызов catalogPages создаёт новый генератор; уже завершённый поток здесь не переиспользуется. Iterator_to_array по умолчанию сохраняет ключи и перезаписывает одинаковые. False собирает значения с новыми числовыми индексами. Это сознательный отказ от исходных ключей, а не исправление дубликатов ID товара.
Если ключ — настоящий ID, повторение может означать дублирование страниц или изменение данных во время обхода. Тогда нельзя молча выбросить проблему через false: проверьте ID и выберите политику конфликтов. Для экспорта без хранения всего результата foreach может сразу записывать строки в файл.
Yield без выражения выдаёт null. Автоматические числовые ключи удобны для последовательности, но при явных ключах и yield from не полагайтесь на глобальную уникальность. Генератор уменьшает память только пока потребитель не собирает все элементы обратно в большой массив.
Элементы потока и итог работы: yield from и getReturn
Экспорт отдаёт строки по одной, а после завершения хочет вернуть число обработанных записей. Обычный return не станет ещё одной строкой. Он завершит генератор и сохранит отдельный итог, доступный через getReturn.
- exportBatch выдаёт заголовок и товар
- yield from передаёт обе строки наружу
- return 1 завершает пакет
- getReturn внешнего генератора получает rows = 1
<?php
declare(strict_types=1);
function exportBatch(): Generator
{
yield 'ID;TITLE';
yield '42;Чай';
return 1;
}
function exportDocument(): Generator
{
$written = yield from exportBatch();
return ['rows' => $written];
}
$export = exportDocument();
foreach ($export as $line) {
echo $line, "\n";
}
echo 'Записей: ', $export->getReturn()['rows'], "\n";
Ожидаемый результат
ID;TITLE
42;Чай
Записей: 1
Yield from передаёт наружу две строки, а его собственное значение после завершения — return вложенного генератора. Внешний генератор оборачивает итог в массив. GetReturn читается только после нормального завершения; до него будет Exception, а не частичный счётчик.
Yield from можно делегировать массиву, Iterator или другому генератору. Полезный return в этом опыте принадлежит именно генератору. Если обход прервали или внутри возникла необработанная ошибка, не объявляйте экспорт успешным только потому, что часть строк уже получена.
В реальном файле выбирайте временный путь и завершайте публикацию результата только после успешного полного обхода и закрытия ресурса. Генератор не предоставляет транзакцию сам: уже записанные строки остаются внешним эффектом. Return/getReturn и yield from поддерживаются с PHP 7.0, поэтому доступны в базовой версии этого руководства.
Send и throw: передать ответ в приостановленный генератор
Обычно foreach только получает очередное значение. Иногда нужен управляемый диалог: вычислитель запросил количество, вызывающий код передал его и получил итог. Это расширенный протокол генератора, а не обязательный способ обрабатывать форму; обычная функция с параметром будет проще, если пауз нет.
- current получает запрос Нужно количество
- Генератор ждёт на yield
- send(3) передаёт число внутрь
- return 360 завершает расчёт, getReturn читает итог
<?php
declare(strict_types=1);
function quoteDialog(): Generator
{
$quantity = yield 'Нужно количество';
if (!is_int($quantity) || $quantity < 1 || $quantity > 10) {
throw new InvalidArgumentException('Ожидалось количество от 1 до 10');
}
return 120 * $quantity;
}
$dialog = quoteDialog();
echo $dialog->current(), "\n";
$dialog->send(3);
echo 'Итого: ', $dialog->getReturn(), "\n";
Ожидаемый результат
Нужно количество
Итого: 360
Current запускает выполнение до первого yield. Наружу выходит текст запроса; send(3) возобновляет код, и выражение yield внутри получает 3. Это разные направления данных. У send возвращаемое значение — следующий yield, если он есть; итог return в данном случае читается отдельно через getReturn.
<?php
declare(strict_types=1);
function previewDialog(): Generator
{
try {
yield 'Черновик готов';
return 'Подтверждён';
} catch (RuntimeException $error) {
return 'Отменён';
} finally {
echo "Локальный буфер освобождён\n";
}
}
$dialog = previewDialog();
echo $dialog->current(), "\n";
$dialog->throw(new RuntimeException('Учебная отмена'));
echo $dialog->getReturn(), "\n";
Ожидаемый результат
Черновик готов
Локальный буфер освобождён
Отменён
Throw внедряет исключение в текущий yield; catch генератора его перехватывает, finally выполняет локальную очистку, затем доступен итог. Если подходящего catch нет, ошибка возвращается вызывающему коду. Throw в уже закрытый генератор тоже выбросит переданное исключение вызывающему.
Send умеет сам начать ещё не запущенный генератор, но явный current в учебном протоколе делает точку паузы видимой. Не смешивайте произвольные next/send/foreach, не определив, кто владеет продвижением: легко пропустить шаг.
Это синхронные примеры без сети, потоков и параллельной работы. Возможность приостановиться сама по себе не делает HTTP асинхронным. Для приложения нужны договор сообщений, владение ресурсами и обработка отмены; иногда для такой задачи подходит Fiber или библиотека цикла событий, а иногда достаточно обычной функции.
Генератор по ссылке: когда потребитель действительно меняет источник
Обычно цикл по генератору получает значения. Если инструмент нормализации должен менять элементы уже существующего небольшого массива, можно намеренно вернуть ссылки. Это отдельный, более тесный договор владения: изменение в потребителе затронет источник.
- stock хранит coffee = -1
- Генератор выдаёт связь с этой ячейкой
- Потребитель присваивает 0
- В исходном stock теперь coffee = 0; unset разрывает связь переменной
<?php
declare(strict_types=1);
function &stockValues(array &$stock): Generator
{
foreach ($stock as &$quantity) {
yield $quantity;
}
unset($quantity);
}
$stock = ['tea' => 2, 'coffee' => -1];
foreach (stockValues($stock) as &$quantity) {
if ($quantity < 0) {
$quantity = 0;
}
}
unset($quantity);
echo $stock['tea'], ', ', $stock['coffee'], "\n";
Ожидаемый результат
2, 0
Первый & перед именем функции разрешает yield по ссылке; второй у параметра связывает исходный массив; & у foreach потребителя разрешает запись в полученный элемент. Unset разрывает оставшуюся после цикла связь переменной с последним элементом. Это не удаление самого элемента.
Обнуление отрицательного остатка здесь — видимое правило учебной нормализации, а не универсально правильная политика склада. В реальном импорте отрицательное число может быть ошибкой, которую нужно сохранить в отчёт и отклонить строку.
Генератор по ссылке не экономит память исходного массива: он уже существует. Чаще проще вернуть нормализованное значение отдельной функцией или явно изменить массив обычным foreach. Выбирайте ссылочный протокол лишь там, где вызывающий код понимает, что источник будет изменён; не применяйте его к общему read-only контракту коллекции.
Пять методов Iterator: как foreach получает очередную запись
Генератор скрывает позицию обхода внутри PHP. Если коллекция сама должна управлять курсором, можно явно выполнить интерфейс Iterator. Для понимания сначала возьмём два названия из обычного массива. Это больше кода, чем yield, зато каждый шаг foreach становится видимым.
Implements Iterator обещает пять методов. Rewind ставит начало; valid сообщает, существует ли текущая позиция; current отдаёт значение; key — его ключ; next передвигает позицию. Возвращаемые типы mixed у current/key разрешают разные виды данных, но наш договор выдаёт строковые названия и числовые позиции.
- rewind устанавливает позицию 0
- valid подтверждает существование элемента
- key/current дают 0 и Чай
- next передвигает позицию; проверка повторяется до конца
<?php
declare(strict_types=1);
class TitleCursor implements Iterator
{
private int $position = 0;
public function __construct(private array $titles) {}
public function rewind(): void { $this->position = 0; }
public function valid(): bool { return array_key_exists($this->position, $this->titles); }
public function current(): mixed { return $this->titles[$this->position]; }
public function key(): mixed { return $this->position; }
public function next(): void { $this->position++; }
}
$cursor = new TitleCursor(['Чай', 'Кофе']);
foreach ($cursor as $position => $title) {
echo $position, ': ', $title, "\n";
}
$cursor->rewind();
echo 'После rewind: ', $cursor->current(), "\n";
Ожидаемый результат
0: Чай
1: Кофе
После rewind: Чай
В примере вход — плотный список с индексами 0 и 1. Для произвольного ассоциативного массива такая позиционная логика неверна: потребуется список ключей или готовый ArrayIterator. Снаружи current следует вызывать только при valid; foreach соблюдает этот протокол.
Если два вложенных цикла используют этот же объект TitleCursor, они меняют одну position и мешают друг другу. IteratorAggregate с новым Iterator при каждом getIterator отделяет курсоры от самой коллекции. Именно это часто удобно для коллекции заказов, которую нужно обходить несколько раз.
Генератор нельзя произвольно перемотать после продвижения; вместо этого обычно создают новый вызовом функции. Iterator может поддерживать rewind, но сам обязан реализовать его честно: сетевой поток или файл не всегда безопасно начинать заново. Не обещайте повторяемость лишь по наличию интерфейса.
Для обычного массива не нужно писать свой Iterator: foreach уже работает. Он полезен, когда в проекте действительно есть отдельный протокол курсора, доступ к источнику по страницам или адаптация чужой коллекции. Выбирать между yield, IteratorAggregate и Iterator следует по владению состоянием и повторным обходам, а не по представлению, что более длинный класс всегда профессиональнее.
Yield без значения: элемент null ещё не означает конец
В потоковом обработчике пустой результат иногда допустим: например, шаг подготовки пока не выдал строку. Yield без выражения создаёт элемент null с очередным числовым ключом. Это реальная остановка генератора, а не return и не пропуск элемента.
После первого генератора разделим два вопроса: current возвращает текущее значение, valid отвечает, есть ли текущий элемент вообще. Если проверять только current !== null, обработчик потеряет законные null-элементы.
- Первый yield отдаёт null, valid = true
- Next продолжает функцию до второго yield
- Второй элемент содержит строку
- После завершения valid = false, хотя current тоже null
<?php
declare(strict_types=1);
function preparationSteps(): Generator
{
yield;
yield 'Отчёт подготовлен';
}
$steps = preparationSteps();
while ($steps->valid()) {
$value = $steps->current();
echo $steps->key(), ': ', $value === null ? '(нет значения)' : $value, "\n";
$steps->next();
}
echo $steps->valid() ? "Есть элемент\n" : "Генератор завершён\n";
echo $steps->current() === null ? "Current после конца: null\n" : "Есть значение\n";
Ожидаемый результат
0: (нет значения)
1: Отчёт подготовлен
Генератор завершён
Current после конца: null
Первый valid запускает тело до первого yield. В обычном коде foreach уже выполняет эту работу. Bare yield не ожидает сеть и не запускает параллельную задачу самостоятельно: он только отдаёт управление потребителю.
Для передачи понятных событий пользователю лучше явные значения вроде массива с type=prepared или отдельного enum. Null уместен там, где его значение уже согласовано между генератором и потребителем. Упражнение: замените первый yield на return. Решение: ни первого, ни второго элемента не будет, потому что return завершит выполнение до второго yield.
Исключение проходит через yield from к приостановленному шагу
Импорт делится на этапы: внешняя функция организует процесс, внутренняя выдаёт очередную запись. Yield from делегирует не только значения: исключение, переданное внешнему генератору, попадает в текущую точку yield внутреннего. Это полезно для управляемой отмены, если ваш обработчик действительно использует такой протокол.
Внутренний finally завершает свой участок, а внешний catch решает, что сообщить вызывающему коду. До этого опыта нужны send и throw и обычный try/finally. Для простого обхода CSV достаточно foreach и обычной обработки исключения.
- Current доходит до yield внутреннего генератора
- Внешний throw передаёт RuntimeException внутрь
- Внутренний finally освобождает свой участок
- Внешний catch выдаёт понятное событие отмены
<?php
declare(strict_types=1);
function importRows(array &$events): Generator
{
try {
yield 'Строка P-42';
yield 'Строка P-43';
} finally {
$events[] = 'Внутренний этап закрыт';
}
}
function runImport(array &$events): Generator
{
try {
yield from importRows($events);
} catch (RuntimeException $error) {
$events[] = 'Внешний этап обработал отмену';
yield 'Импорт отменён';
}
return 'Завершено';
}
$events = [];
$job = runImport($events);
echo $job->current(), "\n";
echo $job->throw(new RuntimeException('Остановить импорт')), "\n";
$job->next();
echo $job->getReturn(), "\n";
echo implode(' / ', $events), "\n";
Ожидаемый результат
Строка P-42
Импорт отменён
Завершено
Внутренний этап закрыт / Внешний этап обработал отмену
Вторая строка не выдаётся, потому что выполнение внутреннего генератора прервано исключением. Throw вернул значение следующего yield, а итоговый return получен отдельно после завершения. Если catch отсутствует в обоих уровнях, исключение выходит к коду, вызвавшему throw.
При yield from из массива нет внутренней PHP-функции с try/catch: исключение обрабатывается во внешнем генераторе. Send в делегирующей цепочке генераторов тоже передаётся к текущему внутреннему yield. Уже закрытому генератору throw не создаёт новую жизнь: исключение выбрасывается в контексте вызывающего кода.
Упражнение: перенесите catch внутрь importRows и выдайте там «Строка пропущена». Решение: внутренний генератор сам решит, продолжать ли следующие строки; внешний catch не сработает для уже обработанного исключения. Место catch задаёт границу восстановления.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
IteratorAggregate::getIteratorВыдать объект обхода коллекции.
public function getIterator(): TraversableВыдать объект обхода коллекции.
| Часть договора | Что означает |
|---|---|
| Аргументы | Нет. |
| Результат | Объект Traversable: например ArrayIterator или Generator, не обычный массив. |
| Ошибки | Реализация может выбросить исключение при подготовке; неверный результат нарушает тип/договор. |
- ProductList хранит товары
- getIterator() создаёт ArrayIterator
- foreach использует курсор, не раскрывая private-массив
<?php
declare(strict_types=1);
class ProductList implements IteratorAggregate {
public function __construct(private array $items) {}
public function getIterator(): Traversable {
return new ArrayIterator($this->items);
}
}
$products = new ProductList(['A', 'B']);
$first = $products->getIterator();
$second = $products->getIterator();
$first->next();
echo $first->current(), ':', $second->current(), "\n";
var_dump($first === $second);
Ожидаемый результат
B:A
bool(false)
Именно new в нашей реализации делает курсоры независимыми. Интерфейс не запрещает вернуть ранее созданный объект, но тогда повторный/вложенный обход разделит состояние.
<?php
class Labels implements IteratorAggregate {
public function getIterator(): Traversable {
yield 'A' => 'Товар A';
yield 'B' => 'Товар B';
}
}
$labels = new Labels();
foreach ($labels as $sku => $label) { echo $sku, ':', $label, "\n"; }
Ожидаемый результат
A:Товар A
B:Товар B
Метод с yield возвращает Generator, который реализует Traversable. Он не возвращает готовый массив. Каждый новый вызов метода создаёт новый генератор; удалённый источник при этом может измениться между обходами.
Связанные методы: Iterator::rewind, Iterator::current, Iterator::valid. Карта интерфейсов.
Метод возвращает объект Traversable, например Generator или ArrayIterator. Для повторяемой коллекции создавайте новый итератор при каждом вызове, чтобы обходы не делили курсор.
<?php
declare(strict_types=1);
final class Skus implements IteratorAggregate {
public function getIterator(): Traversable { yield 'TEA'; yield 'CUP'; }
}
$skus = new Skus();
echo implode(',', iterator_to_array($skus)), "\n";
echo implode(',', iterator_to_array($skus)), "\n";
Ожидаемый результат
TEA,CUP
TEA,CUP
Каждый обход получает новый Generator.
<?php
declare(strict_types=1);
final class Roles implements IteratorAggregate {
public function __construct(private array $roles) {}
public function getIterator(): Traversable { return new ArrayIterator($this->roles); }
}
foreach (new Roles(['reader','editor']) as $role) { echo $role, "\n"; }
Ожидаемый результат
reader
editor
ArrayIterator подходит, когда массив уже существует; он не превращает загрузку массива в потоковую.
Рядом: iterator_to_array, iterator_count.
WeakMapХранить метаданные по объекту, не удерживая ключ живым.
new WeakMap(); $map[$object] = $value; // object keysНачальное объяснение WeakMap и полный справочник всех методов
Ключом является объект по идентичности. Запись исчезает, когда объект-ключ больше ничем не удерживается. Полезно для кэша вычисленных метаданных о временных объектах.
<?php
declare(strict_types=1);
$checks = new WeakMap();
$product = (object) ['sku' => 'TEA'];
$checks[$product] = 'Название и цена проверены';
echo $product->sku, ': ', $checks[$product], "\n";
unset($product);
echo count($checks) === 0 ? "Временные результаты освобождены\n" : "Объект ещё удерживается\n";
Ожидаемый результат
TEA: Название и цена проверены
Временные результаты освобождены
Ключом служит сам объект товара. После unset последней сильной ссылки запись исчезает. Это локальные сведения о текущем экземпляре, а не запись результата проверки в БД; бизнес-статус нужно сохранять отдельно.
<?php
declare(strict_types=1);
$old = (object) ['sku' => 'TEA', 'title' => 'Чай'];
$new = (object) ['sku' => 'TEA', 'title' => ''];
$checks = new WeakMap();
$checks[$old] = 'Можно публиковать';
foreach ([$old, $new] as $product) {
echo $product->sku, ': ', $checks[$product] ?? 'Нужно проверить новый снимок', "\n";
}
Ожидаемый результат
TEA: Можно публиковать
TEA: Нужно проверить новый снимок
Одинаковый sku не делает два PHP-объекта одним ключом. Новый снимок с пустым названием не должен получить старое разрешение публикации. Если нужен кэш по артикулу, используйте явный ключ и правило инвалидирования, а не WeakMap.
Рядом: spl_object_id, clone.
Fiber::suspendПриостановить текущий Fiber и отдать значение его вызывающему коду.
Fiber::suspend(mixed $value = null): mixedПриостановить текущий Fiber и отдать значение его вызывающему коду.
| Параметр / результат | Смысл |
|---|---|
| $value | Промежуточное значение, которое вернёт активный start/resume/throw снаружи. |
| Возвращает внутри | Значение последующего resume. При throw в этой точке выбрасывается переданное исключение. |
| Без Fiber | Вызов из основного кода даёт FiberError. |
| Глубина | Можно вызывать из вложенной обычной функции; приостанавливается весь стек данного Fiber. |
- suspend("Нужен остаток") → наружу строка
- Управляющий код выбирает ответ 5
- resume(5) → внутрь число
- Выражение suspend возвращает 5 → $stock
<?php
$task = new Fiber(function () {
$stock = Fiber::suspend('Нужен остаток');
echo 'Получено: ', $stock, "\n";
});
echo $task->start(), "\n";
$task->resume(5);
Ожидаемый результат
Нужен остаток
Получено: 5
В момент suspend тело не завершено. При resume выполнение возвращается в ту же функцию, с прежними локальными значениями.
<?php
try { Fiber::suspend('запрос'); }
catch (FiberError $error) { echo "Вызов вне Fiber\n"; }
Ожидаемый результат
Вызов вне Fiber
Так часто происходит, если suspend по ошибке вызван после возврата start в основном коде. Чтобы функция могла работать в двух режимах, спроектируйте явный синхронный/асинхронный адаптер; не подменяйте ошибку случайным значением.
Рядом: Fiber::getCurrent, Fiber::resume, Fiber::throw. Карта состояний.
Возвращает управление к start/resume/throw с переданным значением. После возобновления выражение suspend получает значение, переданное resume. Вне Fiber возникает FiberError.
<?php
declare(strict_types=1);
$f=new Fiber(function(): string { $response=Fiber::suspend('need-rate'); return 'rate='.$response; });
echo $f->start(), "\n"; $f->resume(300);
echo $f->getReturn(), "\n";
Ожидаемый результат
need-rate
rate=300
Локальная передача управления, без сети и без обещания параллельности.
<?php
declare(strict_types=1);
try { Fiber::suspend('work'); } catch (FiberError $e) { echo "Нужен контекст Fiber\n"; }
Ожидаемый результат
Нужен контекст Fiber
Обычный контроллер не может просто вызвать suspend без созданного и запущенного Fiber.
Рядом: IteratorAggregate::getIterator, __invoke.