Inhaltsverzeichnis
- Das PHP Iterator Interface — Grundlagen
- AbstractCollection in Magento 2: Lazy Loading
- EAV vs. Flat: addAttributeToFilter vs. addFieldToFilter
- Collections filtern, sortieren und paginieren
- Eigene Collection implementieren
- SearchCriteria und SearchResults als moderne Alternative
- Collection-Queries debuggen
- Performance-Tipps für Collections
- Zusammenfassung
- FAQ
Das PHP Iterator Interface — Grundlagen
Das Iterator Pattern ist eines der klassischen Design Patterns aus dem Gang-of-Four-Buch und beschreibt eine standardisierte Schnittstelle, um über Objekte in einer Sammlung zu iterieren — unabhängig davon, wie diese Sammlung intern organisiert ist. Der Aufrufer muss nicht wissen, ob die Daten in einem Array, einer verketteten Liste, einem Datenbankresultat oder einer anderen Datenstruktur gespeichert sind. Er ruft einfach next(), current() und valid() auf und erhält die Elemente nacheinander.
PHP implementiert das Iterator Pattern über zwei Interfaces in der Standard Library: Iterator und IteratorAggregate. Das Iterator-Interface definiert fünf Methoden: current(), key(), next(), rewind() und valid(). Das IteratorAggregate-Interface ist einfacher und definiert nur getIterator(), das ein Traversable-Objekt zurückgibt. Beide Interfaces ermöglichen die Nutzung der foreach-Sprachkonstruktion.
In PHP 8.4 gibt es wichtige Ergänzungen rund um die Typsicherheit von Iteratoren. Generic-artige Annotationen über PHPDoc (@extends \IteratorAggregate<int, PostInterface>) werden von PHPStan und Psalm verstanden und ermöglichen typsichere foreach-Schleifen. Wenn eine Collection über PostInterface-Objekte iteriert, weiß PHPStan, dass $post in der foreach-Schleife vom Typ PostInterface ist — und kann entsprechende Typchecks durchführen. Das ist ein erheblicher Fortschritt gegenüber dem ungetypten mixed der älteren Magento-Architektur.
Das Traversable-Interface ist das übergeordnete Interface in PHP, das sowohl Iterator als auch IteratorAggregate vereint. Alle nativen PHP-Funktionen, die mit Iterables arbeiten — wie iterator_to_array(), iterator_count() und viele SPL-Funktionen — akzeptieren Traversable-Objekte. Magento 2 Collections sind Traversable und können mit all diesen Funktionen genutzt werden.
AbstractCollection in Magento 2: Lazy Loading
Magento 2 Collections erweitern Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection. Diese Klasse ist das Herzstück des Collection-Systems und implementiert IteratorAggregate. Das bedeutet: Jede Magento 2 Collection ist ein Traversable-Objekt und kann mit foreach durchlaufen werden. Die getIterator()-Methode löst dabei das Laden der Daten aus — das ist Lazy Loading in seiner reinsten Form.
Lazy Loading bedeutet: Die Collection konfiguriert zunächst nur den SQL-Query (Filter, Sortierung, Pagination). Die eigentliche Datenbankabfrage wird erst ausgeführt, wenn tatsächlich auf die Daten zugegriffen wird. Solange man nur Filter hinzufügt und nichts liest, wird kein SQL ausgeführt. Das ist effizient, weil überflüssige Datenbankabfragen vermieden werden. Der Zustand isLoaded wird intern auf false initialisiert und nach dem ersten Laden auf true gesetzt.
<?php
declare(strict_types=1);
namespace Mironsoft\Blog\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Mironsoft\Blog\Model\ResourceModel\Post\CollectionFactory;
/**
* Blog post list ViewModel — uses Collection with Lazy Loading.
*/
class PostList implements ArgumentInterface
{
public function __construct(
private readonly CollectionFactory $collectionFactory
) {}
/**
* Get active blog posts for the current page.
*
* @return \Mironsoft\Blog\Model\Post[]
*/
public function getActivePosts(int $pageSize = 10, int $page = 1): array
{
// Collection is created — NO SQL yet
$collection = $this->collectionFactory->create();
// Filters and pagination are configured — still NO SQL
$collection
->addFieldToFilter('is_active', ['eq' => 1])
->addFieldToFilter('publish_date', ['lteq' => date('Y-m-d H:i:s')])
->setOrder('publish_date', 'DESC')
->setPageSize($pageSize)
->setCurPage($page);
// getItems() triggers SQL execution — Lazy Loading kicks in here
return $collection->getItems();
}
/**
* Get total count of active posts — SELECT COUNT(*) only.
*/
public function getTotalPostCount(): int
{
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('is_active', ['eq' => 1]);
// getSize() executes SELECT COUNT(*) — does NOT load all data
return $collection->getSize();
}
}
Ein wichtiges Detail: Wenn man eine Collection mit foreach durchläuft und danach noch einmal getItems() aufruft, wird der SQL-Query nicht erneut ausgeführt — die Daten sind bereits im Speicher. Das ist effizient, kann aber zu Verwirrung führen wenn man nach dem ersten Laden weitere Filter hinzufügt. In diesem Fall würden die neuen Filter die bereits geladenen Daten nicht beeinflussen. Man muss dann clear() aufrufen, um den geladenen Zustand zurückzusetzen.
EAV vs. Flat: addAttributeToFilter vs. addFieldToFilter
Eine der häufigsten Fehlerquellen bei Magento 2 Collections ist die Verwechslung der beiden Filtermethoden addAttributeToFilter() und addFieldToFilter(). Diese Methoden sehen ähnlich aus, funktionieren aber fundamental unterschiedlich und werden für verschiedene Tabellentypen verwendet.
addAttributeToFilter() ist für EAV-Collections (Entity-Attribute-Value). Im EAV-System werden Attribute nicht als direkte Spalten in der Haupttabelle gespeichert, sondern in separaten Value-Tabellen (eine pro Datentyp: catalog_product_entity_varchar, catalog_product_entity_decimal usw.). Ein Filter auf ein EAV-Attribut erfordert deshalb einen JOIN auf die entsprechende Value-Tabelle. addAttributeToFilter() übernimmt diesen JOIN automatisch und fügt das WHERE-Kriterium für den richtigen Attribut-Code hinzu. Beispiele: alle Catalog Collections (Products, Categories) und Customer Collections.
addFieldToFilter() ist für Flat-Collections — also Tabellen, bei denen jede Eigenschaft eine direkte Spalte in der Haupttabelle ist. Sales-Entities (Orders, Invoices, Shipments, Credit Memos), Quotes und alle eigenen Module mit normalen MySQL-Tabellen verwenden flat Collections. Ein Filter mit addFieldToFilter() fügt direkt ein WHERE-Kriterium zur Haupttabelle hinzu, ohne JOINs. Das macht flat Collections in der Regel schneller als EAV-Collections.
Was passiert, wenn man die falsche Methode verwendet? addFieldToFilter() auf einer EAV-Collection funktioniert nur wenn das Feld tatsächlich eine direkte Spalte der Haupttabelle ist (z.B. entity_id, sku, created_at). Für EAV-Attribute muss man addAttributeToFilter() verwenden, da es sonst zu SQL-Fehlern oder falschen Ergebnissen kommt. Eigene Collections (für eigene Module) verwenden fast immer addFieldToFilter(), weil eigene Tabellen in der Regel flat sind.
Collections filtern, sortieren und paginieren
Die Magento 2 Collection API bietet eine fluent Interface für alle gängigen Datenbankoperationen. Filter werden mit Condition-Arrays übergeben, die dem Zend_Db_Select-Format folgen. Die gängigsten Conditions sind: eq (equals), neq (not equals), like, nlike, in, nin (not in), is, notnull, null, gt (greater than), lt (less than), gteq (greater than or equal), lteq (less than or equal), from/to für Datumsbereiche.
<?php
declare(strict_types=1);
namespace Mironsoft\Blog\Model;
use Mironsoft\Blog\Model\ResourceModel\Post\CollectionFactory;
/**
* Blog post query service — demonstrates collection filtering patterns.
*/
class PostQueryService
{
public function __construct(
private readonly CollectionFactory $collectionFactory
) {}
/**
* Get posts filtered by multiple criteria.
*
* @return \Mironsoft\Blog\Model\Post[]
*/
public function getFilteredPosts(
array $categoryIds,
string $fromDate,
string $toDate,
int $page = 1,
int $pageSize = 20
): array {
$collection = $this->collectionFactory->create();
$collection
// Filter by multiple category IDs (IN condition)
->addFieldToFilter('category_id', ['in' => $categoryIds])
// Filter by date range
->addFieldToFilter('publish_date', ['from' => $fromDate, 'to' => $toDate])
// Only active posts
->addFieldToFilter('is_active', ['eq' => 1])
// Sort by newest first
->setOrder('publish_date', \Magento\Framework\Data\Collection::SORT_ORDER_DESC)
// Pagination
->setPageSize($pageSize)
->setCurPage($page);
return $collection->getItems();
}
/**
* Get pagination metadata — only COUNT query, no data loaded.
*/
public function getPaginationData(array $categoryIds): array
{
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('category_id', ['in' => $categoryIds]);
$collection->addFieldToFilter('is_active', ['eq' => 1]);
$totalCount = $collection->getSize(); // SELECT COUNT(*) only
return [
'total_count' => $totalCount,
'last_page' => $collection->getLastPageNumber(),
];
}
}
Die Methode getSize() ist besonders wichtig für performante Paginierung. Sie führt ein separates SELECT COUNT(*) aus, ohne die eigentlichen Daten zu laden. Das ermöglicht es, die Gesamtanzahl der Ergebnisse für eine Paginierungsanzeige zu ermitteln, ohne alle Datensätze in den Speicher zu laden. In Kombination mit setPageSize() und setCurPage() bildet getSize() die Grundlage für effiziente Paginierung in Magento 2.4.8.
Eigene Collection implementieren
Für ein eigenes Magento 2 Modul muss man eine eigene Collection-Klasse erstellen, die AbstractCollection erweitert. Diese Klasse konfiguriert die Verbindung zwischen Model und ResourceModel und kann eigene Filtermethoden als fluent API anbieten. Die Konvention für den Speicherort ist Model/ResourceModel/EntityName/Collection.php.
<?php
declare(strict_types=1);
namespace Mironsoft\Blog\Model\ResourceModel\Post;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use Mironsoft\Blog\Model\Post;
use Mironsoft\Blog\Model\ResourceModel\Post as PostResource;
/**
* Blog post collection — implements Iterator pattern via AbstractCollection.
*
* @extends AbstractCollection<Post>
*/
class Collection extends AbstractCollection
{
/** @var string Primary key field name */
protected $_idFieldName = 'post_id';
/** @var string Event prefix for collection events */
protected $_eventPrefix = 'mironsoft_blog_post_collection';
/** @var string Event object identifier */
protected $_eventObject = 'post_collection';
/**
* Initialize collection — link to Model and ResourceModel.
*/
protected function _construct(): void
{
$this->_init(Post::class, PostResource::class);
}
/**
* Add filter for active posts only.
*/
public function addActiveFilter(): static
{
$this->addFieldToFilter('is_active', ['eq' => 1]);
return $this;
}
/**
* Filter posts published before a given date.
*/
public function addPublishedFilter(?string $date = null): static
{
$date ??= date('Y-m-d H:i:s');
$this->addFieldToFilter('publish_date', ['lteq' => $date]);
return $this;
}
/**
* Add store filter for multi-store setups.
*/
public function addStoreFilter(int $storeId): static
{
$this->getSelect()->where(
'main_table.store_id IN (0, ?)',
$storeId
);
return $this;
}
/**
* Join author information from admin_user table.
*/
public function joinAuthorName(): static
{
$this->getSelect()->joinLeft(
['author' => $this->getTable('admin_user')],
'main_table.author_id = author.user_id',
['author_name' => new \Zend_Db_Expr("CONCAT(author.firstname, ' ', author.lastname)")]
);
return $this;
}
}
Die Collection kann dann sehr lesbar verwendet werden, weil die eigenen Methoden eine domänenspezifische Sprache bilden. Statt kryptischer Filter-Arrays schreibt man $collection->addActiveFilter()->addPublishedFilter()->addStoreFilter(1). Diese fluent API ist ein wichtiger Aspekt guter Magento 2 Architektur: Collection-Logik gehört in die Collection, nicht in den aufrufenden Code.
SearchCriteria und SearchResults als moderne Alternative
Während direkte Collections im internen Code ihre Berechtigung haben, setzt das Magento 2 Service-Contract-System auf SearchCriteria als abstrakte Abfragesprache. SearchCriteria ist ein Value Object, das Filter, Sortierungen und Pagination kapselt, ohne an eine konkrete Collection oder Datenbanktabelle gebunden zu sein. Es ist die Sprache, in der Repository-Clients mit Repositories kommunizieren.
Der Vorteil von SearchCriteria ist die vollständige Testbarkeit. Man kann ein SearchCriteria-Objekt erstellen, an ein gemocktes Repository übergeben, und das gemockte Repository gibt SearchResults zurück — ohne einen einzigen Datenbankzugriff. Im Gegensatz dazu erfordert das direkte Testen einer Collection einen Datenbankzugriff oder komplexes Mocking des ResourceModels.
SearchResults ist das entsprechende Ergebniscontainer-Interface: getItems() gibt die geladenen Entities zurück, getTotalCount() gibt die Gesamtanzahl zurück (für Pagination). SearchResults ist ein reines Value Object ohne SQL-Kenntnis. Das Repository ist dafür verantwortlich, SearchCriteria in Collection-Operationen zu übersetzen und die Ergebnisse in SearchResults zu verpacken.
Die Verwendung von SearchCriteria ist besonders für REST-API-Endpunkte in Magento 2 relevant: Wenn ein Repository Interface mit getList(SearchCriteriaInterface $criteria) über die REST-API aufgerufen wird, wandelt Magento die Query-Parameter der URL automatisch in ein SearchCriteria-Objekt um. Das bedeutet: Ein korrekt implementiertes Repository ist automatisch über die REST-API erreichbar, ohne zusätzlichen Code.
Collection-Queries debuggen
Das Debuggen von Collection-Queries ist ein unverzichtbares Handwerk für Magento 2 Entwickler. Der direkteste Weg ist (string) $collection->getSelect(), das den vollständigen SQL-Statement als String zurückgibt. Man kann diesen String dann in ein Datenbank-Tool wie MySQL Workbench oder TablePlus einfügen und analysieren. Das zeigt sowohl die WHERE-Klauseln als auch alle JOINs, ORDER BY und LIMIT-Anweisungen.
$collection->printLogQuery(true) gibt den Query direkt in die PHP-Ausgabe aus. Das ist nützlich bei der schnellen Entwicklung, sollte aber im Produktionscode nie vorhanden sein. Eine sauberere Alternative für Entwicklung ist das Schreiben des Query-Strings in das Magento-Log: $this->logger->debug('Collection SQL: ' . $collection->getSelect()).
In der Entwicklungsumgebung kann bin/magento dev:query-log:enable alle Datenbankabfragen in var/log/db.log protokollieren. Das gibt einen vollständigen Überblick über alle SQL-Queries einer Seite, inklusive deren Ausführungszeit. Das ist besonders hilfreich um N+1-Query-Probleme zu erkennen: Wenn man 50 ähnliche Queries sieht, hat man höchstwahrscheinlich eine Collection, die in einer Schleife einzelne load()-Aufrufe macht.
Performance-Tipps für Collections
Der wichtigste Performance-Tipp für Collections: Immer nur die benötigten Felder laden. Bei EAV-Collections mit addAttributeToSelect() und bei Flat-Collections mit einem expliziten getSelect()->columns([...]). Ein SELECT * bei einem Katalogprodukt in Magento 2 kann Dutzende von EAV-JOINs auslösen — das ist teuer. Wenn man nur den Titel und die URL eines Produkts braucht, sollte man auch nur diese beiden Attribute laden.
Für Paginierung gilt: Immer getSize() vor dem Laden der Daten aufrufen, wenn man die Gesamtanzahl benötigt. getSize() führt nur ein SELECT COUNT(*) aus, ohne die Entities zu laden. Wenn man getSize() nach getItems() oder foreach aufruft, sind die Daten bereits im Speicher — das COUNT wird dann aus den geladenen Daten berechnet, was korrekt ist, aber möglicherweise nicht das, was man will wenn man separate Paginierung und Datenladen optimieren möchte.
Collections, die für Exporte oder Batch-Prozesse über große Datenmengen iterieren, sollten nicht alle Daten auf einmal in den Speicher laden. Die empfohlene Strategie ist das seitenweise Laden: Man iteriert über alle Seiten, lädt jeweils nur eine Seite mit setPageSize() und setCurPage(), verarbeitet die Daten und gibt den Speicher frei. In Magento 2.4.8 gibt es dafür auch Walk-Methoden und Stream-basierte Ansätze über Custom ResourceModel-Methoden.
Datenbank-Indizes sind für Collection-Performance entscheidend. Wenn man häufig nach einem bestimmten Feld filtert (z.B. is_active, store_id, publish_date), sollte dieses Feld in der db_schema.xml mit einem INDEX versehen werden. Ohne Index führt MySQL einen Full Table Scan aus, der bei wachsenden Datensätzen exponentiell langsamer wird. Das deklarative Schema in Magento 2.4.8 macht es einfach, Indizes korrekt zu deklarieren und über Patches zu verwalten.
Zusammenfassung: Iterator & Collections in Magento 2
Das Iterator Pattern steckt tief in Magento 2 Collections. AbstractCollection implementiert IteratorAggregate und lädt Daten lazy. EAV-Collections nutzen addAttributeToFilter() mit automatischen JOINs, Flat-Collections addFieldToFilter() mit direktem WHERE. getSize() für COUNT ohne Datenladen. SearchCriteria für Service Contracts. Eigene Collections mit fluent API für domänenspezifische Filter.
Lazy Loading
SQL erst bei erstem Zugriff. Trigger: foreach, getItems(), getSize(), count(). Danach gecacht. clear() setzt den Zustand zurück.
EAV vs. Flat
addAttributeToFilter() für Catalog/Customer. addFieldToFilter() für Sales/eigene Module. Falsche Methode = SQL-Fehler oder falsche Ergebnisse.
SearchCriteria
Für Repository-Interfaces, REST-API, testbaren Code. SearchCriteriaBuilder + SortOrderBuilder. SearchResults mit getItems() und getTotalCount().
Pagination
setPageSize() + setCurPage(). getSize() für COUNT ohne Datenladen. getLastPageNumber() für Gesamtseitenzahl.
Mironsoft
Eigene Collections und Repositories entwickeln?
Wir entwickeln performante Magento 2 Module mit korrektem Collection-Design, SearchCriteria-Integration und vollständigem Repository Pattern. Sauber, testbar, upgrade-sicher.
Custom Collections
Eigene Collections mit fluent Filter-API und Store-Scope-Support
Repository Pattern
Service Contracts mit SearchCriteria, SearchResults und Interface-first
Performance
Query-Optimierung, Index-Analyse und Lazy-Loading-Strategien