CommentStatistics.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. <?php
  2. namespace Drupal\comment;
  3. use Drupal\Core\Database\Connection;
  4. use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
  5. use Drupal\Core\Entity\EntityTypeManagerInterface;
  6. use Drupal\Core\Entity\FieldableEntityInterface;
  7. use Drupal\Core\Entity\EntityChangedInterface;
  8. use Drupal\Core\Entity\EntityInterface;
  9. use Drupal\Core\State\StateInterface;
  10. use Drupal\Core\Session\AccountInterface;
  11. use Drupal\user\EntityOwnerInterface;
  12. class CommentStatistics implements CommentStatisticsInterface {
  13. use DeprecatedServicePropertyTrait;
  14. /**
  15. * {@inheritdoc}
  16. */
  17. protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
  18. /**
  19. * The current database connection.
  20. *
  21. * @var \Drupal\Core\Database\Connection
  22. */
  23. protected $database;
  24. /**
  25. * The replica database connection.
  26. *
  27. * @var \Drupal\Core\Database\Connection
  28. */
  29. protected $databaseReplica;
  30. /**
  31. * The current logged in user.
  32. *
  33. * @var \Drupal\Core\Session\AccountInterface
  34. */
  35. protected $currentUser;
  36. /**
  37. * The entity type manager.
  38. *
  39. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  40. */
  41. protected $entityTypeManager;
  42. /**
  43. * The state service.
  44. *
  45. * @var \Drupal\Core\State\StateInterface
  46. */
  47. protected $state;
  48. /**
  49. * Constructs the CommentStatistics service.
  50. *
  51. * @param \Drupal\Core\Database\Connection $database
  52. * The active database connection.
  53. * @param \Drupal\Core\Session\AccountInterface $current_user
  54. * The current logged in user.
  55. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  56. * The entity type manager.
  57. * @param \Drupal\Core\State\StateInterface $state
  58. * The state service.
  59. * @param \Drupal\Core\Database\Connection|null $database_replica
  60. * (Optional) the replica database connection.
  61. */
  62. public function __construct(Connection $database, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, StateInterface $state, Connection $database_replica = NULL) {
  63. $this->database = $database;
  64. $this->databaseReplica = $database_replica ?: $database;
  65. $this->currentUser = $current_user;
  66. $this->entityTypeManager = $entity_type_manager;
  67. $this->state = $state;
  68. }
  69. /**
  70. * {@inheritdoc}
  71. */
  72. public function read($entities, $entity_type, $accurate = TRUE) {
  73. $connection = $accurate ? $this->database : $this->databaseReplica;
  74. $stats = $connection->select('comment_entity_statistics', 'ces')
  75. ->fields('ces')
  76. ->condition('ces.entity_id', array_keys($entities), 'IN')
  77. ->condition('ces.entity_type', $entity_type)
  78. ->execute();
  79. $statistics_records = [];
  80. while ($entry = $stats->fetchObject()) {
  81. $statistics_records[] = $entry;
  82. }
  83. return $statistics_records;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. public function delete(EntityInterface $entity) {
  89. $this->database->delete('comment_entity_statistics')
  90. ->condition('entity_id', $entity->id())
  91. ->condition('entity_type', $entity->getEntityTypeId())
  92. ->execute();
  93. }
  94. /**
  95. * {@inheritdoc}
  96. */
  97. public function create(FieldableEntityInterface $entity, $fields) {
  98. $query = $this->database->insert('comment_entity_statistics')
  99. ->fields([
  100. 'entity_id',
  101. 'entity_type',
  102. 'field_name',
  103. 'cid',
  104. 'last_comment_timestamp',
  105. 'last_comment_name',
  106. 'last_comment_uid',
  107. 'comment_count',
  108. ]);
  109. foreach ($fields as $field_name => $detail) {
  110. // Skip fields that entity does not have.
  111. if (!$entity->hasField($field_name)) {
  112. continue;
  113. }
  114. // Get the user ID from the entity if it's set, or default to the
  115. // currently logged in user.
  116. $last_comment_uid = 0;
  117. if ($entity instanceof EntityOwnerInterface) {
  118. $last_comment_uid = $entity->getOwnerId();
  119. }
  120. if (!isset($last_comment_uid)) {
  121. // Default to current user when entity does not implement
  122. // EntityOwnerInterface or author is not set.
  123. $last_comment_uid = $this->currentUser->id();
  124. }
  125. // Default to REQUEST_TIME when entity does not have a changed property.
  126. $last_comment_timestamp = REQUEST_TIME;
  127. // @todo Make comment statistics language aware and add some tests. See
  128. // https://www.drupal.org/node/2318875
  129. if ($entity instanceof EntityChangedInterface) {
  130. $last_comment_timestamp = $entity->getChangedTimeAcrossTranslations();
  131. }
  132. $query->values([
  133. 'entity_id' => $entity->id(),
  134. 'entity_type' => $entity->getEntityTypeId(),
  135. 'field_name' => $field_name,
  136. 'cid' => 0,
  137. 'last_comment_timestamp' => $last_comment_timestamp,
  138. 'last_comment_name' => NULL,
  139. 'last_comment_uid' => $last_comment_uid,
  140. 'comment_count' => 0,
  141. ]);
  142. }
  143. $query->execute();
  144. }
  145. /**
  146. * {@inheritdoc}
  147. */
  148. public function getMaximumCount($entity_type) {
  149. return $this->database->query('SELECT MAX(comment_count) FROM {comment_entity_statistics} WHERE entity_type = :entity_type', [':entity_type' => $entity_type])->fetchField();
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. public function getRankingInfo() {
  155. return [
  156. 'comments' => [
  157. 'title' => t('Number of comments'),
  158. 'join' => [
  159. 'type' => 'LEFT',
  160. 'table' => 'comment_entity_statistics',
  161. 'alias' => 'ces',
  162. // Default to comment field as this is the most common use case for
  163. // nodes.
  164. 'on' => "ces.entity_id = i.sid AND ces.entity_type = 'node' AND ces.field_name = 'comment'",
  165. ],
  166. // Inverse law that maps the highest view count on the site to 1 and 0
  167. // to 0. Note that the ROUND here is necessary for PostgreSQL and SQLite
  168. // in order to ensure that the :comment_scale argument is treated as
  169. // a numeric type, because the PostgreSQL PDO driver sometimes puts
  170. // values in as strings instead of numbers in complex expressions like
  171. // this.
  172. 'score' => '2.0 - 2.0 / (1.0 + ces.comment_count * (ROUND(:comment_scale, 4)))',
  173. 'arguments' => [':comment_scale' => \Drupal::state()->get('comment.node_comment_statistics_scale') ?: 0],
  174. ],
  175. ];
  176. }
  177. /**
  178. * {@inheritdoc}
  179. */
  180. public function update(CommentInterface $comment) {
  181. // Allow bulk updates and inserts to temporarily disable the maintenance of
  182. // the {comment_entity_statistics} table.
  183. if (!$this->state->get('comment.maintain_entity_statistics')) {
  184. return;
  185. }
  186. $query = $this->database->select('comment_field_data', 'c');
  187. $query->addExpression('COUNT(cid)');
  188. $count = $query->condition('c.entity_id', $comment->getCommentedEntityId())
  189. ->condition('c.entity_type', $comment->getCommentedEntityTypeId())
  190. ->condition('c.field_name', $comment->getFieldName())
  191. ->condition('c.status', CommentInterface::PUBLISHED)
  192. ->condition('default_langcode', 1)
  193. ->execute()
  194. ->fetchField();
  195. if ($count > 0) {
  196. // Comments exist.
  197. $last_reply = $this->database->select('comment_field_data', 'c')
  198. ->fields('c', ['cid', 'name', 'changed', 'uid'])
  199. ->condition('c.entity_id', $comment->getCommentedEntityId())
  200. ->condition('c.entity_type', $comment->getCommentedEntityTypeId())
  201. ->condition('c.field_name', $comment->getFieldName())
  202. ->condition('c.status', CommentInterface::PUBLISHED)
  203. ->condition('default_langcode', 1)
  204. ->orderBy('c.created', 'DESC')
  205. ->range(0, 1)
  206. ->execute()
  207. ->fetchObject();
  208. // Use merge here because entity could be created before comment field.
  209. $this->database->merge('comment_entity_statistics')
  210. ->fields([
  211. 'cid' => $last_reply->cid,
  212. 'comment_count' => $count,
  213. 'last_comment_timestamp' => $last_reply->changed,
  214. 'last_comment_name' => $last_reply->uid ? '' : $last_reply->name,
  215. 'last_comment_uid' => $last_reply->uid,
  216. ])
  217. ->keys([
  218. 'entity_id' => $comment->getCommentedEntityId(),
  219. 'entity_type' => $comment->getCommentedEntityTypeId(),
  220. 'field_name' => $comment->getFieldName(),
  221. ])
  222. ->execute();
  223. }
  224. else {
  225. // Comments do not exist.
  226. $entity = $comment->getCommentedEntity();
  227. // Get the user ID from the entity if it's set, or default to the
  228. // currently logged in user.
  229. if ($entity instanceof EntityOwnerInterface) {
  230. $last_comment_uid = $entity->getOwnerId();
  231. }
  232. if (!isset($last_comment_uid)) {
  233. // Default to current user when entity does not implement
  234. // EntityOwnerInterface or author is not set.
  235. $last_comment_uid = $this->currentUser->id();
  236. }
  237. $this->database->update('comment_entity_statistics')
  238. ->fields([
  239. 'cid' => 0,
  240. 'comment_count' => 0,
  241. // Use the changed date of the entity if it's set, or default to
  242. // REQUEST_TIME.
  243. 'last_comment_timestamp' => ($entity instanceof EntityChangedInterface) ? $entity->getChangedTimeAcrossTranslations() : REQUEST_TIME,
  244. 'last_comment_name' => '',
  245. 'last_comment_uid' => $last_comment_uid,
  246. ])
  247. ->condition('entity_id', $comment->getCommentedEntityId())
  248. ->condition('entity_type', $comment->getCommentedEntityTypeId())
  249. ->condition('field_name', $comment->getFieldName())
  250. ->execute();
  251. }
  252. // Reset the cache of the commented entity so that when the entity is loaded
  253. // the next time, the statistics will be loaded again.
  254. $this->entityTypeManager->getStorage($comment->getCommentedEntityTypeId())->resetCache([$comment->getCommentedEntityId()]);
  255. }
  256. }