Skip to content

Commit dd929f8

Browse files
committed
MC-36903: Records are not deleted when unassigning an item from a website which causes image duplication when executing POST rest/all/V1/products
- Remove eav attributes stores values when product is unassigned from website
1 parent 849b6f0 commit dd929f8

File tree

4 files changed

+375
-7
lines changed

4 files changed

+375
-7
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,10 +596,21 @@ private function canRemoveImage(ProductInterface $product, string $imageFile) :b
596596
$canRemoveImage = true;
597597
$gallery = $this->getImagesForAllStores($product);
598598
$storeId = $product->getStoreId();
599+
$storeIds = [];
600+
$storeIds[] = 0;
601+
$websiteIds = array_map('intval', $product->getWebsiteIds() ?? []);
602+
foreach ($this->storeManager->getStores() as $store) {
603+
if (in_array((int) $store->getWebsiteId(), $websiteIds, true)) {
604+
$storeIds[] = (int) $store->getId();
605+
}
606+
}
599607

600608
if (!empty($gallery)) {
601609
foreach ($gallery as $image) {
602-
if ($image['filepath'] === $imageFile && (int) $image['store_id'] !== $storeId) {
610+
if (in_array((int) $image['store_id'], $storeIds)
611+
&& $image['filepath'] === $imageFile
612+
&& (int) $image['store_id'] !== $storeId
613+
) {
603614
$canRemoveImage = false;
604615
}
605616
}

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

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,69 @@
55
*/
66
namespace Magento\Catalog\Model\Product\Gallery;
77

8+
use Magento\Catalog\Api\Data\ProductInterface;
9+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Catalog\Model\Product\Media\Config;
812
use Magento\Catalog\Model\ResourceModel\Product\Gallery;
9-
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
13+
use Magento\Eav\Model\ResourceModel\AttributeValue;
14+
use Magento\Framework\App\ObjectManager;
15+
use Magento\Framework\EntityManager\MetadataPool;
16+
use Magento\Framework\Filesystem;
17+
use Magento\Framework\Json\Helper\Data;
18+
use Magento\MediaStorage\Helper\File\Storage\Database;
19+
use Magento\Store\Model\Store;
20+
use Magento\Store\Model\StoreManagerInterface;
1021

1122
/**
1223
* Update handler for catalog product gallery.
1324
*
1425
* @api
1526
* @since 101.0.0
27+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1628
*/
17-
class UpdateHandler extends \Magento\Catalog\Model\Product\Gallery\CreateHandler
29+
class UpdateHandler extends CreateHandler
1830
{
31+
/**
32+
* @var AttributeValue
33+
*/
34+
private $attributeValue;
35+
36+
/**
37+
* @param MetadataPool $metadataPool
38+
* @param ProductAttributeRepositoryInterface $attributeRepository
39+
* @param Gallery $resourceModel
40+
* @param Data $jsonHelper
41+
* @param Config $mediaConfig
42+
* @param Filesystem $filesystem
43+
* @param Database $fileStorageDb
44+
* @param StoreManagerInterface|null $storeManager
45+
* @param AttributeValue|null $attributeValue
46+
*/
47+
public function __construct(
48+
MetadataPool $metadataPool,
49+
ProductAttributeRepositoryInterface $attributeRepository,
50+
Gallery $resourceModel,
51+
Data $jsonHelper,
52+
Config $mediaConfig,
53+
Filesystem $filesystem,
54+
Database $fileStorageDb,
55+
StoreManagerInterface $storeManager = null,
56+
?AttributeValue $attributeValue = null
57+
) {
58+
parent::__construct(
59+
$metadataPool,
60+
$attributeRepository,
61+
$resourceModel,
62+
$jsonHelper,
63+
$mediaConfig,
64+
$filesystem,
65+
$fileStorageDb,
66+
$storeManager
67+
);
68+
$this->attributeValue = $attributeValue ?: ObjectManager::getInstance()->get(AttributeValue::class);
69+
}
70+
1971
/**
2072
* @inheritdoc
2173
*
@@ -26,6 +78,7 @@ protected function processDeletedImages($product, array &$images)
2678
$filesToDelete = [];
2779
$recordsToDelete = [];
2880
$picturesInOtherStores = [];
81+
$imagesToDelete = [];
2982

3083
foreach ($this->resourceModel->getProductImages($product, $this->extractStoreIds($product)) as $image) {
3184
$picturesInOtherStores[$image['filepath']] = true;
@@ -38,6 +91,7 @@ protected function processDeletedImages($product, array &$images)
3891
continue;
3992
}
4093
$recordsToDelete[] = $image['value_id'];
94+
$imagesToDelete[] = $image['file'];
4195
$catalogPath = $this->mediaConfig->getBaseMediaPath();
4296
$isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']);
4397
// only delete physical files if they are not used by any other products and if this file exist
@@ -48,8 +102,8 @@ protected function processDeletedImages($product, array &$images)
48102
}
49103
}
50104

105+
$this->deleteMediaAttributeValues($product, $imagesToDelete);
51106
$this->resourceModel->deleteGallery($recordsToDelete);
52-
53107
$this->removeDeletedImages($filesToDelete);
54108
}
55109

@@ -94,14 +148,14 @@ protected function processNewImage($product, array &$image)
94148
/**
95149
* Retrieve store ids from product.
96150
*
97-
* @param \Magento\Catalog\Model\Product $product
151+
* @param Product $product
98152
* @return array
99153
* @since 101.0.0
100154
*/
101155
protected function extractStoreIds($product)
102156
{
103157
$storeIds = $product->getStoreIds();
104-
$storeIds[] = \Magento\Store\Model\Store::DEFAULT_STORE_ID;
158+
$storeIds[] = Store::DEFAULT_STORE_ID;
105159

106160
// Removing current storeId.
107161
$storeIds = array_flip($storeIds);
@@ -125,5 +179,35 @@ protected function removeDeletedImages(array $files)
125179
foreach ($files as $filePath) {
126180
$this->mediaDirectory->delete($catalogPath . '/' . $filePath);
127181
}
182+
return null;
183+
}
184+
185+
/**
186+
* Delete media attributes values for given images
187+
*
188+
* @param Product $product
189+
* @param string[] $images
190+
*/
191+
private function deleteMediaAttributeValues(Product $product, array $images): void
192+
{
193+
if ($images) {
194+
$values = $this->attributeValue->getValues(
195+
ProductInterface::class,
196+
$product->getData($this->metadata->getLinkField()),
197+
$this->mediaConfig->getMediaAttributeCodes()
198+
);
199+
$valuesToDelete = [];
200+
foreach ($values as $value) {
201+
if (in_array($value['value'], $images, true)) {
202+
$valuesToDelete[] = $value;
203+
}
204+
}
205+
if ($valuesToDelete) {
206+
$this->attributeValue->deleteValues(
207+
ProductInterface::class,
208+
$valuesToDelete
209+
);
210+
}
211+
}
128212
}
129213
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Eav\Model\ResourceModel;
9+
10+
use Magento\Eav\Model\Config;
11+
use Magento\Framework\App\ResourceConnection;
12+
use Magento\Framework\DB\Select;
13+
use Magento\Framework\DB\Sql\UnionExpression;
14+
use Magento\Framework\EntityManager\MetadataPool;
15+
16+
/**
17+
* Entity attribute values resource
18+
*/
19+
class AttributeValue
20+
{
21+
/**
22+
* @var MetadataPool
23+
*/
24+
private $metadataPool;
25+
/**
26+
* @var ResourceConnection
27+
*/
28+
private $resourceConnection;
29+
/**
30+
* @var Config
31+
*/
32+
private $config;
33+
34+
/**
35+
* @param ResourceConnection $resourceConnection
36+
* @param MetadataPool $metadataPool
37+
* @param Config $config
38+
*/
39+
public function __construct(
40+
ResourceConnection $resourceConnection,
41+
MetadataPool $metadataPool,
42+
Config $config
43+
) {
44+
$this->resourceConnection = $resourceConnection;
45+
$this->metadataPool = $metadataPool;
46+
$this->config = $config;
47+
}
48+
49+
/**
50+
* Get attribute values for given entity type, entity ID, attribute codes and store IDs
51+
*
52+
* @param string $entityType
53+
* @param int $entityId
54+
* @param string[] $attributeCodes
55+
* @param int[] $storeIds
56+
* @return array
57+
*/
58+
public function getValues(
59+
string $entityType,
60+
int $entityId,
61+
array $attributeCodes = [],
62+
array $storeIds = []
63+
): array {
64+
$metadata = $this->metadataPool->getMetadata($entityType);
65+
$connection = $metadata->getEntityConnection();
66+
$selects = [];
67+
$attributeTables = [];
68+
$attributes = [];
69+
$allAttributes = $this->getEntityAttributes($entityType);
70+
$result = [];
71+
if ($attributeCodes) {
72+
foreach ($attributeCodes as $attributeCode) {
73+
$attributes[$attributeCode] = $allAttributes[$attributeCode];
74+
}
75+
} else {
76+
$attributes = $allAttributes;
77+
}
78+
79+
foreach ($attributes as $attribute) {
80+
if (!$attribute->isStatic()) {
81+
$attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId();
82+
}
83+
}
84+
85+
if ($attributeTables) {
86+
foreach ($attributeTables as $attributeTable => $attributeIds) {
87+
$select = $connection->select()
88+
->from(
89+
['t' => $attributeTable],
90+
['*']
91+
)
92+
->where($metadata->getLinkField() . ' = ?', $entityId)
93+
->where('attribute_id IN (?)', $attributeIds);
94+
if (!empty($storeIds)) {
95+
$select->where(
96+
'store_id IN (?)',
97+
$storeIds
98+
);
99+
}
100+
$selects[] = $select;
101+
}
102+
103+
if (count($selects) > 1) {
104+
$select = $connection->select();
105+
$select->from(['u' => new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )')]);
106+
} else {
107+
$select = reset($selects);
108+
}
109+
110+
$result = $connection->fetchAll($select);
111+
}
112+
113+
return $result;
114+
}
115+
116+
/**
117+
* Delete attribute values
118+
*
119+
* @param string $entityType
120+
* @param array[][] $values
121+
* Format:
122+
* array(
123+
* 0 => array(
124+
* value_id => 1,
125+
* attribute_id => 11
126+
* ),
127+
* 1 => array(
128+
* value_id => 2,
129+
* attribute_id => 22
130+
* )
131+
* )
132+
* @throws \Magento\Framework\Exception\LocalizedException
133+
*/
134+
public function deleteValues(string $entityType, array $values): void
135+
{
136+
$metadata = $this->metadataPool->getMetadata($entityType);
137+
$connection = $metadata->getEntityConnection();
138+
$attributeTables = [];
139+
$allAttributes = [];
140+
141+
foreach ($this->getEntityAttributes($entityType) as $attribute) {
142+
$allAttributes[(int) $attribute->getAttributeId()] = $attribute;
143+
}
144+
145+
foreach ($values as $value) {
146+
$attribute = $allAttributes[(int) $value['attribute_id']] ?? null;
147+
if ($attribute && !$attribute->isStatic()) {
148+
$attributeTables[$attribute->getBackend()->getTable()][] = (int) $value['value_id'];
149+
}
150+
}
151+
152+
foreach ($attributeTables as $attributeTable => $valueIds) {
153+
$connection->delete(
154+
$attributeTable,
155+
[
156+
'value_id IN (?)' => $valueIds
157+
]
158+
);
159+
}
160+
}
161+
162+
/**
163+
* Get attribute of given entity type
164+
*
165+
* @param string $entityType
166+
*/
167+
private function getEntityAttributes(string $entityType)
168+
{
169+
$metadata = $this->metadataPool->getMetadata($entityType);
170+
$eavEntityType = $metadata->getEavEntityType();
171+
return null === $eavEntityType ? [] : $this->config->getEntityAttributes($eavEntityType);
172+
}
173+
}

0 commit comments

Comments
 (0)