Wzorzec Iterator definiuje sposób sekwencyjnego dostępu do elementów kolekcji bez ujawniania jej wewnętrznej struktury. PHP implementuje go przez interfejsy Iterator i IteratorAggregate. Generatory (dostępne od PHP 5.5) to składniowy cukier który znacznie upraszcza pisanie iteratorów – szczególnie przy leniwym generowaniu dużych zbiorów danych.
Interfejs Iterator w PHP
<?php
declare(strict_types=1);
// Klasyczna implementacja Iteratora - kolekcja produktów
class ProductCollection implements Iterator
{
private array $products = [];
private int $position = 0;
public function add(array $product): void
{
$this->products[] = $product;
}
// Metody interfejsu Iterator
public function current(): mixed
{
return $this->products[$this->position];
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->position++;
}
public function rewind(): void
{
$this->position = 0;
}
public function valid(): bool
{
return isset($this->products[$this->position]);
}
}
$collection = new ProductCollection();
$collection->add(['sku' => 'SKU-001', 'name' => 'Widget', 'price' => 29.99]);
$collection->add(['sku' => 'SKU-002', 'name' => 'Gadget', 'price' => 49.99]);
$collection->add(['sku' => 'SKU-003', 'name' => 'Doohickey', 'price' => 9.99]);
foreach ($collection as $key => $product) {
echo "{$key}: {$product['sku']} - {$product['name']}\n";
}
// Kolekcja jest reużywalna - foreach możemy wywołać wielokrotnie
IteratorAggregate – prostsze podejście
<?php
declare(strict_types=1);
// IteratorAggregate - deleguje iterację do wewnętrznego iteratora
// Prostsze gdy kolekcja już ma strukturę iterowalną
class OrderRepository implements IteratorAggregate
{
private array $orders = [];
public function add(array $order): void
{
$this->orders[] = $order;
}
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->orders);
}
// Możesz dodać metody filtrujące zwracające nowy iterator
public function pending(): ArrayIterator
{
$pending = array_filter(
$this->orders,
fn($o) => $o['status'] === 'pending'
);
return new ArrayIterator(array_values($pending));
}
}
$repo = new OrderRepository();
foreach ($repo->pending() as $order) {
// przetwarzaj zamówienia pending
}
Generator – iterator bez boilerplate
Generator to funkcja z instrukcją yield. PHP automatycznie implementuje za nią interfejs Generator (który implementuje Iterator). Przydatny szczególnie gdy generowanie kolejnych elementów jest kosztowne lub zbiór jest za duży żeby trzymać całość w pamięci:
<?php
declare(strict_types=1);
// Generator Fibonacciego - nieskończony ciąg, leniwie generowany
function fibonacci(): Generator
{
[$a, $b] = [0, 1];
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
echo $fib->current() . ' ';
$fib->next();
}
// 0 1 1 2 3 5 8 13 21 34
// Praktyczny przykład: strumieniowe czytanie dużego CSV
// bez wczytywania całego pliku do pamięci
function readCsvRows(string $filePath): Generator
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new \RuntimeException("Cannot open: {$filePath}");
}
$headers = fgetcsv($handle);
try {
while (($row = fgetcsv($handle)) !== false) {
yield array_combine($headers, $row);
}
} finally {
fclose($handle);
}
}
// Przetwarzanie 100k wierszy CSV - stałe zużycie pamięci
foreach (readCsvRows('/tmp/products.csv') as $row) {
// $row to tablica asocjacyjna: ['sku' => '...', 'name' => '...', ...]
// Każdy wiersz jest czytany na żądanie - nie wszystkie naraz
processProduct($row);
}
// yield from - delegowanie do innego generatora
function mergeGenerators(Generator ...$generators): Generator
{
foreach ($generators as $gen) {
yield from $gen;
}
}
// Generator z kluczem i wartością
function indexedProducts(array $products): Generator
{
foreach ($products as $product) {
yield $product['sku'] => $product; // klucz => wartość
}
}
foreach (indexedProducts($products) as $sku => $product) {
echo "{$sku}: {$product['name']}\n";
}
Generatory a send() – dwukierunkowa komunikacja
<?php
// Generator może odbierać wartości przez send()
// Przydatne do budowania prostych korutyn
function logger(): Generator
{
$messages = [];
while (true) {
$message = yield count($messages); // yield zwraca liczbę zebranych wiadomości
if ($message === null) break;
$messages[] = $message;
echo "[LOG] {$message}\n";
}
return $messages; // getReturn() po zakończeniu generatora
}
$log = logger();
$log->current(); // inicjalizacja - dochodzimy do pierwszego yield
$count = $log->send('Zamówienie #123 przyjęte'); // 1
$count = $log->send('Płatność zatwierdzona'); // 2
$count = $log->send('Wysyłka zlecona'); // 3
$log->send(null); // koniec
$allMessages = $log->getReturn();
Iterator w Magento 2
Magento 2 intensywnie używa tego wzorca. Klasa Magento\Framework\Data\Collection implementuje IteratorAggregate – stąd można po niej iterować foreach. ResourceModel\Collection\AbstractCollection rozszerza to o lazy loading z bazy danych: kolekcja jest faktycznie ładowana dopiero przy pierwszej iteracji lub wywołaniu getItems(). Do pracy z dużymi zbiorami danych (np. reindeksacja) Magento używa BatchProviderInterface który wewnętrznie działa jak generator – pobiera dane stronami.
