PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Iterator i Generator – leniwe przetwarzanie, yield, IteratorAggregate, benchmark pamięci

by Henryk Tews / wtorek, 02 grudnia 2025 / Opublikowano w Wzorce projektowe

Gdy masz 100 000 zamówień do eksportu albo milion wierszy do importu, wczytanie wszystkiego do tablicy to pewna śmierć przez wyczerpanie pamięci. PHP oferuje dwa eleganckie mechanizmy leniwego przetwarzania: interfejs Iterator dla klas które sami piszemy i Generator (yield) dla prostszego kodu który nie wymaga całej klasy. Pokazuję oba z praktycznymi przykładami z Magento 2.

Problem z tablicami przy dużych danych

<?php

// Klasyczne podejście – wszystko do pamięci
function exportAllOrders(): array
{
    return $orderRepository->getList($searchCriteria)->getItems();
    // 100 000 zamówień × ~5KB każde = ~500MB RAM
    // PHP memory limit exceeded -> fatal error
}

// Leniwe podejście – przetwarza po jednym
function exportAllOrdersLazy(): iterable
{
    $page     = 1;
    $pageSize = 100;

    do {
        $sc = $searchCriteriaBuilder
            ->setPageSize($pageSize)
            ->setCurrentPage($page)
            ->create();

        $result = $orderRepository->getList($sc);
        $items  = $result->getItems();

        foreach ($items as $order) {
            yield $order;  // Jeden obiekt w pamięci na raz
        }

        $page++;
    } while (count($items) === $pageSize);

    // Szczyt pamięci: ~5KB (jeden zamówienie) zamiast ~500MB
}

Generator – funkcja z yield

<?php

declare(strict_types=1);

// Generatory są najprostszym sposobem na leniwe sekwencje
// Gdy PHP napotka yield – zawiesza wykonanie funkcji

function fibonacci(): Generator
{
    [$a, $b] = [0, 1];

    while (true) {
        yield $a;
        [$a, $b] = [$b, $a + $b];
    }
}

// Pobierz pierwszych 10 liczb Fibonacciego – bez obliczania nieskończonej sekwencji
$fib = fibonacci();
for ($i = 0; $i < 10; $i++) {
    echo $fib->current() . ' ';
    $fib->next();
}
// 0 1 1 2 3 5 8 13 21 34

// Generator z kluczem i wartością
function readCsvLines(string $path): Generator
{
    $file = fopen($path, 'r');

    if ($file === false) {
        throw new \RuntimeException("Cannot open: {$path}");
    }

    try {
        $lineNumber = 0;
        while (($line = fgets($file)) !== false) {
            yield $lineNumber++ => trim($line);
        }
    } finally {
        fclose($file); // zawsze zamknij plik
    }
}

// Przetwarzanie 1GB pliku CSV z użyciem ~kilku KB RAM
foreach (readCsvLines('/data/orders-export.csv') as $lineNum => $line) {
    if ($lineNum === 0) continue; // header

    $row = str_getcsv($line);
    processOrderRow($row);
    // Jeden wiersz w pamięci na raz
}

Generator z yield from – kompozycja

<?php

declare(strict_types=1);

// yield from deleguje do innego generatora lub iterable

function ordersFromWarehouse(int $warehouseId): Generator
{
    $page = 1;
    do {
        $orders = fetchOrdersPage($warehouseId, $page++);
        foreach ($orders as $order) {
            yield $order;
        }
    } while (count($orders) > 0);
}

// Połącz zamówienia ze wszystkich magazynów w jeden strumień
function allOrders(array $warehouseIds): Generator
{
    foreach ($warehouseIds as $warehouseId) {
        yield from ordersFromWarehouse($warehouseId);
        // yield from transparentnie przekazuje każdy yielded element
        // jakby był bezpośrednio w tej funkcji
    }
}

// Użycie - jednolity strumień z wielu źródeł
$processed = 0;
foreach (allOrders([1, 2, 3, 4]) as $order) {
    processOrder($order);
    $processed++;
}

echo "Przetworzono: {$processed} zamówień\n";

Iterator Interface – pełna kontrola

<?php

declare(strict_types=1);

// Gdy generator nie wystarczy (potrzebujesz rewind, len, lub bardziej złożona logika)
// zaimplementuj interfejs Iterator

class PaginatedOrderIterator implements \Iterator
{
    private array $currentPage   = [];
    private int $currentPosition = 0;
    private int $page            = 1;
    private int $totalFetched    = 0;
    private bool $exhausted      = false;

    public function __construct(
        private \Magento\Sales\Api\OrderRepositoryInterface $repository,
        private \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder,
        private int $pageSize = 100,
        private ?string $statusFilter = null
    ) {}

    // Iterator interface - 5 metod
    public function rewind(): void
    {
        $this->page            = 1;
        $this->currentPosition = 0;
        $this->totalFetched    = 0;
        $this->exhausted       = false;
        $this->currentPage     = [];
        $this->loadPage();
    }

