Skip to content

fix Product duplication of imported products "fails" due to url rewrites #32141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: 2.4-develop
Choose a base branch
from
45 changes: 28 additions & 17 deletions app/code/Magento/Catalog/Controller/Adminhtml/Product/Duplicate.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,68 @@
*/
namespace Magento\Catalog\Controller\Adminhtml\Product;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\View\Result\Redirect;
use Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Catalog\Model\Product\Copier;
use Magento\Catalog\Model\ProductFactory;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\ObjectManager;
use Psr\Log\LoggerInterface;

/**
* Class Duplicate
* Class Duplicate product
*/
class Duplicate extends \Magento\Catalog\Controller\Adminhtml\Product implements
\Magento\Framework\App\Action\HttpGetActionInterface
class Duplicate extends Product implements HttpGetActionInterface
{
/**
* @var \Magento\Catalog\Model\Product\Copier
* @var Copier
*/
protected $productCopier;

/**
* @var \Psr\Log\LoggerInterface
* @var LoggerInterface
*/
private $logger;

/**
* @param Action\Context $context
* @var ProductFactory
*/
private $productFactory;

/**
* @param Context $context
* @param Builder $productBuilder
* @param \Magento\Catalog\Model\Product\Copier $productCopier
* @param \Psr\Log\LoggerInterface $logger
* @param Copier $productCopier
* @param LoggerInterface|null $logger
* @param ProductFactory|null $productFactory
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
Context $context,
Product\Builder $productBuilder,
\Magento\Catalog\Model\Product\Copier $productCopier,
\Psr\Log\LoggerInterface $logger = null
Copier $productCopier,
LoggerInterface $logger = null,
?ProductFactory $productFactory = null
) {
$this->productCopier = $productCopier;
$this->logger = $logger ?: ObjectManager::getInstance()
->get(\Psr\Log\LoggerInterface::class);
$this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class);
$this->productFactory = $productFactory ?: ObjectManager::getInstance()->get(ProductFactory::class);
parent::__construct($context, $productBuilder);
}

/**
* Create product duplicate
*
* @return \Magento\Backend\Model\View\Result\Redirect
* @return Redirect
*/
public function execute()
{
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
/** @var Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();

$product = $this->productBuilder->build($this->getRequest());
try {
$newProduct = $this->productCopier->copy($product);
$newProduct = $this->productCopier->copy($product, $this->productFactory->create());
$this->messageManager->addSuccessMessage(__('You duplicated the product.'));
$resultRedirect->setPath('catalog/*/edit', ['_current' => true, 'id' => $newProduct->getId()]);
} catch (\Exception $e) {
Expand Down
83 changes: 48 additions & 35 deletions app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,51 @@

namespace Magento\Catalog\Controller\Adminhtml\Product;

use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\View\Result\Redirect;
use Magento\Catalog\Api\CategoryLinkManagementInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Controller\Adminhtml\Product;
use Magento\Catalog\Model\Product\Copier;
use Magento\Catalog\Model\Product\TypeTransitionManager;
use Magento\Catalog\Model\ProductFactory;
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\App\Request\DataPersistorInterface;
use Magento\Framework\Escaper;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

/**
* Product save controller
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface
class Save extends Product implements HttpPostActionInterface
{
/**
* @var Initialization\Helper
*/
protected $initializationHelper;

/**
* @var \Magento\Catalog\Model\Product\Copier
* @var Copier
*/
protected $productCopier;

/**
* @var \Magento\Catalog\Model\Product\TypeTransitionManager
* @var TypeTransitionManager
*/
protected $productTypeManager;

/**
* @var \Magento\Catalog\Api\CategoryLinkManagementInterface
* @var CategoryLinkManagementInterface
*/
protected $categoryLinkManagement;

/**
* @var \Magento\Catalog\Api\ProductRepositoryInterface
* @var ProductRepositoryInterface
*/
protected $productRepository;

Expand All @@ -57,61 +65,66 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements Http
private $storeManager;

/**
* @var \Magento\Framework\Escaper
* @var Escaper
*/
private $escaper;

/**
* @var \Psr\Log\LoggerInterface
* @var LoggerInterface
*/
private $logger;

/**
* @var ProductFactory
*/
private $productFactory;

/**
* Save constructor.
*
* @param Action\Context $context
* @param Context $context
* @param Builder $productBuilder
* @param Initialization\Helper $initializationHelper
* @param \Magento\Catalog\Model\Product\Copier $productCopier
* @param \Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager
* @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
* @param \Magento\Framework\Escaper $escaper
* @param \Psr\Log\LoggerInterface $logger
* @param \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement
* @param StoreManagerInterface $storeManager
* @param Copier $productCopier
* @param TypeTransitionManager $productTypeManager
* @param ProductRepositoryInterface $productRepository
* @param Escaper|null $escaper
* @param LoggerInterface|null $logger
* @param CategoryLinkManagementInterface|null $categoryLinkManagement
* @param StoreManagerInterface|null $storeManager
* @param ProductFactory|null $productFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
Context $context,
Product\Builder $productBuilder,
Initialization\Helper $initializationHelper,
\Magento\Catalog\Model\Product\Copier $productCopier,
\Magento\Catalog\Model\Product\TypeTransitionManager $productTypeManager,
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
\Magento\Framework\Escaper $escaper = null,
\Psr\Log\LoggerInterface $logger = null,
\Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement = null,
\Magento\Store\Model\StoreManagerInterface $storeManager = null
Copier $productCopier,
TypeTransitionManager $productTypeManager,
ProductRepositoryInterface $productRepository,
Escaper $escaper = null,
LoggerInterface $logger = null,
CategoryLinkManagementInterface $categoryLinkManagement = null,
StoreManagerInterface $storeManager = null,
?ProductFactory $productFactory = null
) {
parent::__construct($context, $productBuilder);
$this->initializationHelper = $initializationHelper;
$this->productCopier = $productCopier;
$this->productTypeManager = $productTypeManager;
$this->productRepository = $productRepository;
$this->escaper = $escaper ?: ObjectManager::getInstance()
->get(\Magento\Framework\Escaper::class);
$this->logger = $logger ?: ObjectManager::getInstance()
->get(\Psr\Log\LoggerInterface::class);
$this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
$this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class);
$this->categoryLinkManagement = $categoryLinkManagement ?: ObjectManager::getInstance()
->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class);
$this->storeManager = $storeManager ?: ObjectManager::getInstance()
->get(\Magento\Store\Model\StoreManagerInterface::class);
->get(CategoryLinkManagementInterface::class);
$this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
$this->productFactory = $productFactory ?: ObjectManager::getInstance()->get(ProductFactory::class);
}

/**
* Save product action
*
* @return \Magento\Backend\Model\View\Result\Redirect
* @return Redirect
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
Expand Down Expand Up @@ -166,7 +179,7 @@ public function execute()

if ($redirectBack === 'duplicate') {
$product->unsetData('quantity_and_stock_status');
$newProduct = $this->productCopier->copy($product);
$newProduct = $this->productCopier->copy($product, $this->productFactory->create());
$this->checkUniqueAttributes($product);
$this->messageManager->addSuccessMessage(__('You duplicated the product.'));
}
Expand Down
95 changes: 5 additions & 90 deletions app/code/Magento/Catalog/Model/Product/Copier.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Option\Repository as OptionRepository;
use Magento\Catalog\Model\ProductFactory;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Store\Model\Store;
use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException;

/**
* Catalog product copier.
Expand Down Expand Up @@ -75,20 +73,19 @@ public function __construct(
* Create product duplicate
*
* @param Product $product
* @param Product $duplicate
* @return Product
*/
public function copy(Product $product): Product
public function copy(Product $product, Product $duplicate): Product
{
$product->getWebsiteIds();
$product->getCategoryIds();

$metadata = $this->metadataPool->getMetadata(ProductInterface::class);

/** @var Product $duplicate */
$duplicate = $this->productFactory->create();
$productData = $product->getData();
$productData = $this->removeStockItem($productData);
$duplicate->setData($productData);
$duplicate->addData($productData);
$duplicate->setOptions([]);
$duplicate->setMetaTitle(null);
$duplicate->setMetaKeyword(null);
Expand All @@ -101,95 +98,12 @@ public function copy(Product $product): Product
$duplicate->setId(null);
$duplicate->setStoreId(Store::DEFAULT_STORE_ID);
$this->copyConstructor->build($product, $duplicate);
$this->setDefaultUrl($product, $duplicate);
$this->setStoresUrl($product, $duplicate);
$duplicate->save();
$this->optionRepository->duplicate($product, $duplicate);

return $duplicate;
}

/**
* Set default URL.
*
* @param Product $product
* @param Product $duplicate
* @return void
*/
private function setDefaultUrl(Product $product, Product $duplicate) : void
{
$duplicate->setStoreId(Store::DEFAULT_STORE_ID);
$resource = $product->getResource();
$attribute = $resource->getAttribute('url_key');
$productId = $product->getId();
$urlKey = $resource->getAttributeRawValue($productId, 'url_key', Store::DEFAULT_STORE_ID);
do {
$urlKey = $this->modifyUrl($urlKey);
$duplicate->setUrlKey($urlKey);
} while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate));
$duplicate->setData('url_path', null);
$duplicate->save();
}

