Rating.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Review\Model\ResourceModel;
  7. use Magento\Framework\App\Config\ScopeConfigInterface;
  8. use Magento\Framework\App\ObjectManager;
  9. /**
  10. * Rating resource model
  11. *
  12. * @api
  13. *
  14. * @author Magento Core Team <core@magentocommerce.com>
  15. * @since 100.0.2
  16. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  17. */
  18. class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
  19. {
  20. const RATING_STATUS_APPROVED = 'Approved';
  21. /**
  22. * Store manager
  23. *
  24. * @var \Magento\Store\Model\StoreManagerInterface
  25. */
  26. protected $_storeManager;
  27. /**
  28. * @var \Magento\Framework\Module\Manager
  29. */
  30. protected $moduleManager;
  31. /**
  32. * @var \Psr\Log\LoggerInterface
  33. */
  34. protected $_logger;
  35. /**
  36. * @var ScopeConfigInterface
  37. */
  38. private $scopeConfig;
  39. /**
  40. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
  41. * @param \Psr\Log\LoggerInterface $logger
  42. * @param \Magento\Framework\Module\Manager $moduleManager
  43. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  44. * @param Review\Summary $reviewSummary
  45. * @param string $connectionName
  46. * @param ScopeConfigInterface|null $scopeConfig
  47. */
  48. public function __construct(
  49. \Magento\Framework\Model\ResourceModel\Db\Context $context,
  50. \Psr\Log\LoggerInterface $logger,
  51. \Magento\Framework\Module\Manager $moduleManager,
  52. \Magento\Store\Model\StoreManagerInterface $storeManager,
  53. \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary,
  54. $connectionName = null,
  55. ScopeConfigInterface $scopeConfig = null
  56. ) {
  57. $this->moduleManager = $moduleManager;
  58. $this->_storeManager = $storeManager;
  59. $this->_logger = $logger;
  60. $this->_reviewSummary = $reviewSummary;
  61. $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class);
  62. parent::__construct($context, $connectionName);
  63. }
  64. /**
  65. * Resource initialization
  66. *
  67. * @return void
  68. */
  69. protected function _construct()
  70. {
  71. $this->_init('rating', 'rating_id');
  72. }
  73. /**
  74. * Initialize unique fields
  75. *
  76. * @return $this
  77. */
  78. protected function _initUniqueFields()
  79. {
  80. $this->_uniqueFields = [['field' => 'rating_code', 'title' => '']];
  81. return $this;
  82. }
  83. /**
  84. * Retrieve select object for load object data
  85. *
  86. * @param string $field
  87. * @param mixed $value
  88. * @param \Magento\Review\Model\Rating $object
  89. * @return \Magento\Framework\DB\Select
  90. */
  91. protected function _getLoadSelect($field, $value, $object)
  92. {
  93. $connection = $this->getConnection();
  94. $table = $this->getMainTable();
  95. $storeId = (int)$this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId();
  96. $select = parent::_getLoadSelect($field, $value, $object);
  97. $codeExpr = $connection->getIfNullSql('title.value', "{$table}.rating_code");
  98. $select->joinLeft(
  99. ['title' => $this->getTable('rating_title')],
  100. $connection->quoteInto("{$table}.rating_id = title.rating_id AND title.store_id = ?", $storeId),
  101. ['rating_code' => $codeExpr]
  102. );
  103. return $select;
  104. }
  105. /**
  106. * Actions after load
  107. *
  108. * @param \Magento\Framework\Model\AbstractModel|\Magento\Review\Model\Rating $object
  109. * @return $this
  110. */
  111. protected function _afterLoad(\Magento\Framework\Model\AbstractModel $object)
  112. {
  113. parent::_afterLoad($object);
  114. if (!$object->getId()) {
  115. return $this;
  116. }
  117. $connection = $this->getConnection();
  118. $bind = [':rating_id' => (int)$object->getId()];
  119. // load rating titles
  120. $select = $connection->select()->from(
  121. $this->getTable('rating_title'),
  122. ['store_id', 'value']
  123. )->where(
  124. 'rating_id=:rating_id'
  125. );
  126. $result = $connection->fetchPairs($select, $bind);
  127. if ($result) {
  128. $object->setRatingCodes($result);
  129. }
  130. // load rating available in stores
  131. $object->setStores($this->getStores((int)$object->getId()));
  132. return $this;
  133. }
  134. /**
  135. * Retrieve store IDs related to given rating
  136. *
  137. * @param int $ratingId
  138. * @return array
  139. */
  140. public function getStores($ratingId)
  141. {
  142. $select = $this->getConnection()->select()->from(
  143. $this->getTable('rating_store'),
  144. 'store_id'
  145. )->where(
  146. 'rating_id = ?',
  147. $ratingId
  148. );
  149. return $this->getConnection()->fetchCol($select);
  150. }
  151. /**
  152. * Actions after save
  153. *
  154. * @param \Magento\Framework\Model\AbstractModel|\Magento\Review\Model\Rating $object
  155. * @return $this
  156. */
  157. protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
  158. {
  159. parent::_afterSave($object);
  160. if ($object->hasRatingCodes()) {
  161. $this->processRatingCodes($object);
  162. }
  163. if ($object->hasStores()) {
  164. $this->processRatingStores($object);
  165. }
  166. return $this;
  167. }
  168. /**
  169. * Process rating codes
  170. *
  171. * @param \Magento\Framework\Model\AbstractModel $object
  172. * @return $this
  173. */
  174. protected function processRatingCodes(\Magento\Framework\Model\AbstractModel $object)
  175. {
  176. $connection = $this->getConnection();
  177. $ratingId = (int)$object->getId();
  178. $table = $this->getTable('rating_title');
  179. $select = $connection->select()->from($table, ['store_id', 'value'])
  180. ->where('rating_id = :rating_id');
  181. $old = $connection->fetchPairs($select, [':rating_id' => $ratingId]);
  182. $new = array_filter(array_map('trim', $object->getRatingCodes()));
  183. $this->deleteRatingData($ratingId, $table, array_keys(array_diff_assoc($old, $new)));
  184. $insert = [];
  185. foreach (array_diff_assoc($new, $old) as $storeId => $title) {
  186. $insert[] = ['rating_id' => $ratingId, 'store_id' => (int)$storeId, 'value' => $title];
  187. }
  188. $this->insertRatingData($table, $insert);
  189. return $this;
  190. }
  191. /**
  192. * Process rating stores
  193. *
  194. * @param \Magento\Framework\Model\AbstractModel $object
  195. * @return $this
  196. */
  197. protected function processRatingStores(\Magento\Framework\Model\AbstractModel $object)
  198. {
  199. $connection = $this->getConnection();
  200. $ratingId = (int)$object->getId();
  201. $table = $this->getTable('rating_store');
  202. $select = $connection->select()->from($table, ['store_id'])
  203. ->where('rating_id = :rating_id');
  204. $old = $connection->fetchCol($select, [':rating_id' => $ratingId]);
  205. $new = $object->getStores();
  206. $this->deleteRatingData($ratingId, $table, array_diff($old, $new));
  207. $insert = [];
  208. foreach (array_diff($new, $old) as $storeId) {
  209. $insert[] = ['rating_id' => $ratingId, 'store_id' => (int)$storeId];
  210. }
  211. $this->insertRatingData($table, $insert);
  212. return $this;
  213. }
  214. /**
  215. * Delete rating data
  216. *
  217. * @param int $ratingId
  218. * @param string $table
  219. * @param array $storeIds
  220. * @return void
  221. */
  222. protected function deleteRatingData($ratingId, $table, array $storeIds)
  223. {
  224. if (empty($storeIds)) {
  225. return;
  226. }
  227. $connection = $this->getConnection();
  228. $connection->beginTransaction();
  229. try {
  230. $where = ['rating_id = ?' => $ratingId, 'store_id IN(?)' => $storeIds];
  231. $connection->delete($table, $where);
  232. $connection->commit();
  233. } catch (\Exception $e) {
  234. $this->_logger->critical($e);
  235. $connection->rollBack();
  236. }
  237. }
  238. /**
  239. * Insert rating data
  240. *
  241. * @param string $table
  242. * @param array $data
  243. * @return void
  244. */
  245. protected function insertRatingData($table, array $data)
  246. {
  247. if (empty($data)) {
  248. return;
  249. }
  250. $connection = $this->getConnection();
  251. $connection->beginTransaction();
  252. try {
  253. $connection->insertMultiple($table, $data);
  254. $connection->commit();
  255. } catch (\Exception $e) {
  256. $this->_logger->critical($e);
  257. $connection->rollBack();
  258. }
  259. }
  260. /**
  261. * Perform actions after object delete
  262. *
  263. * Prepare rating data for reaggregate all data for reviews
  264. *
  265. * @param \Magento\Framework\Model\AbstractModel $object
  266. * @return $this
  267. */
  268. protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object)
  269. {
  270. parent::_afterDelete($object);
  271. if (!$this->moduleManager->isEnabled('Magento_Review') &&
  272. !$this->scopeConfig->getValue(
  273. \Magento\Review\Observer\PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE,
  274. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  275. )
  276. ) {
  277. return $this;
  278. }
  279. $data = $this->_getEntitySummaryData($object);
  280. $summary = [];
  281. foreach ($data as $row) {
  282. $clone = clone $object;
  283. $clone->addData($row);
  284. $summary[$clone->getStoreId()][$clone->getEntityPkValue()] = $clone;
  285. }
  286. $this->_reviewSummary->reAggregate($summary);
  287. return $this;
  288. }
  289. /**
  290. * Return array of rating summary
  291. *
  292. * @param \Magento\Review\Model\Rating $object
  293. * @param boolean $onlyForCurrentStore
  294. * @return array
  295. */
  296. public function getEntitySummary($object, $onlyForCurrentStore = true)
  297. {
  298. $data = $this->_getEntitySummaryData($object);
  299. if ($onlyForCurrentStore) {
  300. foreach ($data as $row) {
  301. if ($row['store_id'] == $this->_storeManager->getStore()->getId()) {
  302. $object->addData($row);
  303. }
  304. }
  305. return $object;
  306. }
  307. $stores = $this->_storeManager->getStores();
  308. $result = [];
  309. foreach ($data as $row) {
  310. $clone = clone $object;
  311. $clone->addData($row);
  312. $result[$clone->getStoreId()] = $clone;
  313. }
  314. $usedStoresId = array_keys($result);
  315. foreach ($stores as $store) {
  316. if (!in_array($store->getId(), $usedStoresId)) {
  317. $clone = clone $object;
  318. $clone->setCount(0);
  319. $clone->setSum(0);
  320. $clone->setStoreId($store->getId());
  321. $result[$store->getId()] = $clone;
  322. }
  323. }
  324. return array_values($result);
  325. }
  326. /**
  327. * Return data of rating summary
  328. *
  329. * @param \Magento\Review\Model\Rating $object
  330. * @return array
  331. */
  332. protected function _getEntitySummaryData($object)
  333. {
  334. $connection = $this->getConnection();
  335. $sumColumn = new \Zend_Db_Expr("SUM(rating_vote.{$connection->quoteIdentifier('percent')})");
  336. $countColumn = new \Zend_Db_Expr("COUNT(*)");
  337. $select = $connection->select()->from(
  338. ['rating_vote' => $this->getTable('rating_option_vote')],
  339. ['entity_pk_value' => 'rating_vote.entity_pk_value', 'sum' => $sumColumn, 'count' => $countColumn]
  340. )->join(
  341. ['review' => $this->getTable('review')],
  342. 'rating_vote.review_id=review.review_id',
  343. []
  344. )->joinLeft(
  345. ['review_store' => $this->getTable('review_store')],
  346. 'rating_vote.review_id=review_store.review_id',
  347. ['review_store.store_id']
  348. );
  349. if (!$this->_storeManager->isSingleStoreMode()) {
  350. $select->join(
  351. ['rating_store' => $this->getTable('rating_store')],
  352. 'rating_store.rating_id = rating_vote.rating_id AND rating_store.store_id = review_store.store_id',
  353. []
  354. );
  355. }
  356. $select->join(
  357. ['review_status' => $this->getTable('review_status')],
  358. 'review.status_id = review_status.status_id',
  359. []
  360. )->where(
  361. 'review_status.status_code = :status_code'
  362. )->group(
  363. 'rating_vote.entity_pk_value'
  364. )->group(
  365. 'review_store.store_id'
  366. );
  367. $bind = [':status_code' => self::RATING_STATUS_APPROVED];
  368. $entityPkValue = $object->getEntityPkValue();
  369. if ($entityPkValue) {
  370. $select->where('rating_vote.entity_pk_value = :pk_value');
  371. $bind[':pk_value'] = $entityPkValue;
  372. }
  373. return $connection->fetchAll($select, $bind);
  374. }
  375. /**
  376. * Review summary
  377. *
  378. * @param \Magento\Review\Model\Rating $object
  379. * @param boolean $onlyForCurrentStore
  380. * @return array
  381. */
  382. public function getReviewSummary($object, $onlyForCurrentStore = true)
  383. {
  384. $connection = $this->getConnection();
  385. $sumColumn = new \Zend_Db_Expr("SUM(rating_vote.{$connection->quoteIdentifier('percent')})");
  386. $countColumn = new \Zend_Db_Expr('COUNT(*)');
  387. $select = $connection->select()->from(
  388. ['rating_vote' => $this->getTable('rating_option_vote')],
  389. ['sum' => $sumColumn, 'count' => $countColumn]
  390. )->joinLeft(
  391. ['review_store' => $this->getTable('review_store')],
  392. 'rating_vote.review_id = review_store.review_id',
  393. ['review_store.store_id']
  394. );
  395. if (!$this->_storeManager->isSingleStoreMode()) {
  396. $select->join(
  397. ['rating_store' => $this->getTable('rating_store')],
  398. 'rating_store.rating_id = rating_vote.rating_id AND rating_store.store_id = review_store.store_id',
  399. []
  400. );
  401. }
  402. $select->where(
  403. 'rating_vote.review_id = :review_id'
  404. )->group(
  405. 'rating_vote.review_id'
  406. )->group(
  407. 'review_store.store_id'
  408. );
  409. $data = $connection->fetchAll($select, [':review_id' => $object->getReviewId()]);
  410. $currentStore = $this->_storeManager->isSingleStoreMode() ? $this->_storeManager->getStore()->getId() : null;
  411. if ($onlyForCurrentStore) {
  412. foreach ($data as $row) {
  413. if ($row['store_id'] !== $currentStore) {
  414. $object->addData($row);
  415. }
  416. }
  417. return $object;
  418. }
  419. $result = [];
  420. $stores = $this->_storeManager->getStore()->getResourceCollection()->load();
  421. foreach ($data as $row) {
  422. $clone = clone $object;
  423. $clone->addData($row);
  424. $result[$clone->getStoreId()] = $clone;
  425. }
  426. $usedStoresId = array_keys($result);
  427. foreach ($stores as $store) {
  428. if (!in_array($store->getId(), $usedStoresId)) {
  429. $clone = clone $object;
  430. $clone->setCount(0);
  431. $clone->setSum(0);
  432. $clone->setStoreId($store->getId());
  433. $result[$store->getId()] = $clone;
  434. }
  435. }
  436. return array_values($result);
  437. }
  438. /**
  439. * Get rating entity type id by code
  440. *
  441. * @param string $entityCode
  442. * @return int
  443. */
  444. public function getEntityIdByCode($entityCode)
  445. {
  446. $select = $this->getConnection()->select()->from(
  447. $this->getTable('rating_entity'),
  448. ['entity_id']
  449. )->where(
  450. 'entity_code = :entity_code'
  451. );
  452. return $this->getConnection()->fetchOne($select, [':entity_code' => $entityCode]);
  453. }
  454. /**
  455. * Delete ratings by product id
  456. *
  457. * @param int $productId
  458. * @return $this
  459. */
  460. public function deleteAggregatedRatingsByProductId($productId)
  461. {
  462. $entityId = $this->getEntityIdByCode(\Magento\Review\Model\Rating::ENTITY_PRODUCT_CODE);
  463. $connection = $this->getConnection();
  464. $select = $connection->select()->from($this->getMainTable(), 'rating_id')->where('entity_id = :entity_id');
  465. $ratingIds = $connection->fetchCol($select, [':entity_id' => $entityId]);
  466. if ($ratingIds) {
  467. $where = ['entity_pk_value = ?' => (int)$productId, 'rating_id IN(?)' => $ratingIds];
  468. $connection->delete($this->getTable('rating_option_vote_aggregated'), $where);
  469. }
  470. return $this;
  471. }
  472. }