Review.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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\Model\AbstractModel;
  8. /**
  9. * Review resource model
  10. *
  11. * @api
  12. * @since 100.0.2
  13. */
  14. class Review extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
  15. {
  16. /**
  17. * Review table
  18. *
  19. * @var string
  20. */
  21. protected $_reviewTable;
  22. /**
  23. * Review Detail table
  24. *
  25. * @var string
  26. */
  27. protected $_reviewDetailTable;
  28. /**
  29. * Review status table
  30. *
  31. * @var string
  32. */
  33. protected $_reviewStatusTable;
  34. /**
  35. * Review entity table
  36. *
  37. * @var string
  38. */
  39. protected $_reviewEntityTable;
  40. /**
  41. * Review store table
  42. *
  43. * @var string
  44. */
  45. protected $_reviewStoreTable;
  46. /**
  47. * Review aggregate table
  48. *
  49. * @var string
  50. */
  51. protected $_aggregateTable;
  52. /**
  53. * Cache of deleted rating data
  54. *
  55. * @var array
  56. */
  57. private $_deleteCache = [];
  58. /**
  59. * Core date model
  60. *
  61. * @var \Magento\Framework\Stdlib\DateTime\DateTime
  62. */
  63. protected $_date;
  64. /**
  65. * Core model store manager interface
  66. *
  67. * @var \Magento\Store\Model\StoreManagerInterface
  68. */
  69. protected $_storeManager;
  70. /**
  71. * Rating model
  72. *
  73. * @var \Magento\Review\Model\RatingFactory
  74. */
  75. protected $_ratingFactory;
  76. /**
  77. * Rating resource model
  78. *
  79. * @var \Magento\Review\Model\ResourceModel\Rating\Option
  80. */
  81. protected $_ratingOptions;
  82. /**
  83. * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
  84. * @param \Magento\Framework\Stdlib\DateTime\DateTime $date
  85. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  86. * @param \Magento\Review\Model\RatingFactory $ratingFactory
  87. * @param \Magento\Review\Model\ResourceModel\Rating\Option $ratingOptions
  88. * @param string $connectionName
  89. */
  90. public function __construct(
  91. \Magento\Framework\Model\ResourceModel\Db\Context $context,
  92. \Magento\Framework\Stdlib\DateTime\DateTime $date,
  93. \Magento\Store\Model\StoreManagerInterface $storeManager,
  94. \Magento\Review\Model\RatingFactory $ratingFactory,
  95. Rating\Option $ratingOptions,
  96. $connectionName = null
  97. ) {
  98. $this->_date = $date;
  99. $this->_storeManager = $storeManager;
  100. $this->_ratingFactory = $ratingFactory;
  101. $this->_ratingOptions = $ratingOptions;
  102. parent::__construct($context, $connectionName);
  103. }
  104. /**
  105. * Define main table. Define other tables name
  106. *
  107. * @return void
  108. */
  109. protected function _construct()
  110. {
  111. $this->_init('review', 'review_id');
  112. $this->_reviewTable = $this->getTable('review');
  113. $this->_reviewDetailTable = $this->getTable('review_detail');
  114. $this->_reviewStatusTable = $this->getTable('review_status');
  115. $this->_reviewEntityTable = $this->getTable('review_entity');
  116. $this->_reviewStoreTable = $this->getTable('review_store');
  117. $this->_aggregateTable = $this->getTable('review_entity_summary');
  118. }
  119. /**
  120. * Retrieve select object for load object data
  121. *
  122. * @param string $field
  123. * @param mixed $value
  124. * @param AbstractModel $object
  125. * @return \Magento\Framework\DB\Select
  126. */
  127. protected function _getLoadSelect($field, $value, $object)
  128. {
  129. $select = parent::_getLoadSelect($field, $value, $object);
  130. $select->join(
  131. $this->_reviewDetailTable,
  132. $this->getMainTable() . ".review_id = {$this->_reviewDetailTable}.review_id"
  133. );
  134. return $select;
  135. }
  136. /**
  137. * Perform actions before object save
  138. *
  139. * @param AbstractModel $object
  140. * @return $this
  141. */
  142. protected function _beforeSave(AbstractModel $object)
  143. {
  144. if (!$object->getId()) {
  145. $object->setCreatedAt($this->_date->gmtDate());
  146. }
  147. if ($object->hasData('stores') && is_array($object->getStores())) {
  148. $stores = $object->getStores();
  149. $stores[] = 0;
  150. $object->setStores($stores);
  151. } elseif ($object->hasData('stores')) {
  152. $object->setStores([$object->getStores(), 0]);
  153. }
  154. return $this;
  155. }
  156. /**
  157. * Perform actions after object save
  158. *
  159. * @param \Magento\Framework\Model\AbstractModel $object
  160. * @return $this
  161. */
  162. protected function _afterSave(AbstractModel $object)
  163. {
  164. $connection = $this->getConnection();
  165. /**
  166. * save detail
  167. */
  168. $detail = [
  169. 'title' => $object->getTitle(),
  170. 'detail' => $object->getDetail(),
  171. 'nickname' => $object->getNickname(),
  172. ];
  173. $select = $connection->select()->from($this->_reviewDetailTable, 'detail_id')->where('review_id = :review_id');
  174. $detailId = $connection->fetchOne($select, [':review_id' => $object->getId()]);
  175. if ($detailId) {
  176. $condition = ["detail_id = ?" => $detailId];
  177. $connection->update($this->_reviewDetailTable, $detail, $condition);
  178. } else {
  179. $detail['store_id'] = $object->getStoreId();
  180. $detail['customer_id'] = $object->getCustomerId();
  181. $detail['review_id'] = $object->getId();
  182. $connection->insert($this->_reviewDetailTable, $detail);
  183. }
  184. /**
  185. * save stores
  186. */
  187. $stores = $object->getStores();
  188. if (!empty($stores)) {
  189. $condition = ['review_id = ?' => $object->getId()];
  190. $connection->delete($this->_reviewStoreTable, $condition);
  191. $insertedStoreIds = [];
  192. foreach ($stores as $storeId) {
  193. if (in_array($storeId, $insertedStoreIds)) {
  194. continue;
  195. }
  196. $insertedStoreIds[] = $storeId;
  197. $storeInsert = ['store_id' => $storeId, 'review_id' => $object->getId()];
  198. $connection->insert($this->_reviewStoreTable, $storeInsert);
  199. }
  200. }
  201. // reaggregate ratings, that depend on this review
  202. $this->_aggregateRatings($this->_loadVotedRatingIds($object->getId()), $object->getEntityPkValue());
  203. return $this;
  204. }
  205. /**
  206. * Perform actions after object load
  207. *
  208. * @param \Magento\Framework\Model\AbstractModel $object
  209. * @return $this
  210. */
  211. protected function _afterLoad(AbstractModel $object)
  212. {
  213. $connection = $this->getConnection();
  214. $select = $connection->select()->from(
  215. $this->_reviewStoreTable,
  216. ['store_id']
  217. )->where(
  218. 'review_id = :review_id'
  219. );
  220. $stores = $connection->fetchCol($select, [':review_id' => $object->getId()]);
  221. if (empty($stores) && $this->_storeManager->hasSingleStore()) {
  222. $object->setStores([$this->_storeManager->getStore(true)->getId()]);
  223. } else {
  224. $object->setStores($stores);
  225. }
  226. return $this;
  227. }
  228. /**
  229. * Action before delete
  230. *
  231. * @param \Magento\Framework\Model\AbstractModel $object
  232. * @return $this
  233. */
  234. protected function _beforeDelete(AbstractModel $object)
  235. {
  236. // prepare rating ids, that depend on review
  237. $this->_deleteCache = [
  238. 'ratingIds' => $this->_loadVotedRatingIds($object->getId()),
  239. 'entityPkValue' => $object->getEntityPkValue(),
  240. ];
  241. return $this;
  242. }
  243. /**
  244. * Perform actions after object delete
  245. *
  246. * @param \Magento\Framework\Model\AbstractModel $object
  247. * @return $this
  248. */
  249. public function afterDeleteCommit(AbstractModel $object)
  250. {
  251. $this->aggregate($object);
  252. // reaggregate ratings, that depended on this review
  253. $this->_aggregateRatings($this->_deleteCache['ratingIds'], $this->_deleteCache['entityPkValue']);
  254. $this->_deleteCache = [];
  255. return $this;
  256. }
  257. /**
  258. * Retrieves total reviews
  259. *
  260. * @param int $entityPkValue
  261. * @param bool $approvedOnly
  262. * @param int $storeId
  263. * @return int
  264. */
  265. public function getTotalReviews($entityPkValue, $approvedOnly = false, $storeId = 0)
  266. {
  267. $connection = $this->getConnection();
  268. $select = $connection->select()->from(
  269. $this->_reviewTable,
  270. ['review_count' => new \Zend_Db_Expr('COUNT(*)')]
  271. )->where(
  272. "{$this->_reviewTable}.entity_pk_value = :pk_value"
  273. );
  274. $bind = [':pk_value' => $entityPkValue];
  275. if ($storeId > 0) {
  276. $select->join(
  277. ['store' => $this->_reviewStoreTable],
  278. $this->_reviewTable . '.review_id=store.review_id AND store.store_id = :store_id',
  279. []
  280. );
  281. $bind[':store_id'] = (int) $storeId;
  282. }
  283. if ($approvedOnly) {
  284. $select->where("{$this->_reviewTable}.status_id = :status_id");
  285. $bind[':status_id'] = \Magento\Review\Model\Review::STATUS_APPROVED;
  286. }
  287. return $connection->fetchOne($select, $bind);
  288. }
  289. /**
  290. * Aggregate
  291. *
  292. * @param \Magento\Framework\Model\AbstractModel $object
  293. * @return void
  294. */
  295. public function aggregate($object)
  296. {
  297. if (!$object->getEntityPkValue() && $object->getId()) {
  298. $object->load($object->getReviewId());
  299. }
  300. $ratingModel = $this->_ratingFactory->create();
  301. $ratingSummaries = $ratingModel->getEntitySummary($object->getEntityPkValue(), false);
  302. foreach ($ratingSummaries as $ratingSummaryObject) {
  303. $this->aggregateReviewSummary($object, $ratingSummaryObject);
  304. }
  305. }
  306. /**
  307. * Aggregate review summary
  308. *
  309. * @param \Magento\Framework\Model\AbstractModel $object
  310. * @param \Magento\Review\Model\Rating $ratingSummaryObject
  311. * @return void
  312. */
  313. protected function aggregateReviewSummary($object, $ratingSummaryObject)
  314. {
  315. $connection = $this->getConnection();
  316. if ($ratingSummaryObject->getCount()) {
  317. $ratingSummary = round($ratingSummaryObject->getSum() / $ratingSummaryObject->getCount());
  318. } else {
  319. $ratingSummary = $ratingSummaryObject->getSum();
  320. }
  321. $reviewsCount = $this->getTotalReviews(
  322. $object->getEntityPkValue(),
  323. true,
  324. $ratingSummaryObject->getStoreId()
  325. );
  326. $select = $connection->select()->from($this->_aggregateTable)
  327. ->where('entity_pk_value = :pk_value')
  328. ->where('entity_type = :entity_type')
  329. ->where('store_id = :store_id');
  330. $bind = [
  331. ':pk_value' => $object->getEntityPkValue(),
  332. ':entity_type' => $object->getEntityId(),
  333. ':store_id' => $ratingSummaryObject->getStoreId(),
  334. ];
  335. $oldData = $connection->fetchRow($select, $bind);
  336. $data = new \Magento\Framework\DataObject();
  337. $data->setReviewsCount($reviewsCount)
  338. ->setEntityPkValue($object->getEntityPkValue())
  339. ->setEntityType($object->getEntityId())
  340. ->setRatingSummary($ratingSummary > 0 ? $ratingSummary : 0)
  341. ->setStoreId($ratingSummaryObject->getStoreId());
  342. $this->writeReviewSummary($oldData, $data);
  343. }
  344. /**
  345. * Write rating summary
  346. *
  347. * @param array|bool $oldData
  348. * @param \Magento\Framework\DataObject $data
  349. * @return void
  350. */
  351. protected function writeReviewSummary($oldData, \Magento\Framework\DataObject $data)
  352. {
  353. $connection = $this->getConnection();
  354. $connection->beginTransaction();
  355. try {
  356. if (isset($oldData['primary_id']) && $oldData['primary_id'] > 0) {
  357. $condition = ["{$this->_aggregateTable}.primary_id = ?" => $oldData['primary_id']];
  358. $connection->update($this->_aggregateTable, $data->getData(), $condition);
  359. } else {
  360. $connection->insert($this->_aggregateTable, $data->getData());
  361. }
  362. $connection->commit();
  363. } catch (\Exception $e) {
  364. $connection->rollBack();
  365. }
  366. }
  367. /**
  368. * Get rating IDs from review votes
  369. *
  370. * @param int $reviewId
  371. * @return array
  372. */
  373. protected function _loadVotedRatingIds($reviewId)
  374. {
  375. $connection = $this->getConnection();
  376. if (empty($reviewId)) {
  377. return [];
  378. }
  379. $select = $connection->select()->from(['v' => $this->getTable('rating_option_vote')], 'r.rating_id')
  380. ->joinInner(['r' => $this->getTable('rating')], 'v.rating_id=r.rating_id')
  381. ->where('v.review_id = :revire_id');
  382. return $connection->fetchCol($select, [':revire_id' => $reviewId]);
  383. }
  384. /**
  385. * Aggregate this review's ratings.
  386. * Useful, when changing the review.
  387. *
  388. * @param int[] $ratingIds
  389. * @param int $entityPkValue
  390. * @return $this
  391. */
  392. protected function _aggregateRatings($ratingIds, $entityPkValue)
  393. {
  394. if ($ratingIds && !is_array($ratingIds)) {
  395. $ratingIds = [(int)$ratingIds];
  396. }
  397. if ($ratingIds && $entityPkValue) {
  398. foreach ($ratingIds as $ratingId) {
  399. $this->_ratingOptions->aggregateEntityByRatingId($ratingId, $entityPkValue);
  400. }
  401. }
  402. return $this;
  403. }
  404. /**
  405. * Reaggregate this review's ratings.
  406. *
  407. * @param int $reviewId
  408. * @param int $entityPkValue
  409. * @return void
  410. */
  411. public function reAggregateReview($reviewId, $entityPkValue)
  412. {
  413. $this->_aggregateRatings($this->_loadVotedRatingIds($reviewId), $entityPkValue);
  414. }
  415. /**
  416. * Get review entity type id by code
  417. *
  418. * @param string $entityCode
  419. * @return int|bool
  420. */
  421. public function getEntityIdByCode($entityCode)
  422. {
  423. $connection = $this->getConnection();
  424. $select = $connection->select()->from($this->_reviewEntityTable, ['entity_id'])
  425. ->where('entity_code = :entity_code');
  426. return $connection->fetchOne($select, [':entity_code' => $entityCode]);
  427. }
  428. /**
  429. * Delete reviews by product id.
  430. * Better to call this method in transaction, because operation performed on two separated tables
  431. *
  432. * @param int $productId
  433. * @return $this
  434. */
  435. public function deleteReviewsByProductId($productId)
  436. {
  437. $this->getConnection()->delete(
  438. $this->_reviewTable,
  439. [
  440. 'entity_pk_value=?' => $productId,
  441. 'entity_id=?' => $this->getEntityIdByCode(\Magento\Review\Model\Review::ENTITY_PRODUCT_CODE)
  442. ]
  443. );
  444. $this->getConnection()->delete(
  445. $this->getTable('review_entity_summary'),
  446. [
  447. 'entity_pk_value=?' => $productId,
  448. 'entity_type=?' => $this->getEntityIdByCode(\Magento\Review\Model\Review::ENTITY_PRODUCT_CODE)
  449. ]
  450. );
  451. return $this;
  452. }
  453. }