Magento 2.1 – Fixing ‘Load Collection in foreach()’ Issue

collection;magento-2.1

In a custom module UpgradeSchema, I've this constructor :

function __construct(
    AttributeManagementInterfaceFactory $attributeManagementFactory,
) {
    $this->attributeManagementFactory = $attributeManagementFactory;
}

and this upgrade method :

public function upgrade( ModuleDataSetupInterface $setup, ModuleContextInterface $context ) {
    $attribute_id = 135;
    foreach([4, 9, 10] as $set_id) {
        // Get the attributes list for current attribute set
        $attributeManagement = $this->attributeManagementFactory->create();
        $attributes = $attributeManagement->getAttributes(\Magento\Catalog\Model\Product::ENTITY, $set_id);

        if($attributes[$attribute_id]) {
            var_dump($attributes[$attribute_id]->getAttributeSetId());
        }
    }
}

The product attribute with the id 135 is assign in three Attribute Sets (ids : 4, 9 and 10 in the foreach).

When I run the setup:upgrade, the var_dump outputs :

string(1) "4"
string(1) "4"
string(1) "4"

Three times the first Attribute Set ids, instead of 4, 9 and 10.

When I look into the AttributeManagement::getAttributes(), the load is called three times here :

$attributeCollection = $this->attributeCollection
    ->setAttributeSetFilter($attributeSet->getAttributeSetId())
    ->load();

return $attributeCollection->getItems();

But into the load method called (Magento\Framework\Data\Collection\AbstractDb::load()), Magento check if the collection is loaded (and returns the collection) or not (and so goes on) :

public function load($printQuery = false, $logQuery = false)
{
    if ($this->isLoaded()) {
        return $this;
    }

    return $this->loadWithFilter($printQuery, $logQuery);
}

I can't understand why, but it's a fact that the collection is not flagged as loaded the first time (hopefully) and flagged to true the two others times Magento check. And that despite I create new object in the foreach of my Upgrade script :

$attributeManagement = $this->attributeManagementFactory->create();

Someone to explains this phenomenon ? Or/And to explains how to fix it ?

Best Answer

In the constructor of the class AttributeManagement, the collection is injected like that :

public function __construct(
    ...
    \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection $attributeCollection,
    ...
) {
    ...
    $this->attributeCollection = $attributeCollection;
    ...
}

In other words, we inject a \Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection singleton. And when a singleton is injected, it will be this same object (not a new one) wherever you will inject it again : in the same class, in an other class, etc... And I had a lack of knowledge about that.

So when, in my example, I recreate a new object AttributeManagement which inject Collection singleton in its own constructor, in fact it get the already created Collection singleton, with already items loaded, already filters added and everything...

Conclusion : Collection should be injected thanks to the Factory pattern :

public function __construct(
    ...
    \Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory $attributeCollectionFactory,
    ...
) {
    ...
    $this->attributeCollectionFactory = $attributeCollectionFactory;
    ...
}

And that change just a little where it's called

public function getAttributes($entityType, $attributeSetId)
{
    ...
    $attributeCollection = $this->attributeCollectionFactory->create();
    return $attributeCollection
        ->setAttributeSetFilter($attributeSet->getAttributeSetId())
        ->load()
        ->getItems();
}

And that works. But, that would be rewrite the core and it's forbidden. So I will not use the current getAttributes() method but I will copy and paste my solution directly in my upgrade method.