Inhalt
- 1. GoF Iterator Pattern: Das Interface
- 2. PHP Iterator Interface und IteratorAggregate
- 3. Varien_Data_Collection: Das Fundament
- 4. Magento AbstractCollection und Lazy Loading
- 5. Collection-Filter, Joins und Paginierung
- 6. Chunked Iterator: Große Kataloge ohne Memory-Overflow
- 7. PHP Generators als lazy Iteratoren
- 8. Eigener Custom-Iterator für Magento
- 9. Iterator vs. getItems(): Performance-Vergleich
- 10. Fazit: Collections richtig nutzen
Das Iterator Pattern ist eines der häufigst genutzten GoF-Patterns in Magento — du verwendest es jeden Tag, wenn du über eine Collection iterierst. foreach ($productCollection as $product) — das ist Iterator Pattern in Aktion. Aber was passiert intern? Wann wird die DB-Query ausgeführt? Und wie vermeidest du Memory-Overflows bei 100.000 Produkten? Dieses Deep Dive zeigt die vollständige Kette von Varien_Data_Collection bis zu PHP-Generators.
1. GoF Iterator Pattern: Das Interface
Das GoF Iterator Pattern definiert ein Interface, das sequenziellen Zugriff auf Elemente einer Sammlung ermöglicht, ohne die interne Repräsentation offenzulegen.
+------------------+ uses +------------------+
| Client | ---------------------->| Iterator |
| | | + current() |
| foreach(coll) | | + key() |
+------------------+ | + next() |
| + rewind() |
| + valid() |
+------------------+
▲
|
+----------------------------------+
| Magento AbstractCollection |
| implements Iterator + Countable |
+----------------------------------+
Die vier Vorteile des Iterator Patterns:
- Entkopplung: Der Client muss nicht wissen, ob es ein Array, eine DB-Query oder ein Remote-Stream ist
- Lazy Evaluation: Daten werden erst bei Bedarf geladen
- Einheitliche API:
foreachfunktioniert für alle Collections gleich - Composability: Iteratoren können dekoriert und kombiniert werden
2. PHP Iterator Interface und IteratorAggregate
PHP stellt zwei Wege bereit, um Objekte mit foreach traversierbar zu machen:
<?php
// Option 1: Iterator — Klasse implementiert Iterator direkt
// 5 Methoden müssen implementiert werden
class DirectIterator implements \Iterator
{
private int $position = 0;
private array $data = [];
public function current(): mixed { return $this->data[$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->data[$this->position]); }
}
// Option 2: IteratorAggregate — delegiert an einen Iterator
// Sauberer wenn die Klasse selbst keine Iteration darstellt
class Collection implements \IteratorAggregate
{
private array $items = [];
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->items);
}
}
// Option 3: Generator-basiert (PHP 5.5+)
// Laziest approach — perfekt für große Datensätze
function yieldProducts(array $ids): \Generator
{
foreach ($ids as $id) {
yield $id => loadProduct($id); // Lazy: nur bei Bedarf geladen
}
}
3. Varien_Data_Collection: Das Fundament
Magento\Framework\Data\Collection (früher Varien_Data_Collection) ist die Basisklasse aller Magento-Collections. Sie implementiert Iterator, Countable und ArrayAccess:
<?php
// vendor/magento/framework/Data/Collection.php (vereinfacht)
namespace Magento\Framework\Data;
use Magento\Framework\DataObject;
class Collection implements \IteratorAggregate, \Countable, \ArrayAccess
{
/** @var DataObject[] Internal storage — the actual pool of items */
protected array $_items = [];
private int $_curPos = 0;
private bool $_isCollectionLoaded = false;
/**
* PHP IteratorAggregate — used by foreach
* Triggers load() if not yet loaded
*/
public function getIterator(): \ArrayIterator
{
$this->load();
return new \ArrayIterator($this->_items);
}
/**
* Add an item to the collection.
*/
public function addItem(DataObject $item): static
{
$itemId = $this->_getItemId($item);
if ($itemId !== null) {
$this->_items[$itemId] = $item;
} else {
$this->_items[] = $item;
}
return $this;
}
/**
* Load collection — base implementation is a no-op
* Overridden in ResourceModel\Collection to run SQL
*/
public function load(bool $printQuery = false, bool $logQuery = false): static
{
$this->_isCollectionLoaded = true;
return $this;
}
/**
* Countable interface
*/
public function count(): int
{
$this->load();
return count($this->_items);
}
/**
* Get all items as array
*/
public function getItems(): array
{
$this->load();
return $this->_items;
}
}
4. Magento AbstractCollection und Lazy Loading
Die DB-gebundene Collection erbt von Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection und überschreibt load(), um eine SQL-Query auszuführen — aber erst beim ersten Zugriff:
<?php
// vendor/magento/framework/Model/ResourceModel/Db/Collection/AbstractCollection.php
namespace Magento\Framework\Model\ResourceModel\Db\Collection;
use Magento\Framework\Data\Collection\AbstractDb;
use Magento\Framework\DB\Select;
abstract class AbstractCollection extends AbstractDb
{
protected bool $_isCollectionLoaded = false;
/**
* LAZY LOADING: SQL wird erst hier ausgeführt
* Aufgerufen bei: foreach, count(), getItems(), getFirstItem()
*/
public function load(bool $printQuery = false, bool $logQuery = false): static
{
if ($this->isLoaded()) {
return $this; // Already loaded — return immediately
}
$this->_beforeLoad();
$this->_renderFilters();
$this->_renderOrders();
$this->_renderLimit();
// THIS is where the SQL query executes
$this->printLogQuery($printQuery, $logQuery);
$data = $this->getData(); // ← SELECT * FROM ... WHERE ... LIMIT ...
$this->resetData();
if (is_array($data)) {
foreach ($data as $row) {
$item = $this->getNewEmptyItem();
$item->setData($row); // Hydrate model with row data
$this->addItem($item);
// Dispatch event for each item
}
}
$this->_setIsLoaded();
$this->_afterLoad();
return $this;
}
}
Das kritische Verständnis: Die Collection ist ein Query-Builder, kein Array. Erst beim ersten foreach, count() oder getItems() trifft die SQL-Query die Datenbank.
<?php
declare(strict_types=1);
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
// Diese Zeile führt KEINE DB-Query aus!
$collection = $collectionFactory->create();
// Diese Zeilen konfigurieren nur den Query-Builder:
$collection->addAttributeToSelect(['name', 'price', 'sku'])
->addAttributeToFilter('status', 1)
->addAttributeToFilter('visibility', ['in' => [3, 4]])
->setOrder('created_at', 'DESC')
->setPageSize(20)
->setCurPage(1);
// ERST HIER wird die SQL-Query ausgeführt:
foreach ($collection as $product) { // ← load() triggered here
echo $product->getName();
}
// Zweites foreach: kein zweites DB-Query, Daten sind gecacht
foreach ($collection as $product) { // ← uses cached $_items
echo $product->getSku();
}
5. Collection-Filter, Joins und Paginierung
Die AbstractCollection ist ein mächtiger Query-Builder. Die wichtigsten Methoden:
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Model;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
/**
* Demonstrates collection filtering, joining, and pagination.
*/
final class ProductCollectionBuilder
{
public function __construct(
private readonly CollectionFactory $collectionFactory,
) {}
/**
* Builds a filtered, joined, paginated product collection.
*/
public function getFilteredProducts(int $categoryId, int $page = 1): ProductCollection
{
$collection = $this->collectionFactory->create();
// Select only needed attributes (avoids loading all EAV attributes)
$collection->addAttributeToSelect(['name', 'price', 'sku', 'thumbnail']);
// EAV attribute filter
$collection->addAttributeToFilter('status', ['eq' => 1]);
$collection->addAttributeToFilter('visibility', ['in' => [3, 4]]);
// Price range filter
$collection->addAttributeToFilter('price', ['gteq' => 10.00, 'lteq' => 500.00]);
// Category filter via join
$collection->addCategoriesFilter(['in' => [$categoryId]]);
// Stock filter
$collection->joinField(
'is_in_stock',
'cataloginventory_stock_item',
'is_in_stock',
'product_id=entity_id',
'{{table}}.stock_id=1',
'left'
);
$collection->addFieldToFilter('is_in_stock', 1);
// Sorting
$collection->addAttributeToSort('position', 'ASC');
$collection->addAttributeToSort('created_at', 'DESC');
// Pagination — crucial for large catalogs
$collection->setPageSize(24);
$collection->setCurPage($page);
return $collection; // Still not loaded — lazy!
}
}
6. Chunked Iterator: Große Kataloge ohne Memory-Overflow
Das häufigste Performance-Problem: 100.000 Produkte auf einmal laden. Die Lösung ist ein Chunked Iterator, der die Collection in Pages aufteilt:
<?php
declare(strict_types=1);
namespace Mironsoft\Import\Iterator;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
/**
* Memory-efficient chunked iterator for large product collections.
* Loads only one page at a time — prevents PHP OOM errors.
*/
final class ChunkedProductIterator implements \IteratorAggregate
{
private const CHUNK_SIZE = 1000;
public function __construct(
private readonly CollectionFactory $collectionFactory,
) {}
/**
* Yields products in chunks — never loads all products at once.
*
* @return \Generator<int, \Magento\Catalog\Model\Product>
*/
public function getIterator(): \Generator
{
$page = 1;
$totalYielded = 0;
do {
$collection = $this->createPage($page);
$count = $collection->count();
if ($count === 0) {
break;
}
foreach ($collection as $product) {
yield $totalYielded => $product;
$totalYielded++;
}
// Critical: free memory after each chunk
$collection->clear();
unset($collection);
$page++;
} while ($count === self::CHUNK_SIZE);
}
private function createPage(int $page): Collection
{
$collection = $this->collectionFactory->create();
$collection->addAttributeToSelect(['name', 'sku', 'price']);
$collection->setPageSize(self::CHUNK_SIZE);
$collection->setCurPage($page);
return $collection;
}
}
Verwendung im Import-Service:
<?php
declare(strict_types=1);
namespace Mironsoft\Import\Service;
use Mironsoft\Import\Iterator\ChunkedProductIterator;
/**
* Processes all products in chunks — memory-safe.
*/
final class ProductExportService
{
public function __construct(
private readonly ChunkedProductIterator $iterator,
) {}
/**
* Exports all products to CSV without loading all at once.
*/
public function exportToCsv(string $outputPath): void
{
$file = fopen($outputPath, 'w');
fputcsv($file, ['SKU', 'Name', 'Price']);
$count = 0;
foreach ($this->iterator as $position => $product) {
fputcsv($file, [
$product->getSku(),
$product->getName(),
$product->getPrice(),
]);
$count++;
// Memory check every 10,000 products
if ($count % 10000 === 0) {
echo sprintf(
"Processed %d products, memory: %s MB\n",
$count,
round(memory_get_usage(true) / 1024 / 1024, 2)
);
}
}
fclose($file);
echo "Exported {$count} products total.\n";
}
}
7. PHP Generators als lazy Iteratoren
PHP Generators (seit 5.5) sind der eleganteste Weg, lazy Iteratoren zu implementieren:
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Iterator;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SortOrderBuilder;
/**
* Generator-based lazy product iterator using Service Contracts.
* Preferred over direct Collection access for API-layer code.
*/
final class ProductIteratorGenerator
{
private const PAGE_SIZE = 500;
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
private readonly SortOrderBuilder $sortOrderBuilder,
) {}
/**
* Lazily yields all active products using SearchCriteria pagination.
*
* @return \Generator<string, \Magento\Catalog\Api\Data\ProductInterface>
*/
public function iterateActive(): \Generator
{
$sortOrder = $this->sortOrderBuilder
->setField('entity_id')
->setAscendingDirection()
->create();
$page = 1;
do {
$criteria = $this->searchCriteriaBuilder
->addFilter('status', 1)
->addSortOrder($sortOrder)
->setPageSize(self::PAGE_SIZE)
->setCurrentPage($page)
->create();
$result = $this->productRepository->getList($criteria);
$items = $result->getItems();
foreach ($items as $product) {
yield $product->getSku() => $product;
}
$page++;
} while (count($items) === self::PAGE_SIZE);
}
}
Was Generators besonders mächtig macht — sie können bidirektional kommunizieren:
<?php
// Generator mit send() — ermöglicht Feedback vom Consumer
function* batchProcessor(): \Generator
{
$processed = 0;
while (true) {
$item = yield $processed; // Yield current count, receive next item
if ($item === null) break;
process($item);
$processed++;
}
}
$gen = batchProcessor();
$gen->current(); // Start generator
$gen->send($product1); // Send item, generator processes it
$gen->send($product2);
$gen->send(null); // Signal done
8. Eigener Custom-Iterator für Magento
Manchmal brauchst du einen Iterator, der nicht an einer DB-Collection hängt — z.B. für CSV-Dateien oder externe APIs:
<?php
declare(strict_types=1);
namespace Mironsoft\Import\Iterator;
use Magento\Framework\DataObject;
/**
* Lazy CSV iterator — reads one line at a time without loading entire file.
* Memory usage: O(1) regardless of file size.
*/
final class CsvProductIterator implements \Iterator
{
private mixed $fileHandle = null;
private array $currentRow = [];
private int $position = 0;
private array $headers = [];
public function __construct(
private readonly string $filePath,
) {}
public function rewind(): void
{
if ($this->fileHandle !== null) {
fclose($this->fileHandle);
}
$this->fileHandle = fopen($this->filePath, 'r');
$this->position = 0;
// Read header row
$this->headers = fgetcsv($this->fileHandle) ?: [];
// Read first data row
$this->next();
}
public function current(): DataObject
{
return new DataObject(array_combine($this->headers, $this->currentRow));
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$row = fgetcsv($this->fileHandle);
$this->currentRow = $row !== false ? $row : [];
$this->position++;
}
public function valid(): bool
{
return !empty($this->currentRow)
&& count($this->currentRow) === count($this->headers);
}
public function __destruct()
{
if ($this->fileHandle !== null) {
fclose($this->fileHandle);
}
}
}
Verwendung:
<?php
$iterator = new CsvProductIterator('/var/import/products.csv');
foreach ($iterator as $lineNumber => $row) {
echo $row->getData('sku') . ': ' . $row->getData('name') . PHP_EOL;
// Memory: ~4KB regardless of whether file has 100 or 1,000,000 rows
}
9. Iterator vs. getItems(): Performance-Vergleich
| Ansatz | Memory | Queries | Best for |
|---|---|---|---|
foreach ($collection as $item) |
Alle Items in RAM | 1 SQL | <10.000 Items, normale Pages |
getItems() |
Alle Items in RAM | 1 SQL | Wenn Array-Zugriff nötig |
| Chunked Iterator (eigener) | Nur 1 Chunk in RAM | N SQL (N = total/chunk) | Batch-Jobs, Imports/Exports |
| Generator + SearchCriteria | Nur 1 Page in RAM | N SQL | Service-Layer-Code |
| CSV-Iterator | O(1) — 1 Zeile | 0 SQL | File-basierter Import |
Typischer Fehler beim Iterieren großer Collections:
<?php
// FALSCH: Lädt ALLE 500.000 Produkte auf einmal in den RAM
$collection = $collectionFactory->create();
// Kein setPageSize() → PHP memory_limit überschritten
foreach ($collection as $product) { // Fatal: Allowed memory exhausted
processProduct($product);
}
// RICHTIG: Chunk-basiert mit setPageSize()
$collection = $collectionFactory->create();
$collection->setPageSize(1000);
// Oder noch besser: ChunkedProductIterator (siehe oben)
foreach ($chunkedIterator as $product) {
processProduct($product);
}
10. Fazit: Collections richtig nutzen
Das Iterator Pattern ist in Magento omnipräsent — jede Collection ist ein Iterator. Die wichtigsten Regeln:
✓ Best Practices
- Immer
setPageSize()setzen - Nur benötigte Attribute selektieren (
addAttributeToSelect) - Für Batch-Jobs: Chunked Iterator verwenden
- Collection vor forEach-Konfiguration vollständig aufbauen
- Generators für lazy Pipelines bevorzugen
✗ Anti-Patterns
- Kein
setPageSize()bei großen Tabellen addAttributeToSelect('*')in Loops- Collection im Constructor laden (zu früh)
- Nach
load()noch Filter hinzufügen - Count-Query vor und nach Filter-Änderung
Zusammenfassung
Collection-Performance optimieren
Collections analysieren, Memory-Probleme lösen oder Batch-Importe optimieren.