This article is part of a series on design patterns for framework independent code, especially to write extensions for Magento 1 and Magento 2 alike.
All Parts:
- Introduction: Write Extensions For Magento 1 And Magento 2
- Part 1: Accessing Configuration Data
- Part 2: Using Dependency Injection
- Part 3: Building Bridges
- Part 4: Preparing Data For Output
- Part 5: Using Advanced Autoloading
- Part 6: Leveraging PSR Interfaces
- Part 7: Iterating Iterators
In Part 3 (Building Bridges) I explained how we can access Magento models in our framework independent library and also gave the example of a repository interface to access collections. But in some cases you don’t want to load the collection immediately or not even load all items at once. In Magento, if you don’t call load()
on a collection explicitly, it gets triggered as soon as you start iterating over the it with foreach
. And for the other case there’s the iterator resource model to load the result of a query row by row, which can be useful for processing a huge amount of data.
In Magento 2, the Repository APIs (like Magento\Catalog\Api\ProductRepositoryInterface) return result objects (Magento\Catalog\Api\Data\ProductSearchResultsInterface) which contain an array of all items. The collection classes are only used internally by the repositories and there is no lazy loading anymore if you use this API.
Now what we need is a way to make use of these optimizing features transparently, if possible, but it should be an implementation detail that we leave upon the concrete framework dependent implementation. This requires us to change repository interfaces from this:
1 2 3 4 5 6 7 |
interface ProductRepository { /** * @return Product[] */ public function findBySkus(array $skus); } |
to this:
1 2 3 4 5 6 7 |
interface ProductRepository { /** * @return ProductIterator */ public function findBySkus(array $skus); } |
where ProductIterator is a new interface:
1 2 3 4 5 6 7 |
interface ProductIterator extends \Iterator { /** * @return Product */ public function current(); } |
Note that this minimal interface just tells us that it’s an iterator where each element is an instance of Product
. The interface extends the internal Iterator interface. current()
is already part of this interface, the only addition here is the phpDoc return type hint.
You could add more methods if you need them, for example count()
from the Countable interface or accessors like getFirstItem()
:
1 2 3 4 5 6 7 8 9 10 11 12 |
interface ProductIterator extends \Iterator, \Countable { /** * @return Product */ public function current(); /** * @return Product */ public function getFirstItem(); } |
Implementation: Magento 1
Now a Magento 1 implementation that makes use of deferred loading is possible. We can use the IteratorIterator class which is basically a wrapper for other iterators. It implements Iterator, not IteratorAggregate, so that it fulfils our ProductIterator interface, but it can also wrap IteratorAggregate implementations.
This iterator will take the Magento collection as constructor argument and we can access the internal iterator of the collection with getInnerIterator()
. Not the collection itself, which implements IteratorAggregate, this is important to get access to the iterator methods like current()
.
1 2 3 4 5 6 7 8 9 10 11 12 |
class IntegerNet_Example_Model_Bridge_ProductIterator extends IteratorIterator implements ProductIterator { /** * @return Product */ public function current() { $magentoProduct = $this->getInnerIterator()->current(); return new IntegerNet_Example_Model_Bridge_Product($magentoProduct); } } |
Here we override current()
to convert each product to an instance of the product bridge class, everything else is handled by the IteratorIterator
base class. current()
is never implicitly called when there is no current element, so we can assume that $magentoProduct
is a product model instance. The corresponding ProductRepository
implementation can look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class IntegerNet_Example_Model_Bridge_ProductRepository implements ProductRepository { /** * @return ProductIterator */ public function findBySkus(array $skus) { $collection = Mage::getResourceModel('catalog/product_collection') ->addAttributeToSelect('name') ->addUrlRewrite() ->addAttributeToFilter('sku', array('in' => $skus)); return new IntegerNet_Example_Model_Bridge_ProductIterator($collection); } } |
Implementation: Magento 2
Or for Magento 2, without deferred loading:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
namespace IntegerNet\Example\Model\Bridge; use IntegerNet\Example\Implementor\ProductIterator as ProductIteratorInterface; use IntegerNet\Example\Implementor\Product as ProductInterface; use Magento\Catalog\Api\Data\ProductSearchResultsInterface; class ProductIterator extends \ArrayIterator implements ProductIteratorInterface { public function __construct(ProductSearchResultsInterface $result) { parent::__construct($result->getItems()); } /** * @return ProductInterface */ public function current() { $magentoProduct = parent::current(); return new Product($magentoProduct); } } |
The iterator extends ArrayIterator which is the simplest implementation if you operate on a complete array. We just convert the Magento product model to our bridge in the current()
method.
And the corresponding repository implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
namespace IntegerNet\Example\Model\Bridge; use IntegerNet\Example\Implementor\ProductRepository as ProductRepositoryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Api\Filter; use Magento\Framework\Api\Search\SearchCriteriaBuilder; class ProductRepository implements ProductRepositoryInterface { /** * @var ProductRepositoryInterface */ protected $productRepository; /** * @var SearchCriteriaBuilder */ protected $searchCriteriaBuilder; public function __construct(ProductRepositoryInterface $productRepository, SearchCriteriaBuilder $searchCriteriaBuilder) { $this->productRepository = $productRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; } /** * @return ProductIterator */ public function findBySkus(array $skus) { $this->searchCriteriaBuilder->addFilter(new Filter([ Filter::KEY_FIELD => ProductInterface::SKU, Filter::KEY_CONDITION_TYPE => 'in', Filter::KEY_VALUE => $skus ])); $result = $this->productRepository->getList($this->searchCriteriaBuilder->create()); return new ProductIterator($result); } } |
Note that I omitted the “use” statements in all Magento 1 examples, but for Magento 2 they are important to distinguish core classes from our library classes. Also to not overcomplicate the example, I instantiate the bridge classes Product
and ProductIterator
directly instead of injecting the generated factories that Magento 2 provides. To follow best practices and allow extension via plugins, you should change that.
Lazy Loading Product Iterator
To demonstrate how powerful this abstraction is, I’ll show you parts of our product iterator implementation that we use for the indexer in IntegerNet_Solr (Magento 1). It loads the product collection in chunks (default chunk size: 1000) to reduce the memory footprint, but it’s totally transparent so that the library that uses it can use a single foreach to iterate over all products.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
use IntegerNet\Solr\Implementor\ProductIterator; use IntegerNet\Solr\Implementor\Product; /** * Product iterator implementation with lazy loading of multiple collections (chunking). * Collections are prepared to be used by the indexer. */ class IntegerNet_Solr_Model_Bridge_LazyProductIterator implements ProductIterator, OuterIterator { /** * @var int */ protected $_storeId; /** * @var null|int[] */ protected $_productIdFilter; /** * @var int */ protected $_pageSize; /** * @var int */ protected $_currentPage; /** * @var Mage_Catalog_Model_Resource_Product_Collection */ protected $_collection; /** * @var ArrayIterator */ protected $_collectionIterator; /** * @link http://php.net/manual/en/outeriterator.getinneriterator.php * @return Iterator The inner iterator for the current entry. */ public function getInnerIterator() { return $this->_collectionIterator; } /** * @param int $_storeId store id for the collections * @param int[]|null $_productIdFilter array of product ids to be loaded, or null for all product ids * @param int $_pageSize Number of products per loaded collection (chunk) */ public function __construct($_storeId, $_productIdFilter, $_pageSize) { $this->_storeId = $_storeId; $this->_productIdFilter = $_productIdFilter; $this->_pageSize = $_pageSize; } /** * @return void Any returned value is ignored. */ public function next() { $this->getInnerIterator()->next(); } /** * @return mixed scalar on success, or null on failure. */ public function key() { return $this->getInnerIterator()->key(); } /** * @return boolean The return value will be casted to boolean and then evaluated. * Returns true on success or false on failure. */ public function valid() { if ($this->getInnerIterator()->valid()) { return true; } elseif ($this->_currentPage _collection->getLastPageNumber()) { $this->_currentPage++; $this->_collection = self::getProductCollection($this->_storeId, $this->_productIdFilter, $this->_pageSize, $this->_currentPage); $this->_collectionIterator = $this->_collection->getIterator(); $this->getInnerIterator()->rewind(); return $this->getInnerIterator()->valid(); } return false; } /** * @return void Any returned value is ignored. */ public function rewind() { $this->_currentPage = 1; $this->_collection = self::getProductCollection($this->_storeId, $this->_productIdFilter, $this->_pageSize, $this->_currentPage); $this->_collectionIterator = $this->_collection->getIterator(); $this->_collectionIterator->rewind(); } /** * @return Product */ public function current() { $product = $this->getInnerIterator()->current(); $product->setStoreId($this->_storeId); return new IntegerNet_Solr_Model_Bridge_Product($product); } /** * @param int $storeId * @param int[]|null $productIds * @param int $pageSize * @param int $pageNumber * @return Mage_Catalog_Model_Resource_Product_Collection */ private static function getProductCollection($storeId, $productIds = null, $pageSize = null, $pageNumber = 0) { // ... // here we create the product collection // ... return $productCollection; } } |
What happens here? First, the OuterIterator interface is an interface that you can use to implement your own variations of IteratorIterator
as described above. There is nothing special about it, you still have to implement the methods from Iterator
, but it adds a getInnerIterator()
method for access to the actual iterator. The code would work as well if we just implemented Iterator
and made getInnerIterator()
private.
Internally, rewind()
is called when you start iterating, and valid()
to check if there is another element.
- rewind() creates the first collection and sets the inner iterator to the collection iterator
- valid() creates a new collection with increased page number if the current collection doesn’t have any more elements, replaces the current collection iterator and checks if this one has any more elements. Only if this result is empty, the iterator stops.
Conclusion
When dealing with collections of objects, use a collection interface, which should extend PHP’s Iterator interface, instead of plain old arrays. Understand and use the power of PHP SPL iterators. If this is new territory for you, have a look at the Iterating PHP Iterators book by Cal Evans, since the online manual is quite sparse on the various iterators. I have to admit, I did not read it, but I trust Cal enough to recommend it blindly.
The End?
This is the last part of the series – at least for now. If you followed me from the beginning, thank you for your patience! I hope I was able to shed some light on the topic. Please leave a comment if you have any critique or additions. Your feedback is always welcome, I’m open to discuss any points made here in these blog posts and I’m happy about any opportunity to learn from other people’s experiences.

Author: Fabian Schmengler
Fabian Schmengler is Magento developer and trainer at integer_net. His focus lies in backend development, conceptual design and test automation.
Fabian was repeatedly selected as a Magento Master in 2017 and 2018 based on his engagements, active participation on StackExchange and contributions to the Magento 2 core.
Trackbacks/Pingbacks