Skip to content

Refactor addlinks to own class take 3 (follows #21658) #23191

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 27 additions & 219 deletions app/code/Magento/CatalogImportExport/Model/Import/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Product\Link;
use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor;
use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor;
use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor;
use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface;
use Magento\CatalogImportExport\Model\Import\Product\StatusProcessor;
Expand Down Expand Up @@ -224,6 +225,8 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
/**
* Links attribute name-to-link type ID.
*
* @deprecated use DI for LinkProcessor class if you want to add additional types
*
* @var array
*/
protected $_linkNameToId = [
Expand Down Expand Up @@ -757,6 +760,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
*/
private $stockProcessor;

/**
* @var LinkProcessor
*/
private $linkProcessor;

/**
* @param \Magento\Framework\Json\Helper\Data $jsonHelper
* @param \Magento\ImportExport\Helper\Data $importExportData
Expand Down Expand Up @@ -804,6 +812,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
* @param ProductRepositoryInterface|null $productRepository
* @param StatusProcessor|null $statusProcessor
* @param StockProcessor|null $stockProcessor
* @param LinkProcessor|null $linkProcessor
* @throws LocalizedException
* @throws \Magento\Framework\Exception\FileSystemException
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
Expand Down Expand Up @@ -855,7 +864,8 @@ public function __construct(
DateTimeFactory $dateTimeFactory = null,
ProductRepositoryInterface $productRepository = null,
StatusProcessor $statusProcessor = null,
StockProcessor $stockProcessor = null
StockProcessor $stockProcessor = null,
LinkProcessor $linkProcessor = null
) {
$this->_eventManager = $eventManager;
$this->stockRegistry = $stockRegistry;
Expand Down Expand Up @@ -895,6 +905,10 @@ public function __construct(
->get(StatusProcessor::class);
$this->stockProcessor = $stockProcessor ?: ObjectManager::getInstance()
->get(StockProcessor::class);
$this->linkProcessor = $linkProcessor ?? ObjectManager::getInstance()
->get(LinkProcessor::class);
$this->linkProcessor->addNameToIds($this->_linkNameToId);

parent::__construct(
$jsonHelper,
$importExportData,
Expand Down Expand Up @@ -1132,14 +1146,15 @@ protected function _replaceProducts()
* Save products data.
*
* @return $this
* @throws LocalizedException
*/
protected function _saveProductsData()
{
$this->_saveProducts();
foreach ($this->_productTypeModels as $productTypeModel) {
$productTypeModel->saveData();
}
$this->_saveLinks();
$this->linkProcessor->saveLinks($this, $this->_dataSourceModel, $this->getProductEntityLinkField());
$this->_saveStockItem();
if ($this->_replaceFlag) {
$this->getOptionEntity()->clearProductsSkuToId();
Expand Down Expand Up @@ -1273,30 +1288,13 @@ protected function _prepareRowForDb(array $rowData)
*
* Must be called after ALL products saving done.
*
* @deprecated use linkProcessor Directly
*
* @return $this
*/
protected function _saveLinks()
{
/** @var Link $resource */
$resource = $this->_linkFactory->create();
$mainTable = $resource->getMainTable();
$positionAttrId = [];
$nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable);

// pre-load 'position' attributes ID for each link type once
foreach ($this->_linkNameToId as $linkId) {
$select = $this->_connection->select()->from(
$resource->getTable('catalog_product_link_attribute'),
['id' => 'product_link_attribute_id']
)->where(
'link_type_id = :link_id AND product_link_attribute_code = :position'
);
$bind = [':link_id' => $linkId, ':position' => 'position'];
$positionAttrId[$linkId] = $this->_connection->fetchOne($select, $bind);
}
while ($bunch = $this->_dataSourceModel->getNextBunch()) {
$this->processLinkBunches($bunch, $resource, $nextLinkId, $positionAttrId);
}
$this->linkProcessor->saveLinks($this, $this->_dataSourceModel, $this->getProductEntityLinkField());
return $this;
}

Expand Down Expand Up @@ -1533,14 +1531,19 @@ public function getImagesFromRow(array $rowData)
return [$images, $labels];
}

// phpcs:disable Generic.Metrics.NestingLevel

/**
* Gather and save information about product entities.
*
* FIXME: Reduce nesting level
*
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
* @throws LocalizedException
* phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
Expand Down Expand Up @@ -1928,6 +1931,8 @@ protected function _saveProducts()
}
//phpcs:enable Generic.Metrics.NestingLevel

// phpcs:enable

/**
* Prepare array with image states (visible or hidden from product page)
*
Expand Down Expand Up @@ -3113,203 +3118,6 @@ private function getValidationErrorLevel($sku): string
: ProcessingError::ERROR_LEVEL_NOT_CRITICAL;
}

/**
* Processes link bunches
*
* @param array $bunch
* @param Link $resource
* @param int $nextLinkId
* @param array $positionAttrId
* @return void
* @throws LocalizedException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function processLinkBunches(
array $bunch,
Link $resource,
int $nextLinkId,
array $positionAttrId
): void {
$productIds = [];
$linkRows = [];
$positionRows = [];
$linksToDelete = [];

$bunch = array_filter($bunch, [$this, 'isRowAllowedToImport'], ARRAY_FILTER_USE_BOTH);
foreach ($bunch as $rowData) {
$sku = $rowData[self::COL_SKU];
$productId = $this->skuProcessor->getNewSku($sku)[$this->getProductEntityLinkField()];
$productIds[] = $productId;
$productLinkKeys = $this->fetchProductLinks($resource, $productId);
$linkNameToId = array_filter(
$this->_linkNameToId,
function ($linkName) use ($rowData) {
return isset($rowData[$linkName . 'sku']);
},
ARRAY_FILTER_USE_KEY
);
foreach ($linkNameToId as $linkName => $linkId) {
$linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']);
//process empty value
if (!empty($linkSkus[0]) && $linkSkus[0] === $this->getEmptyAttributeValueConstant()) {
$linksToDelete[$linkId][] = $productId;
continue;
}

$linkPositions = !empty($rowData[$linkName . 'position'])
? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position'])
: [];
$linkSkus = array_filter(
$linkSkus,
function ($linkedSku) use ($sku) {
$linkedSku = trim($linkedSku);
return ($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku))
&& strcasecmp($linkedSku, $sku) !== 0;
}
);

foreach ($linkSkus as $linkedKey => $linkedSku) {
$linkedId = $this->getProductLinkedId($linkedSku);
if ($linkedId == null) {
// Import file links to a SKU which is skipped for some reason, which leads to a "NULL"
// link causing fatal errors.
$formatStr = 'WARNING: Orphaned link skipped: From SKU %s (ID %d) to SKU %s, Link type id: %d';
$exception = new \Exception(sprintf($formatStr, $sku, $productId, $linkedSku, $linkId));
$this->_logger->critical($exception);
continue;
}
$linkKey = $this->composeLinkKey($productId, $linkedId, $linkId);
$productLinkKeys[$linkKey] = $productLinkKeys[$linkKey] ?? $nextLinkId;

$linkRows[$linkKey] = $linkRows[$linkKey] ?? [
'link_id' => $productLinkKeys[$linkKey],
'product_id' => $productId,
'linked_product_id' => $linkedId,
'link_type_id' => $linkId,
];

if (!empty($linkPositions[$linkedKey])) {
$positionRows[] = [
'link_id' => $productLinkKeys[$linkKey],
'product_link_attribute_id' => $positionAttrId[$linkId],
'value' => $linkPositions[$linkedKey],
];
}
$nextLinkId++;
}
}
}
$this->deleteProductsLinks($resource, $linksToDelete);
$this->saveLinksData($resource, $productIds, $linkRows, $positionRows);
}

/**
* Delete links
*
* @param Link $resource
* @param array $linksToDelete
* @return void
* @throws LocalizedException
*/
private function deleteProductsLinks(Link $resource, array $linksToDelete)
{
if (!empty($linksToDelete) && Import::BEHAVIOR_APPEND === $this->getBehavior()) {
foreach ($linksToDelete as $linkTypeId => $productIds) {
if (!empty($productIds)) {
$whereLinkId = $this->_connection->quoteInto('link_type_id', $linkTypeId);
$whereProductId = $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds));
$this->_connection->delete(
$resource->getMainTable(),
$whereLinkId . ' AND ' . $whereProductId
);
}
}
}
}

