Configurable products associated products lost on save.
<?php
namespace Company\Test\Console\Command;
use Symfony\Component\Console\Command\Command;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
/**
* Save all products test command.
*/
class SaveProductsCommand extends Command
{
/**
* @var ProductRepositoryInterface
*/
private $productRepository;
/**
* @var CollectionFactory
*/
private $productCollectionFactory;
/**
* @param ProductRepositoryInterface $productRepository
* @param CollectionFactory $productCollectionFactory
*/
public function __construct(
ProductRepositoryInterface $productRepository,
CollectionFactory $productCollectionFactory
) {
$this->productRepository = $productRepository;
$this->productCollectionFactory = $productCollectionFactory;
parent::__construct();
}
/**
* {@inheritdoc}
*
* @see \Symfony\Component\Console\Command\Command::configure()
*/
protected function configure()
{
$this->setName('company:test:saveall');
$this->setDescription('Save all products.');
parent::configure();
}
/**
* {@inheritdoc}
*
* @see \Symfony\Component\Console\Command\Command::execute()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$productCollection = $this->productCollectionFactory->create();
foreach ($productCollection as $product) {
$output->writeln ('Updating product ' . $product->getSku());
$this->productRepository->save($product);
}
}
}
@petol880 If you want to change product attribute values you may have a look at this:
use Magento\Framework\App\State;
use \Magento\Framework\ObjectManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
class CollectionFixCommand extends Command
{
/**
* @var \Magento\Framework\ObjectManagerInterface
*/
protected $objectManager;
/**
* @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
*/
protected $collectionFactory;
public function __construct(
CollectionFactory $collectionFactory,
ObjectManagerInterface $manager,
State $state
)
{
$state->setAreaCode('adminhtml');
$this->collectionFactory = $collectionFactory;
$this->objectManager = $manager;
parent::__construct();
}
protected function configure()
{
$this->setName('collection:fix')->setDescription('Fixes product collection');
}
protected function execute(InputInterface $input, Outputinterface $output)
{
$collection = $this->collectionFactory->create();
$collection->addFieldToSelect('*');
foreach ($collection->getItems() as $product)
{
$description = $product->getData('description');
$search = "http://example.com/media";
if(strpos($description, $search) !== false){
$description = str_replace($search, '{{media url=', $description);
$description = str_replace('.png', '.png}}', $description);
$product->setData('description', $description);
try{
$product->getResource()->saveAttribute($product, 'description');
} catch(Exception $e){
$output->writeln($e->getMessage());
}
}
}
}
}
@developer-lindner Thanks for the tip. The code was just a simple example for reproducing the bug.
@petol880 no problem, you may have to add Magento\Framework\App\State. Have a look at ImagesResizeCommand.php for example to get a clue...
I'm facing what's seem to be the same issue. I'm trying to get products from one store (language), find the products translations in an external pim system, and save the transalated version to a different store.
In this process which is basically: get product collection, switch scope, add translated values to the product , save the product. All the configurable products lose their links to simple products (in all stores).
Now I can offcourse get those links first, save the product, and re-add the the links. But this seems like a weird work-a-round for something that seems trivial: read product -> save product that loses data at the moment. And is this the only data that get's lost, or are other parts lost as well?
As a work-a-round it's possible to load a new product object through the repository instead of saving the object that comes from the collection directly. That way you seem to get a complete product object that won't break on saving.
$productCollection = $this->productCollectionFactory->create();
foreach ($productCollection as $product) {
$output->writeln ('Updating product ' . $product->getSku());
// Reload the product through the repository
$product = $this->productRepository->getById($product->getId() );
$this->productRepository->save($product);
}
Just for info I think the bug is in the Magento\ConfigurableProductModel\ProductSaveHandler. I'm not sure how to solve it properly, but I just did an override to check if the extension attributes array is empty.
/**
* @param ProductInterface $entity
* @param array $arguments
* @return ProductInterface
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function execute($entity, $arguments = [])
{
if ($entity->getTypeId() !== Configurable::TYPE_CODE) {
return $entity;
}
$extensionAttributes = $entity->getExtensionAttributes();
if ($extensionAttributes === null) {
return $entity;
}
if ($extensionAttributes->getConfigurableProductOptions() !== null) {
$this->deleteConfigurableProductAttributes($entity);
}
$configurableOptions = (array) $extensionAttributes->getConfigurableProductOptions();
if (!empty($configurableOptions)) {
$this->saveConfigurableProductAttributes($entity, $configurableOptions);
}
$configurableLinks = (array) $extensionAttributes->getConfigurableProductLinks();
$this->resourceModel->saveProducts($entity, $configurableLinks);
return $entity;
}
It never checks if the extension attributes are loaded before saving them ($extensionAttributes is never null I think). So if extension attributes aren't loaded existing links will be removed.
I'm not sure about the "correct" solution, but I added this after the null check.:
$extensionAttributesArray = $extensionAttributes->__toArray();
if (empty($extensionAttributesArray)) {
return $entity;
}
Can anyone confirm progress on this issue, as I am experiencing the same.
I have found probably solution for this issue. This solution checks is configurable product links are null (the same like configurable products options checking), this prevents links from being removed when links are not set.
if ($extensionAttributes->getConfigurableProductLinks() !== null) {
$configurableLinks = (array) $extensionAttributes->getConfigurableProductLinks();
$this->resourceModel->saveProducts($entity, $configurableLinks);
}
This solution was tested in REST API product update method.
I am facing the same problem in magento 2.1.7.
Products are loaded via the collection factory:
$collection = $this->productCollectionFactory->create();
After that foreach on that collection:
$this->productRepository->save($product);
The result is that the configurable products are not connected to their children any more.
The records are lost in the 'catalog_product_relation' table.
The same issue in magento CE 2.1.5
Facing the same issue after save configurable products. Our setup consists of:
Magento 2.1.7 CE
PHP 7.0.19
chrome and firefox browser.
It seems to be fixed in 2.1.9!
@petol880, thank you for your report.
We were not able to reproduce this issue by following the steps you provided. If you'd like to update it, please reopen the issue.
We tested the issue on 2.3.0-dev, 2.2.0, 2.1.9
@roseofgold I do not concur. I still have this problem in 2.1.9 - update of configurable product via rest API causes all child products to be lost.
We're on Magento cloud 2.2.0 and I'm receiving the same issue. Try to update a configurable and the associated products are being lost.
Most helpful comment
As a work-a-round it's possible to load a new product object through the repository instead of saving the object that comes from the collection directly. That way you seem to get a complete product object that won't break on saving.