AfterImportDataObserver.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\CatalogUrlRewrite\Observer;
  7. use Magento\Catalog\Model\Category;
  8. use Magento\Catalog\Model\Product\Visibility;
  9. use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
  10. use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
  11. use Magento\CatalogImportExport\Model\Import\Product as ImportProduct;
  12. use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
  13. use Magento\Framework\App\ObjectManager;
  14. use Magento\Framework\Event\Observer;
  15. use Magento\Framework\Event\ObserverInterface;
  16. use Magento\ImportExport\Model\Import as ImportExport;
  17. use Magento\Store\Model\Store;
  18. use Magento\UrlRewrite\Model\MergeDataProviderFactory;
  19. use Magento\UrlRewrite\Model\OptionProvider;
  20. use Magento\UrlRewrite\Model\UrlFinderInterface;
  21. use Magento\UrlRewrite\Model\UrlPersistInterface;
  22. use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
  23. use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory;
  24. /**
  25. * Class AfterImportDataObserver
  26. *
  27. * @SuppressWarnings(PHPMD.TooManyFields)
  28. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  29. */
  30. class AfterImportDataObserver implements ObserverInterface
  31. {
  32. /**
  33. * Url Key Attribute
  34. */
  35. const URL_KEY_ATTRIBUTE_CODE = 'url_key';
  36. /**
  37. * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService
  38. */
  39. protected $storeViewService;
  40. /**
  41. * @var \Magento\Catalog\Model\Product
  42. */
  43. protected $product;
  44. /**
  45. * @var array
  46. */
  47. protected $productsWithStores;
  48. /**
  49. * @var array
  50. */
  51. protected $products = [];
  52. /**
  53. * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory
  54. */
  55. protected $objectRegistryFactory;
  56. /**
  57. * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry
  58. */
  59. protected $productCategories;
  60. /**
  61. * @var \Magento\UrlRewrite\Model\UrlFinderInterface
  62. */
  63. protected $urlFinder;
  64. /**
  65. * @var \Magento\Store\Model\StoreManagerInterface
  66. */
  67. protected $storeManager;
  68. /**
  69. * @var \Magento\UrlRewrite\Model\UrlPersistInterface
  70. */
  71. protected $urlPersist;
  72. /**
  73. * @var \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory
  74. */
  75. protected $urlRewriteFactory;
  76. /**
  77. * @var \Magento\CatalogImportExport\Model\Import\Product
  78. */
  79. protected $import;
  80. /**
  81. * @var \Magento\Catalog\Model\ProductFactory
  82. */
  83. protected $catalogProductFactory;
  84. /**
  85. * @var array
  86. */
  87. protected $acceptableCategories;
  88. /**
  89. * @var \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator
  90. */
  91. protected $productUrlPathGenerator;
  92. /**
  93. * @var array
  94. */
  95. protected $websitesToStoreIds;
  96. /**
  97. * @var array
  98. */
  99. protected $storesCache = [];
  100. /**
  101. * @var array
  102. */
  103. protected $categoryCache = [];
  104. /**
  105. * @var array
  106. */
  107. protected $websiteCache = [];
  108. /**
  109. * @var array
  110. */
  111. protected $vitalForGenerationFields = [
  112. 'sku',
  113. 'url_key',
  114. 'url_path',
  115. 'name',
  116. 'visibility',
  117. 'save_rewrites_history'
  118. ];
  119. /**
  120. * @var \Magento\UrlRewrite\Model\MergeDataProvider
  121. */
  122. private $mergeDataProviderPrototype;
  123. /**
  124. * Factory for creating category collection.
  125. *
  126. * @var CategoryCollectionFactory
  127. */
  128. private $categoryCollectionFactory;
  129. /**
  130. * Array of invoked categories during url rewrites generation.
  131. *
  132. * @var array
  133. */
  134. private $categoriesCache = [];
  135. /**
  136. * @param \Magento\Catalog\Model\ProductFactory $catalogProductFactory
  137. * @param \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory
  138. * @param \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator $productUrlPathGenerator
  139. * @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService
  140. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  141. * @param UrlPersistInterface $urlPersist
  142. * @param UrlRewriteFactory $urlRewriteFactory
  143. * @param UrlFinderInterface $urlFinder
  144. * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory
  145. * @param CategoryCollectionFactory|null $categoryCollectionFactory
  146. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  147. */
  148. public function __construct(
  149. \Magento\Catalog\Model\ProductFactory $catalogProductFactory,
  150. \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory,
  151. \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator $productUrlPathGenerator,
  152. \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService,
  153. \Magento\Store\Model\StoreManagerInterface $storeManager,
  154. UrlPersistInterface $urlPersist,
  155. UrlRewriteFactory $urlRewriteFactory,
  156. UrlFinderInterface $urlFinder,
  157. MergeDataProviderFactory $mergeDataProviderFactory = null,
  158. CategoryCollectionFactory $categoryCollectionFactory = null
  159. ) {
  160. $this->urlPersist = $urlPersist;
  161. $this->catalogProductFactory = $catalogProductFactory;
  162. $this->objectRegistryFactory = $objectRegistryFactory;
  163. $this->productUrlPathGenerator = $productUrlPathGenerator;
  164. $this->storeViewService = $storeViewService;
  165. $this->storeManager = $storeManager;
  166. $this->urlRewriteFactory = $urlRewriteFactory;
  167. $this->urlFinder = $urlFinder;
  168. if (!isset($mergeDataProviderFactory)) {
  169. $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class);
  170. }
  171. $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create();
  172. $this->categoryCollectionFactory = $categoryCollectionFactory ?:
  173. ObjectManager::getInstance()->get(CategoryCollectionFactory::class);
  174. }
  175. /**
  176. * Action after data import.
  177. *
  178. * Save new url rewrites and remove old if exist.
  179. *
  180. * @param Observer $observer
  181. *
  182. * @return void
  183. */
  184. public function execute(Observer $observer)
  185. {
  186. $this->import = $observer->getEvent()->getAdapter();
  187. if ($products = $observer->getEvent()->getBunch()) {
  188. foreach ($products as $product) {
  189. $this->_populateForUrlGeneration($product);
  190. }
  191. $productUrls = $this->generateUrls();
  192. if ($productUrls) {
  193. $this->urlPersist->replace($productUrls);
  194. }
  195. }
  196. }
  197. /**
  198. * Create product model from imported data for URL rewrite purposes.
  199. *
  200. * @param array $rowData
  201. *
  202. * @return ImportExport
  203. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  204. */
  205. protected function _populateForUrlGeneration($rowData)
  206. {
  207. $newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]);
  208. if (empty($newSku) || !isset($newSku['entity_id'])) {
  209. return null;
  210. }
  211. if ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE
  212. && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) {
  213. return null;
  214. }
  215. $rowData['entity_id'] = $newSku['entity_id'];
  216. $product = $this->catalogProductFactory->create();
  217. $product->setId($rowData['entity_id']);
  218. foreach ($this->vitalForGenerationFields as $field) {
  219. if (isset($rowData[$field])) {
  220. $product->setData($field, $rowData[$field]);
  221. }
  222. }
  223. $this->categoryCache[$rowData['entity_id']] = $this->import->getProductCategories($rowData['sku']);
  224. $this->websiteCache[$rowData['entity_id']] = $this->import->getProductWebsites($rowData['sku']);
  225. foreach ($this->websiteCache[$rowData['entity_id']] as $websiteId) {
  226. if (!isset($this->websitesToStoreIds[$websiteId])) {
  227. $this->websitesToStoreIds[$websiteId] = $this->storeManager->getWebsite($websiteId)->getStoreIds();
  228. }
  229. }
  230. $this->setStoreToProduct($product, $rowData);
  231. if ($this->isGlobalScope($product->getStoreId())) {
  232. $this->populateGlobalProduct($product);
  233. } else {
  234. $this->addProductToImport($product, $product->getStoreId());
  235. }
  236. return $this;
  237. }
  238. /**
  239. * Add store id to product data.
  240. *
  241. * @param \Magento\Catalog\Model\Product $product
  242. * @param array $rowData
  243. * @return void
  244. */
  245. protected function setStoreToProduct(\Magento\Catalog\Model\Product $product, array $rowData)
  246. {
  247. if (!empty($rowData[ImportProduct::COL_STORE])
  248. && ($storeId = $this->import->getStoreIdByCode($rowData[ImportProduct::COL_STORE]))
  249. ) {
  250. $product->setStoreId($storeId);
  251. } elseif (!$product->hasData(\Magento\Catalog\Model\Product::STORE_ID)) {
  252. $product->setStoreId(Store::DEFAULT_STORE_ID);
  253. }
  254. }
  255. /**
  256. * Add product to import
  257. *
  258. * @param \Magento\Catalog\Model\Product $product
  259. * @param string $storeId
  260. * @return $this
  261. */
  262. protected function addProductToImport($product, $storeId)
  263. {
  264. if ($product->getVisibility() == (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]) {
  265. return $this;
  266. }
  267. if (!isset($this->products[$product->getId()])) {
  268. $this->products[$product->getId()] = [];
  269. }
  270. $this->products[$product->getId()][$storeId] = $product;
  271. return $this;
  272. }
  273. /**
  274. * Populate global product
  275. *
  276. * @param \Magento\Catalog\Model\Product $product
  277. * @return $this
  278. */
  279. protected function populateGlobalProduct($product)
  280. {
  281. foreach ($this->import->getProductWebsites($product->getSku()) as $websiteId) {
  282. foreach ($this->websitesToStoreIds[$websiteId] as $storeId) {
  283. $this->storesCache[$storeId] = true;
  284. if (!$this->isGlobalScope($storeId)) {
  285. $this->addProductToImport($product, $storeId);
  286. }
  287. }
  288. }
  289. return $this;
  290. }
  291. /**
  292. * Generate product url rewrites
  293. *
  294. * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[]
  295. */
  296. protected function generateUrls()
  297. {
  298. $mergeDataProvider = clone $this->mergeDataProviderPrototype;
  299. $mergeDataProvider->merge($this->canonicalUrlRewriteGenerate());
  300. $mergeDataProvider->merge($this->categoriesUrlRewriteGenerate());
  301. $mergeDataProvider->merge($this->currentUrlRewritesRegenerate());
  302. $this->productCategories = null;
  303. unset($this->products);
  304. $this->products = [];
  305. return $mergeDataProvider->getData();
  306. }
  307. /**
  308. * Check is global scope
  309. *
  310. * @param int|null $storeId
  311. * @return bool
  312. */
  313. protected function isGlobalScope($storeId)
  314. {
  315. return null === $storeId || $storeId == Store::DEFAULT_STORE_ID;
  316. }
  317. /**
  318. * Generate list based on store view
  319. *
  320. * @return UrlRewrite[]
  321. */
  322. protected function canonicalUrlRewriteGenerate()
  323. {
  324. $urls = [];
  325. foreach ($this->products as $productId => $productsByStores) {
  326. foreach ($productsByStores as $storeId => $product) {
  327. if ($this->productUrlPathGenerator->getUrlPath($product)) {
  328. $urls[] = $this->urlRewriteFactory->create()
  329. ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
  330. ->setEntityId($productId)
  331. ->setRequestPath($this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId))
  332. ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product))
  333. ->setStoreId($storeId);
  334. }
  335. }
  336. }
  337. return $urls;
  338. }
  339. /**
  340. * Generate list based on categories.
  341. *
  342. * @return UrlRewrite[]
  343. */
  344. protected function categoriesUrlRewriteGenerate()
  345. {
  346. $urls = [];
  347. foreach ($this->products as $productId => $productsByStores) {
  348. foreach ($productsByStores as $storeId => $product) {
  349. foreach ($this->categoryCache[$productId] as $categoryId) {
  350. $category = $this->getCategoryById($categoryId, $storeId);
  351. if ($category->getParentId() == Category::TREE_ROOT_ID) {
  352. continue;
  353. }
  354. $requestPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category);
  355. $urls[] = $this->urlRewriteFactory->create()
  356. ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
  357. ->setEntityId($productId)
  358. ->setRequestPath($requestPath)
  359. ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category))
  360. ->setStoreId($storeId)
  361. ->setMetadata(['category_id' => $category->getId()]);
  362. }
  363. }
  364. }
  365. return $urls;
  366. }
  367. /**
  368. * Generate list based on current rewrites
  369. *
  370. * @return UrlRewrite[]
  371. */
  372. protected function currentUrlRewritesRegenerate()
  373. {
  374. $currentUrlRewrites = $this->urlFinder->findAllByData(
  375. [
  376. UrlRewrite::STORE_ID => array_keys($this->storesCache),
  377. UrlRewrite::ENTITY_ID => array_keys($this->products),
  378. UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE,
  379. ]
  380. );
  381. $urlRewrites = [];
  382. foreach ($currentUrlRewrites as $currentUrlRewrite) {
  383. $category = $this->retrieveCategoryFromMetadata($currentUrlRewrite);
  384. if ($category === false) {
  385. continue;
  386. }
  387. $url = $currentUrlRewrite->getIsAutogenerated()
  388. ? $this->generateForAutogenerated($currentUrlRewrite, $category)
  389. : $this->generateForCustom($currentUrlRewrite, $category);
  390. $urlRewrites = array_merge($urlRewrites, $url);
  391. }
  392. $this->product = null;
  393. $this->productCategories = null;
  394. return $urlRewrites;
  395. }
  396. /**
  397. * Generate url-rewrite for outogenerated url-rewirte.
  398. *
  399. * @param UrlRewrite $url
  400. * @param Category $category
  401. * @return array
  402. */
  403. protected function generateForAutogenerated($url, $category)
  404. {
  405. $storeId = $url->getStoreId();
  406. $productId = $url->getEntityId();
  407. if (isset($this->products[$productId][$storeId])) {
  408. $product = $this->products[$productId][$storeId];
  409. if (!$product->getData('save_rewrites_history')) {
  410. return [];
  411. }
  412. $targetPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category);
  413. if ($url->getRequestPath() === $targetPath) {
  414. return [];
  415. }
  416. return [
  417. $this->urlRewriteFactory->create()
  418. ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
  419. ->setEntityId($productId)
  420. ->setRequestPath($url->getRequestPath())
  421. ->setTargetPath($targetPath)
  422. ->setRedirectType(OptionProvider::PERMANENT)
  423. ->setStoreId($storeId)
  424. ->setDescription($url->getDescription())
  425. ->setIsAutogenerated(0)
  426. ->setMetadata($url->getMetadata())
  427. ];
  428. }
  429. return [];
  430. }
  431. /**
  432. * Generate url-rewrite for custom url-rewirte.
  433. *
  434. * @param UrlRewrite $url
  435. * @param Category $category
  436. * @return array
  437. */
  438. protected function generateForCustom($url, $category)
  439. {
  440. $storeId = $url->getStoreId();
  441. $productId = $url->getEntityId();
  442. if (isset($this->products[$productId][$storeId])) {
  443. $product = $this->products[$productId][$storeId];
  444. $targetPath = $url->getRedirectType()
  445. ? $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category)
  446. : $url->getTargetPath();
  447. if ($url->getRequestPath() === $targetPath) {
  448. return [];
  449. }
  450. return [
  451. $this->urlRewriteFactory->create()
  452. ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
  453. ->setEntityId($productId)
  454. ->setRequestPath($url->getRequestPath())
  455. ->setTargetPath($targetPath)
  456. ->setRedirectType($url->getRedirectType())
  457. ->setStoreId($storeId)
  458. ->setDescription($url->getDescription())
  459. ->setIsAutogenerated(0)
  460. ->setMetadata($url->getMetadata())
  461. ];
  462. }
  463. return [];
  464. }
  465. /**
  466. * Retrieve category from url metadata.
  467. *
  468. * @param UrlRewrite $url
  469. * @return Category|null|bool
  470. */
  471. protected function retrieveCategoryFromMetadata($url)
  472. {
  473. $metadata = $url->getMetadata();
  474. if (isset($metadata['category_id'])) {
  475. $category = $this->import->getCategoryProcessor()->getCategoryById($metadata['category_id']);
  476. return $category === null ? false : $category;
  477. }
  478. return null;
  479. }
  480. /**
  481. * Check, category suited for url-rewrite generation.
  482. *
  483. * @param \Magento\Catalog\Model\Category $category
  484. * @param int $storeId
  485. * @return bool
  486. */
  487. protected function isCategoryProperForGenerating($category, $storeId)
  488. {
  489. if (isset($this->acceptableCategories[$storeId]) &&
  490. isset($this->acceptableCategories[$storeId][$category->getId()])) {
  491. return $this->acceptableCategories[$storeId][$category->getId()];
  492. }
  493. $acceptable = false;
  494. if ($category->getParentId() != \Magento\Catalog\Model\Category::TREE_ROOT_ID) {
  495. list(, $rootCategoryId) = $category->getParentIds();
  496. $acceptable = ($rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId());
  497. }
  498. if (!isset($this->acceptableCategories[$storeId])) {
  499. $this->acceptableCategories[$storeId] = [];
  500. }
  501. $this->acceptableCategories[$storeId][$category->getId()] = $acceptable;
  502. return $acceptable;
  503. }
  504. /**
  505. * Get category by id considering store scope.
  506. *
  507. * @param int $categoryId
  508. * @param int $storeId
  509. * @return Category|\Magento\Framework\DataObject
  510. */
  511. private function getCategoryById($categoryId, $storeId)
  512. {
  513. if (!isset($this->categoriesCache[$categoryId][$storeId])) {
  514. /** @var CategoryCollection $categoryCollection */
  515. $categoryCollection = $this->categoryCollectionFactory->create();
  516. $categoryCollection->addIdFilter([$categoryId])
  517. ->setStoreId($storeId)
  518. ->addAttributeToSelect('name')
  519. ->addAttributeToSelect('url_key')
  520. ->addAttributeToSelect('url_path');
  521. $this->categoriesCache[$categoryId][$storeId] = $categoryCollection->getFirstItem();
  522. }
  523. return $this->categoriesCache[$categoryId][$storeId];
  524. }
  525. }