WebsiteAttributesSynchronizer.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Catalog\Model\ResourceModel\Attribute;
  7. use Magento\Catalog\Api\Data\CategoryInterface;
  8. use Magento\Catalog\Api\Data\ProductInterface;
  9. use Magento\Framework\App\ResourceConnection;
  10. use Magento\Framework\DB\Adapter\AdapterInterface;
  11. use Magento\Framework\DB\Query\Generator;
  12. use Magento\Framework\EntityManager\MetadataPool;
  13. use Magento\Framework\Exception\LocalizedException;
  14. use Magento\Framework\FlagManager;
  15. /**
  16. * Class WebsiteAttributesSynchronizer
  17. * @package Magento\Catalog\Cron
  18. */
  19. class WebsiteAttributesSynchronizer
  20. {
  21. const FLAG_SYNCHRONIZED = 0;
  22. const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1;
  23. const FLAG_REQUIRES_SYNCHRONIZATION = 2;
  24. const FLAG_NAME = 'catalog_website_attribute_is_sync_required';
  25. const ATTRIBUTE_WEBSITE = 2;
  26. const GLOBAL_STORE_VIEW_ID = 0;
  27. const MASK_ATTRIBUTE_VALUE = '%d_%d_%d';
  28. /**
  29. * Map table names to metadata classes where link field might be found
  30. */
  31. private $tableMetaDataClass = [
  32. 'catalog_category_entity_datetime' => CategoryInterface::class,
  33. 'catalog_category_entity_decimal' => CategoryInterface::class,
  34. 'catalog_category_entity_int' => CategoryInterface::class,
  35. 'catalog_category_entity_text' => CategoryInterface::class,
  36. 'catalog_category_entity_varchar' => CategoryInterface::class,
  37. 'catalog_product_entity_datetime' => ProductInterface::class,
  38. 'catalog_product_entity_decimal' => ProductInterface::class,
  39. 'catalog_product_entity_int' => ProductInterface::class,
  40. 'catalog_product_entity_text' => ProductInterface::class,
  41. 'catalog_product_entity_varchar' => ProductInterface::class,
  42. ];
  43. /**
  44. * Internal format :
  45. * [
  46. * website_id => [
  47. * store_view_id_1,
  48. * store_view_id_2,
  49. * ...
  50. * ]
  51. * ]
  52. *
  53. * @var array
  54. */
  55. private $groupedStoreViews = [];
  56. /**
  57. * @var array
  58. */
  59. private $processedAttributeValues = [];
  60. /**
  61. * @var ResourceConnection
  62. */
  63. private $resourceConnection;
  64. /**
  65. * @var AdapterInterface
  66. */
  67. private $connection;
  68. /**
  69. * @var FlagManager
  70. */
  71. private $flagManager;
  72. /**
  73. * @var Generator
  74. */
  75. private $batchQueryGenerator;
  76. /**
  77. * @var MetadataPool
  78. */
  79. private $metaDataPool;
  80. /**
  81. * @var array
  82. */
  83. private $linkFields = [];
  84. /**
  85. * WebsiteAttributesSynchronizer constructor.
  86. * @param ResourceConnection $resourceConnection
  87. * @param FlagManager $flagManager
  88. * @param Generator $batchQueryGenerator,
  89. * @param MetadataPool $metadataPool
  90. */
  91. public function __construct(
  92. ResourceConnection $resourceConnection,
  93. FlagManager $flagManager,
  94. Generator $batchQueryGenerator,
  95. MetadataPool $metadataPool
  96. ) {
  97. $this->resourceConnection = $resourceConnection;
  98. $this->connection = $this->resourceConnection->getConnection();
  99. $this->flagManager = $flagManager;
  100. $this->batchQueryGenerator = $batchQueryGenerator;
  101. $this->metaDataPool = $metadataPool;
  102. }
  103. /**
  104. * Synchronizes attribute values between different store views on website level
  105. * @return void
  106. * @throws \Exception
  107. */
  108. public function synchronize()
  109. {
  110. $this->markSynchronizationInProgress();
  111. $this->connection->beginTransaction();
  112. try {
  113. foreach (array_keys($this->tableMetaDataClass) as $tableName) {
  114. $this->synchronizeTable($tableName);
  115. }
  116. $this->markSynchronized();
  117. $this->connection->commit();
  118. } catch (\Exception $exception) {
  119. $this->connection->rollBack();
  120. $this->scheduleSynchronization();
  121. throw $exception;
  122. }
  123. }
  124. /**
  125. * @return bool
  126. */
  127. public function isSynchronizationRequired()
  128. {
  129. return self::FLAG_REQUIRES_SYNCHRONIZATION === $this->flagManager->getFlagData(self::FLAG_NAME);
  130. }
  131. /**
  132. * Puts a flag that synchronization is required
  133. * @return void
  134. */
  135. public function scheduleSynchronization()
  136. {
  137. $this->flagManager->saveFlag(self::FLAG_NAME, self::FLAG_REQUIRES_SYNCHRONIZATION);
  138. }
  139. /**
  140. * Marks flag as in progress in case if several crons enabled, so sync. won't be duplicated
  141. * @return void
  142. */
  143. private function markSynchronizationInProgress()
  144. {
  145. $this->flagManager->saveFlag(self::FLAG_NAME, self::FLAG_SYNCHRONIZATION_IN_PROGRESS);
  146. }
  147. /**
  148. * Turn off synchronization flag
  149. * @return void
  150. */
  151. private function markSynchronized()
  152. {
  153. $this->flagManager->saveFlag(self::FLAG_NAME, self::FLAG_SYNCHRONIZED);
  154. }
  155. /**
  156. * @param string $tableName
  157. * @return void
  158. */
  159. private function synchronizeTable($tableName)
  160. {
  161. foreach ($this->fetchAttributeValues($tableName) as $attributeValueItems) {
  162. $this->processAttributeValues($attributeValueItems, $tableName);
  163. }
  164. }
  165. /**
  166. * Aligns website attribute values
  167. * @param array $attributeValueItems
  168. * @param string $tableName
  169. * @return void
  170. */
  171. private function processAttributeValues(array $attributeValueItems, $tableName)
  172. {
  173. $this->resetProcessedAttributeValues();
  174. foreach ($attributeValueItems as $attributeValueItem) {
  175. if ($this->isAttributeValueProcessed($attributeValueItem, $tableName)) {
  176. continue;
  177. }
  178. $insertions = $this->generateAttributeValueInsertions($attributeValueItem, $tableName);
  179. if (!empty($insertions)) {
  180. $this->executeInsertions($insertions, $tableName);
  181. }
  182. $this->markAttributeValueProcessed($attributeValueItem, $tableName);
  183. }
  184. }
  185. /**
  186. * Yields batch of AttributeValues
  187. *
  188. * @param string $tableName
  189. * @yield array
  190. * @return void
  191. */
  192. private function fetchAttributeValues($tableName)
  193. {
  194. $batchSelectIterator = $this->batchQueryGenerator->generate(
  195. 'value_id',
  196. $this->connection
  197. ->select()
  198. ->from(
  199. ['cpei' => $this->resourceConnection->getTableName($tableName)],
  200. '*'
  201. )
  202. ->join(
  203. [
  204. 'cea' => $this->resourceConnection->getTableName('catalog_eav_attribute'),
  205. ],
  206. 'cpei.attribute_id = cea.attribute_id',
  207. ''
  208. )
  209. ->join(
  210. [
  211. 'st' => $this->resourceConnection->getTableName('store'),
  212. ],
  213. 'st.store_id = cpei.store_id',
  214. 'st.website_id'
  215. )
  216. ->where(
  217. 'cea.is_global = ?',
  218. self::ATTRIBUTE_WEBSITE
  219. )
  220. ->where(
  221. 'cpei.store_id <> ?',
  222. self::GLOBAL_STORE_VIEW_ID
  223. )
  224. );
  225. foreach ($batchSelectIterator as $select) {
  226. yield $this->connection->fetchAll($select);
  227. }
  228. }
  229. /**
  230. * @return array
  231. */
  232. private function getGroupedStoreViews()
  233. {
  234. if (!empty($this->groupedStoreViews)) {
  235. return $this->groupedStoreViews;
  236. }
  237. $query = $this->connection
  238. ->select()
  239. ->from(
  240. $this->resourceConnection->getTableName('store'),
  241. '*'
  242. );
  243. $storeViews = $this->connection->fetchAll($query);
  244. $this->groupedStoreViews = [];
  245. foreach ($storeViews as $storeView) {
  246. if ($storeView['store_id'] != 0) {
  247. $this->groupedStoreViews[$storeView['website_id']][] = $storeView['store_id'];
  248. }
  249. }
  250. return $this->groupedStoreViews;
  251. }
  252. /**
  253. * @param array $attributeValue
  254. * @param string $tableName
  255. * @return bool
  256. */
  257. private function isAttributeValueProcessed(array $attributeValue, $tableName)
  258. {
  259. return in_array(
  260. $this->getAttributeValueKey(
  261. $attributeValue[$this->getTableLinkField($tableName)],
  262. $attributeValue['attribute_id'],
  263. $attributeValue['website_id']
  264. ),
  265. $this->processedAttributeValues
  266. );
  267. }
  268. /**
  269. * Resets processed attribute values
  270. * @return void
  271. */
  272. private function resetProcessedAttributeValues()
  273. {
  274. $this->processedAttributeValues = [];
  275. }
  276. /**
  277. * @param array $attributeValue
  278. * @param string $tableName
  279. * @return void
  280. */
  281. private function markAttributeValueProcessed(array $attributeValue, $tableName)
  282. {
  283. $this->processedAttributeValues[] = $this->getAttributeValueKey(
  284. $attributeValue[$this->getTableLinkField($tableName)],
  285. $attributeValue['attribute_id'],
  286. $attributeValue['website_id']
  287. );
  288. }
  289. /**
  290. * @param int $entityId
  291. * @param int $attributeId
  292. * @param int $websiteId
  293. * @return string
  294. */
  295. private function getAttributeValueKey($entityId, $attributeId, $websiteId)
  296. {
  297. return sprintf(
  298. self::MASK_ATTRIBUTE_VALUE,
  299. $entityId,
  300. $attributeId,
  301. $websiteId
  302. );
  303. }
  304. /**
  305. * @param array $attributeValue
  306. * @param string $tableName
  307. * @return array|null
  308. */
  309. private function generateAttributeValueInsertions(array $attributeValue, $tableName)
  310. {
  311. $groupedStoreViews = $this->getGroupedStoreViews();
  312. if (empty($groupedStoreViews[$attributeValue['website_id']])) {
  313. return null;
  314. }
  315. $currentStoreViewIds = $groupedStoreViews[$attributeValue['website_id']];
  316. $insertions = [];
  317. foreach ($currentStoreViewIds as $index => $storeViewId) {
  318. $insertions[] = [
  319. ':attribute_id' . $index => $attributeValue['attribute_id'],
  320. ':store_id' . $index => $storeViewId,
  321. ':entity_id' . $index => $attributeValue[$this->getTableLinkField($tableName)],
  322. ':value' . $index => $attributeValue['value'],
  323. ];
  324. }
  325. return $insertions;
  326. }
  327. /**
  328. * @param array $insertions
  329. * @param string $tableName
  330. * @return void
  331. */
  332. private function executeInsertions(array $insertions, $tableName)
  333. {
  334. $rawQuery = sprintf(
  335. 'INSERT INTO
  336. %s(attribute_id, store_id, %s, `value`)
  337. VALUES
  338. %s
  339. ON duplicate KEY UPDATE `value` = VALUES(`value`)',
  340. $this->resourceConnection->getTableName($tableName),
  341. $this->getTableLinkField($tableName),
  342. $this->prepareInsertValuesStatement($insertions)
  343. );
  344. $this->connection->query($rawQuery, $this->getPlaceholderValues($insertions));
  345. }
  346. /**
  347. * Maps $insertions hierarchy to single-level $placeholder => $value array
  348. *
  349. * @param array $insertions
  350. * @return array
  351. */
  352. private function getPlaceholderValues(array $insertions)
  353. {
  354. $placeholderValues = [];
  355. foreach ($insertions as $insertion) {
  356. $placeholderValues = array_merge(
  357. $placeholderValues,
  358. $insertion
  359. );
  360. }
  361. return $placeholderValues;
  362. }
  363. /**
  364. * Extracts from $insertions values placeholders and turns it into query statement view
  365. *
  366. * @param array $insertions
  367. * @return string
  368. */
  369. private function prepareInsertValuesStatement(array $insertions)
  370. {
  371. $statement = '';
  372. foreach ($insertions as $insertion) {
  373. $statement .= sprintf('(%s),', implode(',', array_keys($insertion)));
  374. }
  375. return rtrim($statement, ',');
  376. }
  377. /**
  378. * @param string $tableName
  379. * @return string
  380. * @throws LocalizedException
  381. */
  382. private function getTableLinkField($tableName)
  383. {
  384. if (!isset($this->tableMetaDataClass[$tableName])) {
  385. throw new LocalizedException(
  386. sprintf(
  387. 'Specified table: %s is not defined in tables list',
  388. $tableName
  389. )
  390. );
  391. }
  392. if (!isset($this->linkFields[$tableName])) {
  393. $this->linkFields[$tableName] = $this->metaDataPool
  394. ->getMetadata($this->tableMetaDataClass[$tableName])
  395. ->getLinkField();
  396. }
  397. return $this->linkFields[$tableName];
  398. }
  399. }