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ń.
