Skip to content
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