/**
* Fetches Product Links
*
* @param Link $resource
* @param int $productId
* @return array
*/
private function fetchProductLinks(Link $resource, int $productId) : array
{
$productLinkKeys = [];
$select = $this->_connection->select()->from(
$resource->getTable('catalog_product_link'),
['id' => 'link_id', 'linked_id' => 'linked_product_id', 'link_type_id' => 'link_type_id']
)->where(
'product_id = :product_id'
);
$bind = [':product_id' => $productId];
foreach ($this->_connection->fetchAll($select, $bind) as $linkData) {
$linkKey = $this->composeLinkKey($productId, $linkData['linked_id'], $linkData['link_type_id']);
$productLinkKeys[$linkKey] = $linkData['id'];
}

return $productLinkKeys;
}

/**
* Gets the Id of the Sku
*
* @param string $linkedSku
* @return int|null
*/
private function getProductLinkedId(string $linkedSku) : ?int
{
$linkedSku = trim($linkedSku);
$newSku = $this->skuProcessor->getNewSku($linkedSku);
$linkedId = !empty($newSku) ? $newSku['entity_id'] : $this->getExistingSku($linkedSku)['entity_id'];
return $linkedId;
}

/**
* Saves information about product links
*
* @param Link $resource
* @param array $productIds
* @param array $linkRows
* @param array $positionRows
* @throws LocalizedException
*/
private function saveLinksData(Link $resource, array $productIds, array $linkRows, array $positionRows)
{
$mainTable = $resource->getMainTable();
if (Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) {
$this->_connection->delete(
$mainTable,
$this->_connection->quoteInto('product_id IN (?)', array_unique($productIds))
);
}
if ($linkRows) {
$this->_connection->insertOnDuplicate($mainTable, $linkRows, ['link_id']);
}
if ($positionRows) {
// process linked product positions
$this->_connection->insertOnDuplicate(
$resource->getAttributeTypeTable('int'),
$positionRows,
['value']
);
}
}

/**
* Composes the link key
*
* @param int $productId
* @param int $linkedId
* @param int $linkTypeId
* @return string
*/
private function composeLinkKey(int $productId, int $linkedId, int $linkTypeId) : string
{
return "{$productId}-{$linkedId}-{$linkTypeId}";
}

/**
* Get row store ID
*
Expand Down
Loading