Magento2 Product EAV – How to Save Store Specific Product Attributes Without Modifying Untouched Attributes

eavmagento2product

When I try to update a product in a controller in the backend, default values of products are overwritten.

For example given a product with id 1 having its name set in store 0 and storeview 1 using this default name. If you execute this code in a backend controller the name attribute in storeview 1 no longer uses the default value, but the value of the name attribute of store 0 is copied to storeview 1.

$product = $this->_productRepository->getById(1, true, 1);
$product->setDescription('Test');
$this->_productRepository->save($product);

The behavior in magento 1 was that only changed attributes are saved.

It gets even more weird if there are more than one storeview. Given a product and two storeviews with these values:

Store | Attribute | Value
0 | name | "Testproduct"
1 | name | NULL
2 | name | NULL

0 | description | "Testdescription"
1 | description | NULL
2 | description | "Store 2 description"

where a value of NULL means the store should use the default value
and you try to update the name of the product in store 2 with the following code

$product = $this->_productRepository->getById(1, true, 2);
$product->setName('Testproduct in store 2');
$this->_productRepository->save($product);

the resulting data is:

Store | Attribute | Value
0 | name | "Testproduct"
1 | name | "Testproduct in store 2"
2 | name | NULL

0 | description | "Testdescription"
1 | description | "Store 2 description"
2 | description | "Store 2 description"

Notice how the name is saved to the wrong store and the description of store 2 is copied to store 1.

So what is the correct way to programmatically modify product attributes?

I've already filed a bug report on this issue on github, but I can't believe I am the first to run into this issue.

Best Answer

Magento since version 2.1 uses an entity manager (Magento\Framework\EntityManager\EntityManager) to save entities to the database (see for example Magento\Catalog\Model\ResourceModel\Product::save()). And you can use this entity manager to modify a product without accessing the resource model and thus defeating the abstraction.

You can get the entity manager via constructor dependency injection and then use it like this to for example modify the name of product 1 in the store with id 2:

$product = $this->_productRepository->getById(1,true, 2);
$saveProduct = $this->_productFactory->create();
$saveProduct->setId($product->getId());
$saveProduct->setStoreId(2);
$saveProduct->setSku($product->getSku());
$saveProduct->setName("Testproduct name in store 2");
$this->_entityManager->save($saveProduct);

Please note that you have to set the product id, sku and the storeId in the product you intend to save. Notice that if you don't set the storeId the data won't be saved in the default store, but in the currently active store.

This almost works as expected, except attributes that define a default value (like status and options_container), have its default value saved in the store scope. I think this is another bug in magento and I could work around that by creating a plugin for the Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend class that defines the following function:

public function aroundBeforeSave(AbstractBackend $subject,
                                 callable $proceed,
                                 $object)
{
    if ($object->hasData('store_id') && $object->getData('store_id') != 0) {
        return $this;
    }
    return $proceed($object);
}

The default implementation of the beforeSave method tests if a value for the attribute is set and if it's not, it fetches the default value for this attribute and sets it in the entity. This plugin prevents this behavior if it is not the default store.

Related Topic