Skip to content

Commit ceaa83e

Browse files
committed
ACP2E-2672: [Cloud] Special price API endpoint returns error when updating large numbers of products concurrently
1 parent bfea788 commit ceaa83e

File tree

2 files changed

+227
-57
lines changed

2 files changed

+227
-57
lines changed

app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php

Lines changed: 174 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Magento\Catalog\Model\Product;
1313
use Magento\Catalog\Model\Product\Media\Config;
1414
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
15+
use Magento\Catalog\Model\ResourceModel\Product\MediaGalleryValue;
16+
use Magento\Eav\Model\ResourceModel\AttributeValue;
1517
use Magento\Framework\App\Filesystem\DirectoryList;
1618
use Magento\Framework\App\ObjectManager;
1719
use Magento\Framework\EntityManager\MetadataPool;
@@ -30,6 +32,8 @@
3032
* @api
3133
*
3234
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35+
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
36+
* @SuppressWarnings(PHPMD.TooManyFields)
3337
* @since 101.0.0
3438
*/
3539
class CreateHandler implements ExtensionInterface
@@ -90,7 +94,7 @@ class CreateHandler implements ExtensionInterface
9094
/**
9195
* @var array
9296
*/
93-
private $imagesGallery;
97+
private $mediaEavCache;
9498

9599
/**
96100
* @var \Magento\Store\Model\StoreManagerInterface
@@ -102,6 +106,21 @@ class CreateHandler implements ExtensionInterface
102106
*/
103107
private $deleteValidator;
104108

