Skip to content

Commit 2195e72

Browse files
author
Viktor Kopin
committed
MC-38931: Product URL Rewrites are not removed when product removed from website
1 parent 500a9e5 commit 2195e72

File tree

17 files changed

+1308
-262
lines changed

17 files changed

+1308
-262
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\CatalogUrlRewrite\Model\Product;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\CatalogUrlRewrite\Model\ResourceModel\Product\GetUrlRewriteData;
12+
use Magento\Store\Model\Store;
13+
14+
/**
15+
* Product data needed for url rewrite generation locator class
16+
*/
17+
class GetProductUrlRewriteDataByStore
18+
{
19+
/**
20+
* @var array
21+
*/
22+
private $urlRewriteData = [];
23+
24+
/**
25+
* @var GetUrlRewriteData
26+
*/
27+
private $getUrlRewriteData;
28+
29+
/**
30+
* @param GetUrlRewriteData $getUrlRewriteData
31+
*/
32+
public function __construct(GetUrlRewriteData $getUrlRewriteData)
33+
{
34+
$this->getUrlRewriteData = $getUrlRewriteData;
35+
}
36+
37+
/**
38+
* Retrieves data for product by store
39+
*
40+
* @param ProductInterface $product
41+
* @param int $storeId
42+
* @return array
43+
*/
44+
public function execute(ProductInterface $product, int $storeId): array
45+
{
46+
$productId = $product->getId();
47+
if (isset($this->urlRewriteData[$productId][$storeId])) {
48+
return $this->urlRewriteData[$productId][$storeId];
49+
}
50+
if (empty($this->urlRewriteData[$productId])) {
51+
$storesData = $this->getUrlRewriteData->execute($product);
52+
foreach ($storesData as $storeData) {
53+
$this->urlRewriteData[$productId][$storeData['store_id']] = [
54+
'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']),
55+
'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'],
56+
];
57+
}
58+
}
59+
60+
if (!isset($this->urlRewriteData[$productId][$storeId])) {
61+
$this->urlRewriteData[$productId][$storeId] = $this->urlRewriteData[$productId][Store::DEFAULT_STORE_ID];
62+
}
63+
64+
return $this->urlRewriteData[$productId][$storeId];
65+
}
66+
67+
/**
68+
* Clears product url rewrite data in local cache
69+
*
70+
* @param ProductInterface $product
71+
*/
72+
public function clearProductUrlRewriteDataCache(ProductInterface $product)
73+
{
74+
unset($this->urlRewriteData[$product->getId()]);
75+
}
76+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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\CatalogUrlRewrite\Model\Products;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\Catalog\Model\Product\Visibility;
13+
use Magento\CatalogUrlRewrite\Model\Product\GetProductUrlRewriteDataByStore;
14+
use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator;
15+
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
16+
use Magento\CatalogUrlRewrite\Service\V1\StoreViewService;
17+
use Magento\Store\Model\Store;
18+
use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException;
19+
use Magento\UrlRewrite\Model\UrlPersistInterface;
20+
21+
/**
22+
* Update existing url rewrites or create new ones if needed
23+
*/
24+
class AppendUrlRewritesToProducts
25+
{
26+
/**
27+
* @var ProductUrlRewriteGenerator
28+
*/
29+
private $productUrlRewriteGenerator;
30+
31+
/**
32+
* @var StoreViewService
33+
*/
34+
private $storeViewService;
35+
36+
/**
37+
* @var ProductUrlPathGenerator
38+
*/
39+
private $productUrlPathGenerator;
40+
41+
/**
42+
* @var UrlPersistInterface
43+
*/
44+
private $urlPersist;
45+
46+
/**
47+
* @var GetProductUrlRewriteDataByStore
48+
*/
49+
private $getDataByStore;
50+
51+
/**
52+
* @param ProductUrlRewriteGenerator $urlRewriteGenerator
53+
* @param StoreViewService $storeViewService
54+
* @param ProductUrlPathGenerator $urlPathGenerator
55+
* @param UrlPersistInterface $urlPersist
56+
* @param GetProductUrlRewriteDataByStore $getDataByStore
57+
*/
58+
public function __construct(
59+
ProductUrlRewriteGenerator $urlRewriteGenerator,
60+
StoreViewService $storeViewService,
61+
ProductUrlPathGenerator $urlPathGenerator,
62+
UrlPersistInterface $urlPersist,
63+
GetProductUrlRewriteDataByStore $getDataByStore
64+
) {
65+
$this->productUrlRewriteGenerator = $urlRewriteGenerator;
66+
$this->storeViewService = $storeViewService;
67+
$this->productUrlPathGenerator = $urlPathGenerator;
68+
$this->urlPersist = $urlPersist;
69+
$this->getDataByStore = $getDataByStore;
70+
}
71+
72+
/**
73+
* Update existing rewrites and add for specific stores websites
74+
*
75+
* @param ProductInterface[] $products
76+
* @param array $storesToAdd
77+
* @throws UrlAlreadyExistsException
78+
*/
79+
public function execute(array $products, array $storesToAdd): void
80+
{
81+
foreach ($products as $product) {
82+
$forceGenerateDefault = false;
83+
foreach ($storesToAdd as $storeId) {
84+
if ($this->needGenerateUrlForStore($product, (int)$storeId)) {
85+
$urls[] = $this->generateUrls($product, (int)$storeId);
86+
} elseif ((int)$product->getStoreId() !== Store::DEFAULT_STORE_ID) {
87+
$forceGenerateDefault = true;
88+
}
89+
}
90+
if ($product->getStoreId() === Store::DEFAULT_STORE_ID
91+
|| $this->isProductAssignedToStore($product)) {
92+
$product->unsUrlPath();
93+
$product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product));
94+
$urls[] = $this->productUrlRewriteGenerator->generate($product);
95+
}
96+
if ($forceGenerateDefault && $product->getStoreId() !== Store::DEFAULT_STORE_ID) {
97+
$urls[] = $this->generateUrls($product, Store::DEFAULT_STORE_ID);
98+
}
99+
$this->getDataByStore->clearProductUrlRewriteDataCache($product);
100+
}
101+
if (!empty($urls)) {
102+
$this->urlPersist->replace(array_merge(...$urls));
103+
}
104+
}
105+
106+
/**
107+
* Generate urls for specific store
108+
*
109+
* @param ProductInterface $product
110+
* @param int $storeId
111+
* @return array
112+
*/
113+
private function generateUrls(ProductInterface $product, int $storeId): array
114+
{
115+
$storeData = $this->getDataByStore->execute($product, $storeId);
116+
$origStoreId = $product->getStoreId();
117+
$origVisibility = $product->getVisibility();
118+
$origUrlKey = $product->getUrlKey();
119+
$product->setStoreId($storeId);
120+
$product->setVisibility($storeData['visibility'] ?? Visibility::VISIBILITY_NOT_VISIBLE);
121+
$product->setUrlKey($storeData['url_key'] ?? '');
122+
$product->unsUrlPath();
123+
$product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product));
124+
$urls = $this->productUrlRewriteGenerator->generate($product);
125+
$product->setStoreId($origStoreId);
126+
$product->setVisibility($origVisibility);
127+
$product->setUrlKey($origUrlKey);
128+
129+
return $urls;
130+
}
131+
132+
/**
133+
* Does product has scope overridden url key value
134+
*
135+
* @param ProductInterface $product
136+
* @param int $storeId
137+
* @return bool
138+
*/
139+
private function needGenerateUrlForStore(ProductInterface $product, int $storeId): bool
140+
{
141+
return (int)$product->getStoreId() !== $storeId
142+
&& $this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore(
143+
$storeId,
144+
$product->getId(),
145+
Product::ENTITY
146+
);
147+
}
148+
149+
/**
150+
* Is product still assigned to store which request is performed from
151+
*
152+
* @param ProductInterface $product
153+
* @return bool
154+
*/
155+
private function isProductAssignedToStore(ProductInterface $product): bool
156+
{
157+
return in_array($product->getStoreId(), $product->getStoreIds());
158+
}
159+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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\CatalogUrlRewrite\Model\ResourceModel\Product;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\Eav\Model\Config;
13+
use Magento\Framework\App\ResourceConnection;
14+
use Magento\Framework\DB\Select;
15+
use Magento\Framework\EntityManager\MetadataPool;
16+
17+
/**
18+
* Fetch product url rewrite data from database
19+
*/
20+
class GetUrlRewriteData
21+
{
22+
/**
23+
* @var MetadataPool
24+
*/
25+
private $metadataPool;
26+
27+
/**
28+
* @var ResourceConnection
29+
*/
30+
private $resource;
31+
32+
/**
33+
* @var Config
34+
*/
35+
private $eavConfig;
36+
37+
/**
38+
* @param MetadataPool $metadataPool
39+
* @param ResourceConnection $connection
40+
* @param Config $eavConfig
41+
*/
42+
public function __construct(
43+
MetadataPool $metadataPool,
44+
ResourceConnection $connection,
45+
Config $eavConfig
46+
) {
47+
$this->metadataPool = $metadataPool;
48+
$this->resource = $connection;
49+
$this->eavConfig = $eavConfig;
50+
}
51+
52+
/**
53+
* Fetches product store data required for url key generation
54+
*
55+
* @param ProductInterface $product
56+
* @return array
57+
*/
58+
public function execute(ProductInterface $product): array
59+
{
60+
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
61+
$linkField = $metadata->getLinkField();
62+
$connection = $this->resource->getConnection();
63+
$visibilityAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'visibility');
64+
$urlKeyAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'url_key');
65+
$visibilitySelect = $connection->select()
66+
->from(['visibility' => $visibilityAttribute->getBackendTable()])
67+
->joinRight(
68+
['url_key' => $urlKeyAttribute->getBackendTable()],
69+
'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id'
70+
. ' AND url_key.attribute_id = ' . $urlKeyAttribute->getId(),
71+
['url_key.value as url_key']
72+
)
73+
->reset(Select::COLUMNS)
74+
->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value AS visibility'])
75+
->where('visibility.' . $linkField . ' = ?', $product->getData($linkField))
76+
->where('visibility.attribute_id = ?', $visibilityAttribute->getId());
77+
$urlKeySelect = $connection->select()
78+
->from(['url_key' => $urlKeyAttribute->getBackendTable()])
79+
->joinLeft(
80+
['visibility' => $visibilityAttribute->getBackendTable()],
81+
'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id'
82+
. ' AND visibility.attribute_id = ' . $visibilityAttribute->getId(),
83+
['visibility.value as visibility']
84+
)
85+
->reset(Select::COLUMNS)
86+
->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value as visibility'])
87+
->where('url_key.' . $linkField . ' = ?', $product->getData($linkField))
88+
->where('url_key.attribute_id = ?', $urlKeyAttribute->getId());
89+
90+
$select = $connection->select()->union([$visibilitySelect, $urlKeySelect], Select::SQL_UNION);
91+
92+
return $connection->fetchAll($select);
93+
}
94+
}

0 commit comments

Comments
 (0)