UrlRewriteHandler.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\CatalogUrlRewrite\Observer;
  8. use Magento\Catalog\Model\Product;
  9. use Magento\Catalog\Model\Category;
  10. use Magento\Catalog\Model\ResourceModel\Product\Collection;
  11. use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
  12. use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider;
  13. use Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator;
  14. use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator;
  15. use Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator;
  16. use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
  17. use Magento\Framework\App\ObjectManager;
  18. use Magento\Framework\Serialize\Serializer\Json;
  19. use Magento\UrlRewrite\Model\MergeDataProvider;
  20. use Magento\UrlRewrite\Model\MergeDataProviderFactory;
  21. use Magento\UrlRewrite\Model\UrlPersistInterface;
  22. use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
  23. /**
  24. * Class for management url rewrites.
  25. *
  26. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  27. */
  28. class UrlRewriteHandler
  29. {
  30. /**
  31. * @var ChildrenCategoriesProvider
  32. */
  33. protected $childrenCategoriesProvider;
  34. /**
  35. * @var CategoryUrlRewriteGenerator
  36. */
  37. protected $categoryUrlRewriteGenerator;
  38. /**
  39. * @var ProductUrlRewriteGenerator
  40. */
  41. protected $productUrlRewriteGenerator;
  42. /**
  43. * @var UrlPersistInterface
  44. */
  45. protected $urlPersist;
  46. /**
  47. * @var array
  48. */
  49. protected $isSkippedProduct;
  50. /**
  51. * @var CollectionFactory
  52. */
  53. protected $productCollectionFactory;
  54. /**
  55. * @var CategoryProductUrlPathGenerator
  56. */
  57. private $categoryBasedProductRewriteGenerator;
  58. /**
  59. * @var MergeDataProvider
  60. */
  61. private $mergeDataProviderPrototype;
  62. /**
  63. * @var Json
  64. */
  65. private $serializer;
  66. /**
  67. * @var ProductScopeRewriteGenerator
  68. */
  69. private $productScopeRewriteGenerator;
  70. /**
  71. * @param ChildrenCategoriesProvider $childrenCategoriesProvider
  72. * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator
  73. * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator
  74. * @param UrlPersistInterface $urlPersist
  75. * @param CollectionFactory $productCollectionFactory
  76. * @param CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator
  77. * @param MergeDataProviderFactory|null $mergeDataProviderFactory
  78. * @param Json|null $serializer
  79. * @param ProductScopeRewriteGenerator|null $productScopeRewriteGenerator
  80. */
  81. public function __construct(
  82. ChildrenCategoriesProvider $childrenCategoriesProvider,
  83. CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator,
  84. ProductUrlRewriteGenerator $productUrlRewriteGenerator,
  85. UrlPersistInterface $urlPersist,
  86. CollectionFactory $productCollectionFactory,
  87. CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator,
  88. MergeDataProviderFactory $mergeDataProviderFactory = null,
  89. Json $serializer = null,
  90. ProductScopeRewriteGenerator $productScopeRewriteGenerator = null
  91. ) {
  92. $this->childrenCategoriesProvider = $childrenCategoriesProvider;
  93. $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
  94. $this->productUrlRewriteGenerator = $productUrlRewriteGenerator;
  95. $this->urlPersist = $urlPersist;
  96. $this->productCollectionFactory = $productCollectionFactory;
  97. $this->categoryBasedProductRewriteGenerator = $categoryBasedProductRewriteGenerator;
  98. $objectManager = ObjectManager::getInstance();
  99. $mergeDataProviderFactory = $mergeDataProviderFactory ?: $objectManager->get(MergeDataProviderFactory::class);
  100. $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create();
  101. $this->serializer = $serializer ?: $objectManager->get(Json::class);
  102. $this->productScopeRewriteGenerator = $productScopeRewriteGenerator
  103. ?: $objectManager->get(ProductScopeRewriteGenerator::class);
  104. }
  105. /**
  106. * Generates URL rewrites for products assigned to category.
  107. *
  108. * @param Category $category
  109. * @return array
  110. */
  111. public function generateProductUrlRewrites(Category $category): array
  112. {
  113. $mergeDataProvider = clone $this->mergeDataProviderPrototype;
  114. $this->isSkippedProduct[$category->getEntityId()] = [];
  115. $saveRewriteHistory = (bool)$category->getData('save_rewrites_history');
  116. $storeId = (int)$category->getStoreId();
  117. if ($category->getChangedProductIds()) {
  118. $this->generateChangedProductUrls($mergeDataProvider, $category, $storeId, $saveRewriteHistory);
  119. } else {
  120. $mergeDataProvider->merge(
  121. $this->getCategoryProductsUrlRewrites(
  122. $category,
  123. $storeId,
  124. $saveRewriteHistory,
  125. $category->getEntityId()
  126. )
  127. );
  128. }
  129. foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) {
  130. $mergeDataProvider->merge(
  131. $this->getCategoryProductsUrlRewrites(
  132. $childCategory,
  133. $storeId,
  134. $saveRewriteHistory,
  135. $category->getEntityId()
  136. )
  137. );
  138. }
  139. return $mergeDataProvider->getData();
  140. }
  141. /**
  142. * Update product url rewrites for changed product.
  143. *
  144. * @param Category $category
  145. * @return array
  146. */
  147. public function updateProductUrlRewritesForChangedProduct(Category $category): array
  148. {
  149. $mergeDataProvider = clone $this->mergeDataProviderPrototype;
  150. $this->isSkippedProduct[$category->getEntityId()] = [];
  151. $saveRewriteHistory = (bool)$category->getData('save_rewrites_history');
  152. $storeIds = $this->getCategoryStoreIds($category);
  153. if ($category->getChangedProductIds()) {
  154. foreach ($storeIds as $storeId) {
  155. $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory);
  156. }
  157. }
  158. return $mergeDataProvider->getData();
  159. }
  160. /**
  161. * Delete category rewrites for children.
  162. *
  163. * @param Category $category
  164. * @return void
  165. */
  166. public function deleteCategoryRewritesForChildren(Category $category)
  167. {
  168. $categoryIds = $this->childrenCategoriesProvider->getChildrenIds($category, true);
  169. $categoryIds[] = $category->getId();
  170. foreach ($categoryIds as $categoryId) {
  171. $this->urlPersist->deleteByData(
  172. [
  173. UrlRewrite::ENTITY_ID =>
  174. $categoryId,
  175. UrlRewrite::ENTITY_TYPE =>
  176. CategoryUrlRewriteGenerator::ENTITY_TYPE,
  177. ]
  178. );
  179. $this->urlPersist->deleteByData(
  180. [
  181. UrlRewrite::METADATA =>
  182. $this->serializer->serialize(['category_id' => $categoryId]),
  183. UrlRewrite::ENTITY_TYPE =>
  184. ProductUrlRewriteGenerator::ENTITY_TYPE,
  185. ]
  186. );
  187. }
  188. }
  189. /**
  190. * Get category products url rewrites.
  191. *
  192. * @param Category $category
  193. * @param int $storeId
  194. * @param bool $saveRewriteHistory
  195. * @param int|null $rootCategoryId
  196. * @return array
  197. */
  198. private function getCategoryProductsUrlRewrites(
  199. Category $category,
  200. $storeId,
  201. $saveRewriteHistory,
  202. $rootCategoryId = null
  203. ) {
  204. $mergeDataProvider = clone $this->mergeDataProviderPrototype;
  205. /** @var Collection $productCollection */
  206. $productCollection = $this->productCollectionFactory->create();
  207. $productCollection->addCategoriesFilter(['eq' => [$category->getEntityId()]])
  208. ->setStoreId($storeId)
  209. ->addAttributeToSelect('name')
  210. ->addAttributeToSelect('visibility')
  211. ->addAttributeToSelect('url_key')
  212. ->addAttributeToSelect('url_path');
  213. foreach ($productCollection as $product) {
  214. if (isset($this->isSkippedProduct[$category->getEntityId()]) &&
  215. in_array($product->getId(), $this->isSkippedProduct[$category->getEntityId()])
  216. ) {
  217. continue;
  218. }
  219. $this->isSkippedProduct[$category->getEntityId()][] = $product->getId();
  220. $product->setStoreId($storeId);
  221. $product->setData('save_rewrites_history', $saveRewriteHistory);
  222. $mergeDataProvider->merge(
  223. $this->categoryBasedProductRewriteGenerator->generate($product, $rootCategoryId)
  224. );
  225. }
  226. return $mergeDataProvider->getData();
  227. }
  228. /**
  229. * Generates product URL rewrites.
  230. *
  231. * @param MergeDataProvider $mergeDataProvider
  232. * @param Category $category
  233. * @param int $storeId
  234. * @param bool $saveRewriteHistory
  235. * @return void
  236. */
  237. private function generateChangedProductUrls(
  238. MergeDataProvider $mergeDataProvider,
  239. Category $category,
  240. int $storeId,
  241. bool $saveRewriteHistory
  242. ) {
  243. $this->isSkippedProduct[$category->getEntityId()] = $category->getAffectedProductIds();
  244. $categoryStoreIds = [$storeId];
  245. // If category is changed in the Global scope when need to regenerate product URL rewrites for all other scopes.
  246. if ($this->productScopeRewriteGenerator->isGlobalScope($storeId)) {
  247. $categoryStoreIds = $this->getCategoryStoreIds($category);
  248. }
  249. foreach ($categoryStoreIds as $categoryStoreId) {
  250. /* @var Collection $collection */
  251. $collection = $this->productCollectionFactory->create()
  252. ->setStoreId($categoryStoreId)
  253. ->addIdFilter($category->getAffectedProductIds())
  254. ->addAttributeToSelect('visibility')
  255. ->addAttributeToSelect('name')
  256. ->addAttributeToSelect('url_key')
  257. ->addAttributeToSelect('url_path');
  258. $collection->setPageSize(1000);
  259. $pageCount = $collection->getLastPageNumber();
  260. $currentPage = 1;
  261. while ($currentPage <= $pageCount) {
  262. $collection->setCurPage($currentPage);
  263. foreach ($collection as $product) {
  264. $product->setData('save_rewrites_history', $saveRewriteHistory);
  265. $product->setStoreId($categoryStoreId);
  266. $mergeDataProvider->merge(
  267. $this->productUrlRewriteGenerator->generate($product, $category->getEntityId())
  268. );
  269. }
  270. $collection->clear();
  271. $currentPage++;
  272. }
  273. }
  274. }
  275. /**
  276. * Gets category store IDs without Global Store.
  277. *
  278. * @param Category $category
  279. * @return array
  280. */
  281. private function getCategoryStoreIds(Category $category): array
  282. {
  283. $ids = $category->getStoreIds();
  284. return array_filter($ids);
  285. }
  286. }