    public function current(): mixed
    {
        return $this->currentPage[$this->currentPosition] ?? null;
    }

    public function key(): int
    {
        return $this->totalFetched - count($this->currentPage) + $this->currentPosition;
    }

    public function next(): void
    {
        $this->currentPosition++;

        // Jeśli wyczerpaliśmy bieżącą stronę, załaduj następną
        if ($this->currentPosition >= count($this->currentPage) && !$this->exhausted) {
            $this->loadPage();
        }
    }

    public function valid(): bool
    {
        return isset($this->currentPage[$this->currentPosition]);
    }

    private function loadPage(): void
    {
        $builder = $this->criteriaBuilder
            ->setPageSize($this->pageSize)
            ->setCurrentPage($this->page);

        if ($this->statusFilter !== null) {
            $builder->addFilter('status', $this->statusFilter);
        }

        $result      = $this->repository->getList($builder->create());
        $items       = $result->getItems();

        $this->currentPage     = array_values($items);
        $this->currentPosition = 0;
        $this->totalFetched   += count($items);
        $this->page++;

        if (count($items) < $this->pageSize) {
            $this->exhausted = true;
        }
    }
}

// Użycie - działa ze foreach, ale też z Iterator-aware funkcjami
$iterator = new PaginatedOrderIterator($orderRepository, $criteriaBuilder, 50, 'pending');

foreach ($iterator as $key => $order) {
    echo "[{$key}] {$order->getIncrementId()}\n";
    processOrder($order);
}

// Możliwy rewind - możesz iterować wielokrotnie
foreach ($iterator as $order) {
    validateOrder($order); // drugie przejście po tych samych danych
}

IteratorAggregate – prostszy sposób niż Iterator

<?php

declare(strict_types=1);

// IteratorAggregate wymaga tylko jednej metody: getIterator()
// Deleguje do generatora lub istniejącego iteratora

class ProductCatalogExport implements \IteratorAggregate
{
    public function __construct(
        private \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory,
        private int $batchSize = 200
    ) {}

    public function getIterator(): \Generator
    {
        $collection = $this->collectionFactory->create();
        $collection->addAttributeToSelect(['sku', 'name', 'price', 'status']);
        $collection->setPageSize($this->batchSize);

        $lastPage = $collection->getLastPageNumber();

        for ($page = 1; $page <= $lastPage; $page++) {
            $collection->setCurPage($page)->load();

            foreach ($collection as $product) {
                yield [
                    'sku'    => $product->getSku(),
                    'name'   => $product->getName(),
                    'price'  => $product->getPrice(),
                    'active' => $product->getStatus() === 1,
                ];
            }

            $collection->clear(); // zwolnij pamięć między stronami
        }
    }

    // Bonus: możemy dodać metody klasy bez implementacji pełnego iteratora
    public function count(): int
    {
        return $this->collectionFactory->create()->getSize();
    }
}

// Użycie
$export = new ProductCatalogExport($collectionFactory, batchSize: 100);

echo "Produktów do eksportu: " . $export->count() . "\n";

$csvLines = [];
foreach ($export as $product) {
    $csvLines[] = implode(',', $product);
}

file_put_contents('/tmp/catalog.csv', implode("\n", $csvLines));

Benchmark – pamięć Generator vs tablica

<?php

// Porównanie użycia pamięci

// Tablica - wszystko w pamięci
$start = memory_get_usage();
$data  = range(1, 100_000);
$peak1 = memory_get_peak_usage() - $start;
echo "Tablica 100k int: " . number_format($peak1 / 1024, 0) . " KB\n";
// ok. 3500 KB

// Generator - leniwe
function lazyRange(int $from, int $to): Generator
{
    for ($i = $from; $i <= $to; $i++) {
        yield $i;
    }
}

$start = memory_get_usage();
$gen   = lazyRange(1, 100_000);
foreach ($gen as $value) {
    // przetwórz $value
}
$peak2 = memory_get_peak_usage() - $start;
echo "Generator 100k int: " . number_format($peak2 / 1024, 0) . " KB\n";
// ok. 1 KB - stała pamięć niezależnie od rozmiaru zbioru

Podsumowanie

Iterator i Generator to standardowe narzędzia PHP do obsługi dużych danych bez wyczerpania pamięci. Generator (yield) jest prostszy i wystarczy w 90% przypadków – używaj go przy eksporcie CSV, paginowanym pobieraniu z API czy strumieniowym przetwarzaniu plików. Interfejs Iterator ma sens gdy potrzebujesz rewind, lub gdy budujesz klasę która ma być wielokrotnie iterowana z różnych miejsc kodu. W Magento 2 obie techniki są naturalne przy pracy z dużymi kolekcjami produktów i zamówień.

About Henryk Tews

Co możesz przeczytać następne

CQRS – Command Bus, Query Bus, read models, integracja z Magento 2
Wzorzec State – maszyna stanów dla zamówienia, serializacja, porównanie ze Strategy
Event Sourcing – Domain Events, Aggregate Root, Event Store, połączenie z CQRS
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}