/**
* Set URL for each store.
*
* @param Product $product
* @param Product $duplicate
*
* @return void
* @throws UrlAlreadyExistsException
*/
private function setStoresUrl(Product $product, Product $duplicate) : void
{
$storeIds = $duplicate->getStoreIds();
$productId = $product->getId();
$productResource = $product->getResource();
$attribute = $productResource->getAttribute('url_key');
$duplicate->setData('save_rewrites_history', false);
foreach ($storeIds as $storeId) {
$useDefault = !$this->scopeOverriddenValue->containsValue(
ProductInterface::class,
$product,
'url_key',
$storeId
);
if ($useDefault) {
continue;
}

$duplicate->setStoreId($storeId);
$urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId);
$iteration = 0;

do {
if ($iteration === 10) {
throw new UrlAlreadyExistsException();
}

$urlKey = $this->modifyUrl($urlKey);
$duplicate->setUrlKey($urlKey);
$iteration++;
} while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate));
$duplicate->setData('url_path', null);
$productResource->saveAttribute($duplicate, 'url_path');
$productResource->saveAttribute($duplicate, 'url_key');
}
$duplicate->setStoreId(Store::DEFAULT_STORE_ID);
}

/**
* Modify URL key.
*
* @param string $urlKey
* @return string
*/
private function modifyUrl(string $urlKey) : string
{
return preg_match('/(.*)-(\d+)$/', $urlKey, $matches)
? $matches[1] . '-' . ($matches[2] + 1)
: $urlKey . '-1';
}

/**
* Remove stock item
*
Expand All @@ -204,6 +118,7 @@ private function removeStockItem(array $productData): array
$extensionAttributes->setData('stock_item', null);
}
}

return $productData;
}
}
Loading