109+
/**
110+
* @var MediaGalleryValue
111+
*/
112+
private $mediaGalleryValue;
113+
114+
/**
115+
* @var AttributeValue
116+
*/
117+
private AttributeValue $attributeValue;
118+
119+
/**
120+
* @var \Magento\Eav\Model\Config
121+
*/
122+
private $eavConfig;
123+
105124
/**
106125
* @var string[]
107126
*/
@@ -121,7 +140,11 @@ class CreateHandler implements ExtensionInterface
121140
* @param Database $fileStorageDb
122141
* @param StoreManagerInterface|null $storeManager
123142
* @param DeleteValidator|null $deleteValidator
143+
* @param MediaGalleryValue|null $mediaGalleryValue
144+
* @param AttributeValue|null $attributeValue
145+
* @param \Magento\Eav\Model\Config|null $config
124146
* @throws FileSystemException
147+
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
125148
*/
126149
public function __construct(
127150
MetadataPool $metadataPool,
@@ -132,7 +155,10 @@ public function __construct(
132155
Filesystem $filesystem,
133156
Database $fileStorageDb,
134157
StoreManagerInterface $storeManager = null,
135-
?DeleteValidator $deleteValidator = null
158+
?DeleteValidator $deleteValidator = null,
159+
?MediaGalleryValue $mediaGalleryValue = null,
160+
?AttributeValue $attributeValue = null,
161+
?\Magento\Eav\Model\Config $config = null
136162
) {
137163
$this->metadata = $metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
138164
$this->attributeRepository = $attributeRepository;
@@ -143,6 +169,9 @@ public function __construct(
143169
$this->fileStorageDb = $fileStorageDb;
144170
$this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
145171
$this->deleteValidator = $deleteValidator ?: ObjectManager::getInstance()->get(DeleteValidator::class);
172+
$this->mediaGalleryValue = $mediaGalleryValue ?? ObjectManager::getInstance()->get(MediaGalleryValue::class);
173+
$this->attributeValue = $attributeValue ?? ObjectManager::getInstance()->get(AttributeValue::class);
174+
$this->eavConfig = $config ?? ObjectManager::getInstance()->get(\Magento\Eav\Model\Config::class);
146175
}
147176

148177
/**
@@ -159,6 +188,7 @@ public function __construct(
159188
*/
160189
public function execute($product, $arguments = [])
161190
{
191+
$this->mediaEavCache = null;
162192
$attrCode = $this->getAttribute()->getAttributeCode();
163193

164194
$value = $product->getData($attrCode);
@@ -279,14 +309,15 @@ protected function processDeletedImages($product, array &$images)
279309
*/
280310
protected function processNewAndExistingImages($product, array &$images)
281311
{
312+
$existingGalleryStoreValues = $this->getExistingGalleryStoreValues($product);
282313
foreach ($images as &$image) {
283314
if (empty($image['removed'])) {
284315
$isNew = empty($image['value_id']);
285316
$data = $this->processNewImage($product, $image);
286317

287318
// Add per store labels, position, disabled
288-
$data['value_id'] = $image['value_id'];
289-
$data['label'] = isset($image['label']) ? $image['label'] : '';
319+
$data['value_id'] = (int) $image['value_id'];
320+
$data['label'] = !empty($image['label']) ? $image['label'] : null;
290321
$data['position'] = isset($image['position']) && $image['position'] !== ''
291322
? (int)$image['position']
292323
: null;
@@ -295,34 +326,90 @@ protected function processNewAndExistingImages($product, array &$images)
295326

296327
$data[$this->metadata->getLinkField()] = (int)$product->getData($this->metadata->getLinkField());
297328

298-
$this->saveGalleryStoreValue($product, $data);
299-
if ($isNew && $data['store_id'] !== Store::DEFAULT_STORE_ID) {
300-
$dataForDefaultScope = $data;
301-
$dataForDefaultScope['store_id'] = Store::DEFAULT_STORE_ID;
302-
$dataForDefaultScope['disabled'] = 0;
303-
$dataForDefaultScope['label'] = null;
304-
$this->saveGalleryStoreValue($product, $dataForDefaultScope);
329+
if ($isNew || $this->hasGalleryStoreValueChanged($data, $existingGalleryStoreValues)) {
330+
$this->saveGalleryStoreValue($product, $data, $isNew);
305331
}
306332
}
307333
}
308334
}
309335

336+
/**
337+
* Get existing gallery store values
338+
*
339+
* @param Product $product
340+
* @return array
341+
* @throws \Exception
342+
*/
343+
private function getExistingGalleryStoreValues(Product $product): array
344+
{
345+
$existingMediaGalleryValues = [];
346+
if (!$product->isObjectNew()) {
347+
$productId = (int)$product->getData($this->metadata->getLinkField());
348+
foreach ($this->mediaGalleryValue->getAllByEntityId($productId) as $data) {
349+
$existingMediaGalleryValues[] = [
350+
'value_id' => (int) $data['value_id'],
351+
'store_id' => (int) $data['store_id'],
352+
'label' => $data['label'] ?: null,
353+
'position' => $data['position'] !== null ? (int)$data['position'] : null,
354+
'disabled' => (int) $data['disabled'],
355+
];
356+
}
357+
}
358+
return $existingMediaGalleryValues;
359+
}
360+
361+
/**
362+
* Check if gallery store value has changed
363+
*
364+
* @param array $data
365+
* @param array $existingGalleryStoreValues
366+
* @return bool
367+
*/
368+
private function hasGalleryStoreValueChanged(array $data, array $existingGalleryStoreValues): bool
369+
{
370+
foreach ($existingGalleryStoreValues as $existingGalleryStoreValue) {
371+
if ($existingGalleryStoreValue['value_id'] === $data['value_id']
372+
&& $existingGalleryStoreValue['store_id'] === $data['store_id']
373+
&& $existingGalleryStoreValue['label'] === $data['label']
374+
&& $existingGalleryStoreValue['position'] === $data['position']
375+
&& $existingGalleryStoreValue['disabled'] === $data['disabled']
376+
) {
377+
return false;
378+
}
379+
}
380+
381+
return true;
382+
}
383+
310384
/**
311385
* Save media gallery store value
312386
*
313387
* @param Product $product
314388
* @param array $data
389+
* @param bool $isNewImage
315390
*/
316-
private function saveGalleryStoreValue(Product $product, array $data): void
391+
private function saveGalleryStoreValue(Product $product, array $data, bool $isNewImage): void
317392
{
318-
if (!$product->isObjectNew()) {
319-
$this->resourceModel->deleteGalleryValueInStore(
320-
$data['value_id'],
321-
$data[$this->metadata->getLinkField()],
322-
$data['store_id']
323-
);
393+
$items = [];
394+
$items[] = $data;
395+
if ($isNewImage && $data['store_id'] !== Store::DEFAULT_STORE_ID) {
396+
$dataForDefaultScope = $data;
397+
$dataForDefaultScope['store_id'] = Store::DEFAULT_STORE_ID;
398+
$dataForDefaultScope['disabled'] = 0;
399+
$dataForDefaultScope['label'] = null;
400+
$items[] = $dataForDefaultScope;
401+
}
402+
403+
foreach ($items as $item) {
404+
if (!$product->isObjectNew()) {
405+
$this->resourceModel->deleteGalleryValueInStore(
406+
$item['value_id'],
407+
$item[$this->metadata->getLinkField()],
408+
$item['store_id']
409+
);
410+
}
411+
$this->resourceModel->insertGalleryValueInStore($item);
324412
}
325-
$this->resourceModel->insertGalleryValueInStore($data);
326413
}
327414

328415
/**
@@ -530,29 +617,26 @@ private function processMediaAttribute(
530617
array $clearImages,
531618
array $newImages
532619
): void {
533-
$storeId = $product->isObjectNew() ? Store::DEFAULT_STORE_ID : (int) $product->getStoreId();
534-
/***
535-
* Attributes values are saved as default value in single store mode
536-
* @see \Magento\Catalog\Model\ResourceModel\AbstractResource::_saveAttributeValue
537-
*/
538-
if ($storeId === Store::DEFAULT_STORE_ID
539-
|| $this->storeManager->hasSingleStore()
540-
|| $this->getMediaAttributeStoreValue($product, $mediaAttrCode, $storeId) !== null
541-
) {
542-
$value = $product->getData($mediaAttrCode);
620+
$storeId = $this->getStoreIdForUpdate($product);
621+
$oldValue = $this->getMediaAttributeStoreValue($product, $mediaAttrCode, $storeId);
622+
// Prevent from breaking store inheritance
623+
if ($oldValue !== false || $storeId === Store::DEFAULT_STORE_ID) {
624+
$value = $product->hasData($mediaAttrCode) ? $product->getData($mediaAttrCode) : $oldValue;
543625
$newValue = $value;
544626
if (in_array($value, $clearImages)) {
545627
$newValue = 'no_selection';
546628
}
547629
if (in_array($value, array_keys($newImages))) {
548630
$newValue = $newImages[$value]['new_file'];
549631
}
550-
$product->setData($mediaAttrCode, $newValue);
551-
$product->addAttributeUpdate(
552-
$mediaAttrCode,
553-
$newValue,
554-
$storeId
555-
);
632+
if ($oldValue !== $newValue) {
633+
$product->setData($mediaAttrCode, $newValue);
634+
$product->addAttributeUpdate(
635+
$mediaAttrCode,
636+
$newValue,
637+
$storeId
638+
);
639+
}
556640
}
557641
}
558642

@@ -565,6 +649,7 @@ private function processMediaAttribute(
565649
* @param array $newImages
566650
* @param array $existImages
567651
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
652+
* @SuppressWarnings(PHPMD.NPathComplexity)
568653
*/
569654
private function processMediaAttributeLabel(
570655
Product $product,
@@ -573,6 +658,9 @@ private function processMediaAttributeLabel(
573658
array $newImages,
574659
array $existImages
575660
): void {
661+
$storeId = $this->getStoreIdForUpdate($product);
662+
$oldAttrLabelValue = $this->getMediaAttributeStoreValue($product, $mediaAttrCode . '_label', $storeId);
663+
576664
$resetLabel = false;
577665
$attrData = $product->getData($mediaAttrCode);
578666
if (in_array($attrData, $clearImages)) {
@@ -595,33 +683,58 @@ private function processMediaAttributeLabel(
595683
$product->setData($mediaAttrCode . '_label', null);
596684
$resetLabel = true;
597685
}
598-
if (!empty($product->getData($mediaAttrCode . '_label'))
599-
|| $resetLabel === true
600-
) {
686+
687+
$newAttrLabelValue = $product->getData($mediaAttrCode . '_label');
688+
689+
if ($newAttrLabelValue !== $oldAttrLabelValue && ($resetLabel || !empty($newAttrLabelValue))) {
601690
$product->addAttributeUpdate(
602691
$mediaAttrCode . '_label',
603-
$product->getData($mediaAttrCode . '_label'),
604-
$product->getStoreId()
692+
$newAttrLabelValue,
693+
$storeId
605694
);
606695
}
607696
}
608697

609698
/**
610-
* Get product images for all stores
699+
* Get store id to update media attribute
611700
*
612-
* @param ProductInterface $product
613-
* @return array
701+
* Attributes values are saved in "all store views" in single store mode
702+
*
703+
* @param Product $product
704+
* @return int
705+
* @see \Magento\Catalog\Model\ResourceModel\AbstractResource::_saveAttributeValue
614706
*/
615-
private function getImagesForAllStores(ProductInterface $product)
707+
private function getStoreIdForUpdate(Product $product): int
616708
{
617-
if ($this->imagesGallery === null) {
618-
$storeIds = array_keys($this->storeManager->getStores());
619-
$storeIds[] = 0;
709+
return $product->isObjectNew() || $this->storeManager->hasSingleStore()
710+
? Store::DEFAULT_STORE_ID
711+
: (int) $product->getStoreId();
712+
}
620713

621-
$this->imagesGallery = $this->resourceModel->getProductImages($product, $storeIds);
714+
/**
715+
* Get all media attributes values
716+
*
717+
* @param Product $product
718+
* @return array
719+
*/
720+
private function getMediaAttributesValues(Product $product): array
721+
{
722+
if ($this->mediaEavCache === null) {
723+
$attributeCodes = [];
724+
foreach ($this->mediaConfig->getMediaAttributeCodes() as $attributeCode) {
725+
$attributeCodes[] = $attributeCode;
726+
if (in_array($attributeCode, $this->mediaAttributesWithLabels)) {
727+
$attributeCodes[] = $attributeCode . '_label';
728+
}
729+
}
730+
$this->mediaEavCache = $this->attributeValue->getValues(
731+
ProductInterface::class,
732+
(int) $product->getData($this->metadata->getLinkField()),
733+
$attributeCodes
734+
);
622735
}
623736

624-
return $this->imagesGallery;
737+
return $this->mediaEavCache;
625738
}
626739

627740
/**
@@ -630,18 +743,22 @@ private function getImagesForAllStores(ProductInterface $product)
630743
* @param Product $product
631744
* @param string $attributeCode
632745
* @param int|null $storeId
633-
* @return string|null
746+
* @return mixed|false
634747
*/
635-
private function getMediaAttributeStoreValue(Product $product, string $attributeCode, int $storeId = null): ?string
636-
{
637-
$gallery = $this->getImagesForAllStores($product);
748+
private function getMediaAttributeStoreValue(
749+
Product $product,
750+
string $attributeCode,
751+
int $storeId = null
752+
): mixed {
753+
$attributes = $this->eavConfig->getEntityAttributes(Product::ENTITY);
754+
$attributeId = $attributes[$attributeCode]->getAttributeId();
638755
$storeId = $storeId === null ? (int) $product->getStoreId() : $storeId;
639-
foreach ($gallery as $image) {
640-
if ($image['attribute_code'] === $attributeCode && ((int)$image['store_id']) === $storeId) {
641-
return $image['filepath'];
756+
foreach ($this->getMediaAttributesValues($product) as $value) {
757+
if ($value['attribute_id'] === $attributeId && ((int)$value['store_id']) === $storeId) {
758+
return $value['value'];
642759
}
643760
}
644-
return null;
761+
return false;
645762
}
646763

647764
/**

0 commit comments

Comments
 (0)