4

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.

3

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.

  • using entityManager is it possible to retrieve also information about product (not only saving)? – Bartosz Kubicki Dec 30 '16 at 8:37
  • I just did a quick test and it is possible. To load a product you could use this code: $product = $this->_productFactory->create(); $this->_entityManager->load($product,1); or to load the values for another store: $product = $this->_productFactory->create(); $product->setStoreId(2); $this->_entityManager->load($product,1); – ochnygosch Dec 30 '16 at 8:46
  • For loading whole product I think it's better to use productRepository. I meant retrieving only value of one or more attributes of product, but without loading whole instance. – Bartosz Kubicki Dec 30 '16 at 8:58
  • 1
    I don't think that is possible with the entity manager. At least I couldn't find a method. – ochnygosch Dec 30 '16 at 9:12
1

I had similar problem in frontend -try not to load whole product model ans save it.but instead look for method updateAttributes() in product's resource model - it can save only attributes you need for only one store

  • Doesn't that defeat the purpose of the abstraction of the data api? – ochnygosch Dec 29 '16 at 8:08
  • I think it is no ideal, but it works for saving only particular attributes - I don't know if there is an interface for this (I think there is no) – Bartosz Kubicki Dec 29 '16 at 9:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.