diff --git a/vendor/magento/module-catalog-url-rewrite/Model/Product/GetProductUrlRewriteDataByStore.php b/vendor/magento/module-catalog-url-rewrite/Model/Product/GetProductUrlRewriteDataByStore.php new file mode 100644 index 00000000000..fbacddac1ce --- /dev/null +++ b/vendor/magento/module-catalog-url-rewrite/Model/Product/GetProductUrlRewriteDataByStore.php @@ -0,0 +1,76 @@ +getUrlRewriteData = $getUrlRewriteData; + } + + /** + * Retrieves data for product by store + * + * @param ProductInterface $product + * @param int $storeId + * @return array + */ + public function execute(ProductInterface $product, int $storeId): array + { + $productId = $product->getId(); + if (isset($this->urlRewriteData[$productId][$storeId])) { + return $this->urlRewriteData[$productId][$storeId]; + } + if (empty($this->urlRewriteData[$productId])) { + $storesData = $this->getUrlRewriteData->execute($product); + foreach ($storesData as $storeData) { + $this->urlRewriteData[$productId][$storeData['store_id']] = [ + 'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), + 'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], + ]; + } + } + + if (!isset($this->urlRewriteData[$productId][$storeId])) { + $this->urlRewriteData[$productId][$storeId] = $this->urlRewriteData[$productId][Store::DEFAULT_STORE_ID]; + } + + return $this->urlRewriteData[$productId][$storeId]; + } + + /** + * Clears product url rewrite data in local cache + * + * @param ProductInterface $product + */ + public function clearProductUrlRewriteDataCache(ProductInterface $product) + { + unset($this->urlRewriteData[$product->getId()]); + } +} diff --git a/vendor/magento/module-catalog-url-rewrite/Model/Products/AppendUrlRewritesToProducts.php b/vendor/magento/module-catalog-url-rewrite/Model/Products/AppendUrlRewritesToProducts.php new file mode 100644 index 00000000000..15d4aabf424 --- /dev/null +++ b/vendor/magento/module-catalog-url-rewrite/Model/Products/AppendUrlRewritesToProducts.php @@ -0,0 +1,159 @@ +productUrlRewriteGenerator = $urlRewriteGenerator; + $this->storeViewService = $storeViewService; + $this->productUrlPathGenerator = $urlPathGenerator; + $this->urlPersist = $urlPersist; + $this->getDataByStore = $getDataByStore; + } + + /** + * Update existing rewrites and add for specific stores websites + * + * @param ProductInterface[] $products + * @param array $storesToAdd + * @throws UrlAlreadyExistsException + */ + public function execute(array $products, array $storesToAdd): void + { + foreach ($products as $product) { + $forceGenerateDefault = false; + foreach ($storesToAdd as $storeId) { + if ($this->needGenerateUrlForStore($product, (int)$storeId)) { + $urls[] = $this->generateUrls($product, (int)$storeId); + } elseif ((int)$product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $forceGenerateDefault = true; + } + } + if ($product->getStoreId() === Store::DEFAULT_STORE_ID + || $this->isProductAssignedToStore($product)) { + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); + $urls[] = $this->productUrlRewriteGenerator->generate($product); + } + if ($forceGenerateDefault && $product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $urls[] = $this->generateUrls($product, Store::DEFAULT_STORE_ID); + } + $this->getDataByStore->clearProductUrlRewriteDataCache($product); + } + if (!empty($urls)) { + $this->urlPersist->replace(array_merge(...$urls)); + } + } + + /** + * Generate urls for specific store + * + * @param ProductInterface $product + * @param int $storeId + * @return array + */ + private function generateUrls(ProductInterface $product, int $storeId): array + { + $storeData = $this->getDataByStore->execute($product, $storeId); + $origStoreId = $product->getStoreId(); + $origVisibility = $product->getVisibility(); + $origUrlKey = $product->getUrlKey(); + $product->setStoreId($storeId); + $product->setVisibility($storeData['visibility'] ?? Visibility::VISIBILITY_NOT_VISIBLE); + $product->setUrlKey($storeData['url_key'] ?? ''); + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); + $urls = $this->productUrlRewriteGenerator->generate($product); + $product->setStoreId($origStoreId); + $product->setVisibility($origVisibility); + $product->setUrlKey($origUrlKey); + + return $urls; + } + + /** + * Does product has scope overridden url key value + * + * @param ProductInterface $product + * @param int $storeId + * @return bool + */ + private function needGenerateUrlForStore(ProductInterface $product, int $storeId): bool + { + return (int)$product->getStoreId() !== $storeId + && $this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + $storeId, + $product->getId(), + Product::ENTITY + ); + } + + /** + * Is product still assigned to store which request is performed from + * + * @param ProductInterface $product + * @return bool + */ + private function isProductAssignedToStore(ProductInterface $product): bool + { + return in_array($product->getStoreId(), $product->getStoreIds()); + } +} diff --git a/vendor/magento/module-catalog-url-rewrite/Model/ResourceModel/Product/GetUrlRewriteData.php b/vendor/magento/module-catalog-url-rewrite/Model/ResourceModel/Product/GetUrlRewriteData.php new file mode 100644 index 00000000000..f4cef73a040 --- /dev/null +++ b/vendor/magento/module-catalog-url-rewrite/Model/ResourceModel/Product/GetUrlRewriteData.php @@ -0,0 +1,94 @@ +metadataPool = $metadataPool; + $this->resource = $connection; + $this->eavConfig = $eavConfig; + } + + /** + * Fetches product store data required for url key generation + * + * @param ProductInterface $product + * @return array + */ + public function execute(ProductInterface $product): array + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + $connection = $this->resource->getConnection(); + $visibilityAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'visibility'); + $urlKeyAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'url_key'); + $visibilitySelect = $connection->select() + ->from(['visibility' => $visibilityAttribute->getBackendTable()]) + ->joinRight( + ['url_key' => $urlKeyAttribute->getBackendTable()], + 'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id' + . ' AND url_key.attribute_id = ' . $urlKeyAttribute->getId(), + ['url_key.value as url_key'] + ) + ->reset(Select::COLUMNS) + ->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value AS visibility']) + ->where('visibility.' . $linkField . ' = ?', $product->getData($linkField)) + ->where('visibility.attribute_id = ?', $visibilityAttribute->getId()); + $urlKeySelect = $connection->select() + ->from(['url_key' => $urlKeyAttribute->getBackendTable()]) + ->joinLeft( + ['visibility' => $visibilityAttribute->getBackendTable()], + 'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id' + . ' AND visibility.attribute_id = ' . $visibilityAttribute->getId(), + ['visibility.value as visibility'] + ) + ->reset(Select::COLUMNS) + ->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value as visibility']) + ->where('url_key.' . $linkField . ' = ?', $product->getData($linkField)) + ->where('url_key.attribute_id = ?', $urlKeyAttribute->getId()); + + $select = $connection->select()->union([$visibilitySelect, $urlKeySelect], Select::SQL_UNION); + + return $connection->fetchAll($select); + } +} diff --git a/vendor/magento/module-catalog-url-rewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/vendor/magento/module-catalog-url-rewrite/Observer/ProductProcessUrlRewriteSavingObserver.php index 6eda8dd0b61..51234035417 100644 --- a/vendor/magento/module-catalog-url-rewrite/Observer/ProductProcessUrlRewriteSavingObserver.php +++ b/vendor/magento/module-catalog-url-rewrite/Observer/ProductProcessUrlRewriteSavingObserver.php @@ -3,73 +3,156 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Product; -use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Framework\App\ObjectManager; -use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** * Class ProductProcessUrlRewriteSavingObserver + * + * Generates urls for product url rewrites */ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface { /** - * @var ProductUrlRewriteGenerator + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var AppendUrlRewritesToProducts */ - private $productUrlRewriteGenerator; + private $appendRewrites; /** - * @var UrlPersistInterface + * @var ScopeConfigInterface */ - private $urlPersist; + private $scopeConfig; /** - * @var ProductUrlPathGenerator + * @var GetStoresListByWebsiteIds */ - private $productUrlPathGenerator; + private $getStoresList; /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist - * @param ProductUrlPathGenerator|null $productUrlPathGenerator + * @param AppendUrlRewritesToProducts|null $appendRewrites + * @param ScopeConfigInterface $scopeConfig + * @param GetStoresListByWebsiteIds $getStoresList */ public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, UrlPersistInterface $urlPersist, - ProductUrlPathGenerator $productUrlPathGenerator = null + AppendUrlRewritesToProducts $appendRewrites, + ScopeConfigInterface $scopeConfig, + GetStoresListByWebsiteIds $getStoresList ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; - $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() - ->get(ProductUrlPathGenerator::class); + $this->appendRewrites = $appendRewrites; + $this->scopeConfig = $scopeConfig; + $this->getStoresList = $getStoresList; } /** * Generate urls for UrlRewrite and save it in storage * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return void - * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException + * @throws UrlAlreadyExistsException */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { /** @var Product $product */ $product = $observer->getEvent()->getProduct(); - if ($product->dataHasChangedFor('url_key') - || $product->getIsChangedCategories() - || $product->getIsChangedWebsites() - || $product->dataHasChangedFor('visibility') - ) { - if ($product->isVisibleInSiteVisibility()) { - $product->unsUrlPath(); - $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } + if ($this->isNeedUpdateRewrites($product)) { + $this->deleteObsoleteRewrites($product); + $oldWebsiteIds = $product->getOrigData('website_ids') ?? []; + $storesToAdd = $this->getStoresList->execute( + array_diff($product->getWebsiteIds(), $oldWebsiteIds) + ); + $this->appendRewrites->execute([$product], $storesToAdd); } } + + /** + * Remove obsolete Url rewrites + * + * @param Product $product + */ + private function deleteObsoleteRewrites(Product $product): void + { + //do not perform redundant delete request for new product + if ($product->getOrigData('entity_id') === null) { + return; + } + $oldWebsiteIds = $product->getOrigData('website_ids') ?? []; + $storesToRemove = $this->getStoresList->execute( + array_diff($oldWebsiteIds, $product->getWebsiteIds()) + ); + if ((int)$product->getVisibility() === Visibility::VISIBILITY_NOT_VISIBLE) { + $isGlobalScope = $product->getStoreId() == Store::DEFAULT_STORE_ID; + $storesToRemove[] = $isGlobalScope ? $product->getStoreIds() : $product->getStoreId(); + } + if ($storesToRemove) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storesToRemove, + ] + ); + } + } + + /** + * Is website assignment updated + * + * @param Product $product + * @return bool + */ + private function isWebsiteChanged(Product $product) + { + $oldWebsiteIds = $product->getOrigData('website_ids'); + $newWebsiteIds = $product->getWebsiteIds(); + + return array_diff($oldWebsiteIds, $newWebsiteIds) || array_diff($newWebsiteIds, $oldWebsiteIds); + } + + + /** + * Is product rewrites need to be updated + * + * @param Product $product + * @return bool + */ + private function isNeedUpdateRewrites(Product $product): bool + { + return ($product->dataHasChangedFor('url_key') + && (int)$product->getVisibility() !== Visibility::VISIBILITY_NOT_VISIBLE) + || ($product->getIsChangedCategories() && $this->isGenerateCategoryProductRewritesEnabled()) + || $this->isWebsiteChanged($product) + || $product->dataHasChangedFor('visibility'); + } + + /** + * Return product use category path in rewrite config value + * + * @return bool + */ + private function isGenerateCategoryProductRewritesEnabled(): bool + { + return $this->scopeConfig->isSetFlag('catalog/seo/generate_category_product_rewrites'); + } } diff --git a/vendor/magento/module-catalog-url-rewrite/Observer/ProductToWebsiteChangeObserver.php b/vendor/magento/module-catalog-url-rewrite/Observer/ProductToWebsiteChangeObserver.php deleted file mode 100644 index 44b47faf3d4..00000000000 --- a/vendor/magento/module-catalog-url-rewrite/Observer/ProductToWebsiteChangeObserver.php +++ /dev/null @@ -1,104 +0,0 @@ -productUrlRewriteGenerator = $productUrlRewriteGenerator; - $this->urlPersist = $urlPersist; - $this->productRepository = $productRepository; - $this->request = $request; - $this->storeWebsiteRelation = $storeWebsiteRelation ?: - ObjectManager::getInstance()->get(StoreWebsiteRelationInterface::class); - } - - /** - * Generate urls for UrlRewrite and save it in storage - * - * @param \Magento\Framework\Event\Observer $observer - * @return void - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - foreach ($observer->getEvent()->getProducts() as $productId) { - $product = $this->productRepository->getById( - $productId, - false, - $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) - ); - - if (!empty($this->productUrlRewriteGenerator->generate($product))) { - if ($this->request->getParam('remove_website_ids')) { - foreach ($this->request->getParam('remove_website_ids') as $webId) { - foreach ($this->storeWebsiteRelation->getStoreByWebsiteId($webId) as $storeId) { - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId - ]); - } - } - } - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } - } - } - } -} diff --git a/vendor/magento/module-catalog-url-rewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php b/vendor/magento/module-catalog-url-rewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php new file mode 100644 index 00000000000..f9c605ab489 --- /dev/null +++ b/vendor/magento/module-catalog-url-rewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php @@ -0,0 +1,97 @@ +urlPersist = $urlPersist; + $this->productCollection = $productCollection; + $this->appendRewrites = $appendRewrites; + $this->getStoresList = $getStoresList; + } + + /** + * Update url rewrites after website changes + * + * @param ProductAction $subject + * @param void $result + * @param array $productIds + * @param array $websiteIds + * @param string $type + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateWebsites( + ProductAction $subject, + $result, + array $productIds, + array $websiteIds, + string $type + ): void { + if (empty($websiteIds)) { + return; + } + $storeIds = $this->getStoresList->execute($websiteIds); + // Remove the URLs from websites this product no longer belongs to + if ($type == 'remove') { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $productIds, + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeIds, + ] + ); + } else { + $collection = $this->productCollection->addFieldToFilter('entity_id', ['in' => implode(',', $productIds)]); + $this->appendRewrites->execute($collection->getItems(), $storeIds); + } + } +} diff --git a/vendor/magento/module-catalog-url-rewrite/etc/adminhtml/events.xml b/vendor/magento/module-catalog-url-rewrite/etc/adminhtml/events.xml deleted file mode 100644 index 9c4a8aaf412..00000000000 --- a/vendor/magento/module-catalog-url-rewrite/etc/adminhtml/events.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/vendor/magento/module-catalog-url-rewrite/etc/di.xml b/vendor/magento/module-catalog-url-rewrite/etc/di.xml index 5fb7d33546d..d22816243f6 100644 --- a/vendor/magento/module-catalog-url-rewrite/etc/di.xml +++ b/vendor/magento/module-catalog-url-rewrite/etc/di.xml @@ -27,6 +27,9 @@ + + + diff --git a/vendor/magento/module-store/Model/StoreResolver/GetStoresListByWebsiteIds.php b/vendor/magento/module-store/Model/StoreResolver/GetStoresListByWebsiteIds.php new file mode 100644 index 00000000000..416537caaf0 --- /dev/null +++ b/vendor/magento/module-store/Model/StoreResolver/GetStoresListByWebsiteIds.php @@ -0,0 +1,45 @@ +storeWebsiteRelation = $storeWebsiteRelation; + } + + /** + * Retrieve list of stores by website ids + * + * @param array $websiteIds + * @return array + */ + public function execute(array $websiteIds): array + { + $storeIdsArray = []; + foreach ($websiteIds as $websiteId) { + $storeIdsArray[] = $this->storeWebsiteRelation->getStoreByWebsiteId($websiteId); + } + + return array_merge([], ...$storeIdsArray); + } +}