3 namespace Drupal\forum;
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8 use Drupal\Core\Entity\EntityManagerInterface;
9 use Drupal\Core\Session\AccountInterface;
10 use Drupal\Core\StringTranslation\TranslationInterface;
11 use Drupal\Core\StringTranslation\StringTranslationTrait;
12 use Drupal\comment\CommentManagerInterface;
13 use Drupal\node\NodeInterface;
16 * Provides forum manager service.
18 class ForumManager implements ForumManagerInterface {
19 use StringTranslationTrait;
20 use DependencySerializationTrait {
21 __wakeup as defaultWakeup;
22 __sleep as defaultSleep;
26 * Forum sort order, newest first.
28 const NEWEST_FIRST = 1;
31 * Forum sort order, oldest first.
33 const OLDEST_FIRST = 2;
36 * Forum sort order, posts with most comments first.
38 const MOST_POPULAR_FIRST = 3;
41 * Forum sort order, posts with the least comments first.
43 const LEAST_POPULAR_FIRST = 4;
46 * Forum settings config object.
48 * @var \Drupal\Core\Config\ConfigFactoryInterface
50 protected $configFactory;
53 * Entity manager service
55 * @var \Drupal\Core\Entity\EntityManagerInterface
57 protected $entityManager;
62 * @var \Drupal\Core\Database\Connection
64 protected $connection;
67 * The comment manager service.
69 * @var \Drupal\comment\CommentManagerInterface
71 protected $commentManager;
74 * Array of last post information keyed by forum (term) id.
78 protected $lastPostData = [];
81 * Array of forum statistics keyed by forum (term) id.
85 protected $forumStatistics = [];
88 * Array of forum children keyed by parent forum (term) id.
92 protected $forumChildren = [];
95 * Array of history keyed by nid.
99 protected $history = [];
102 * Cached forum index.
104 * @var \Drupal\taxonomy\TermInterface
109 * Constructs the forum manager service.
111 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
112 * The config factory service.
113 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
114 * The entity manager service.
115 * @param \Drupal\Core\Database\Connection $connection
116 * The current database connection.
117 * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
118 * The translation manager service.
119 * @param \Drupal\comment\CommentManagerInterface $comment_manager
120 * The comment manager service.
122 public function __construct(ConfigFactoryInterface $config_factory, EntityManagerInterface $entity_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager) {
123 $this->configFactory = $config_factory;
124 $this->entityManager = $entity_manager;
125 $this->connection = $connection;
126 $this->stringTranslation = $string_translation;
127 $this->commentManager = $comment_manager;
133 public function getTopics($tid, AccountInterface $account) {
134 $config = $this->configFactory->get('forum.settings');
135 $forum_per_page = $config->get('topics.page_limit');
136 $sortby = $config->get('topics.order');
139 ['data' => $this->t('Topic'), 'field' => 'f.title'],
140 ['data' => $this->t('Replies'), 'field' => 'f.comment_count'],
141 ['data' => $this->t('Last reply'), 'field' => 'f.last_comment_timestamp'],
144 $order = $this->getTopicOrder($sortby);
145 for ($i = 0; $i < count($header); $i++) {
146 if ($header[$i]['field'] == $order['field']) {
147 $header[$i]['sort'] = $order['sort'];
151 $query = $this->connection->select('forum_index', 'f')
152 ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
153 ->extend('Drupal\Core\Database\Query\TableSortExtender');
156 ->condition('f.tid', $tid)
157 ->addTag('node_access')
158 ->addMetaData('base_table', 'forum_index')
159 ->orderBy('f.sticky', 'DESC')
160 ->orderByHeader($header)
161 ->limit($forum_per_page);
163 $count_query = $this->connection->select('forum_index', 'f');
164 $count_query->condition('f.tid', $tid);
165 $count_query->addExpression('COUNT(*)');
166 $count_query->addTag('node_access');
167 $count_query->addMetaData('base_table', 'forum_index');
169 $query->setCountQuery($count_query);
170 $result = $query->execute();
172 foreach ($result as $record) {
173 $nids[] = $record->nid;
176 $nodes = $this->entityManager->getStorage('node')->loadMultiple($nids);
178 $query = $this->connection->select('node_field_data', 'n')
179 ->extend('Drupal\Core\Database\Query\TableSortExtender');
180 $query->fields('n', ['nid']);
182 $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'");
183 $query->fields('ces', [
186 'last_comment_timestamp',
190 $query->join('forum_index', 'f', 'f.nid = n.nid');
191 $query->addField('f', 'tid', 'forum_tid');
193 $query->join('users_field_data', 'u', 'n.uid = u.uid AND u.default_langcode = 1');
194 $query->addField('u', 'name');
196 $query->join('users_field_data', 'u2', 'ces.last_comment_uid = u2.uid AND u.default_langcode = 1');
198 $query->addExpression('CASE ces.last_comment_uid WHEN 0 THEN ces.last_comment_name ELSE u2.name END', 'last_comment_name');
201 ->orderBy('f.sticky', 'DESC')
202 ->orderByHeader($header)
203 ->condition('n.nid', $nids, 'IN')
204 // @todo This should be actually filtering on the desired node language
205 // and just fall back to the default language.
206 ->condition('n.default_langcode', 1);
209 foreach ($query->execute() as $row) {
210 $topic = $nodes[$row->nid];
211 $topic->comment_mode = $topic->comment_forum->status;
213 foreach ($row as $key => $value) {
214 $topic->{$key} = $value;
224 $first_new_found = FALSE;
225 foreach ($result as $topic) {
226 if ($account->isAuthenticated()) {
227 // A forum is new if the topic is new, or if there are new comments since
228 // the user's last visit.
229 if ($topic->forum_tid != $tid) {
233 $history = $this->lastVisit($topic->id(), $account);
234 $topic->new_replies = $this->commentManager->getCountNewComments($topic, 'comment_forum', $history);
235 $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
239 // Do not track "new replies" status for topics if the user is anonymous.
240 $topic->new_replies = 0;
244 // Make sure only one topic is indicated as the first new topic.
245 $topic->first_new = FALSE;
246 if ($topic->new != 0 && !$first_new_found) {
247 $topic->first_new = TRUE;
248 $first_new_found = TRUE;
251 if ($topic->comment_count > 0) {
252 $last_reply = new \stdClass();
253 $last_reply->created = $topic->last_comment_timestamp;
254 $last_reply->name = $topic->last_comment_name;
255 $last_reply->uid = $topic->last_comment_uid;
256 $topic->last_reply = $last_reply;
258 $topics[$topic->id()] = $topic;
261 return ['topics' => $topics, 'header' => $header];
266 * Gets topic sorting information based on an integer code.
269 * One of the following integers indicating the sort criteria:
270 * - ForumManager::NEWEST_FIRST: Date - newest first.
271 * - ForumManager::OLDEST_FIRST: Date - oldest first.
272 * - ForumManager::MOST_POPULAR_FIRST: Posts with the most comments first.
273 * - ForumManager::LEAST_POPULAR_FIRST: Posts with the least comments first.
276 * An array with the following values:
277 * - field: A field for an SQL query.
278 * - sort: 'asc' or 'desc'.
280 protected function getTopicOrder($sortby) {
282 case static::NEWEST_FIRST:
283 return ['field' => 'f.last_comment_timestamp', 'sort' => 'desc'];
285 case static::OLDEST_FIRST:
286 return ['field' => 'f.last_comment_timestamp', 'sort' => 'asc'];
288 case static::MOST_POPULAR_FIRST:
289 return ['field' => 'f.comment_count', 'sort' => 'desc'];
291 case static::LEAST_POPULAR_FIRST:
292 return ['field' => 'f.comment_count', 'sort' => 'asc'];
298 * Gets the last time the user viewed a node.
302 * @param \Drupal\Core\Session\AccountInterface $account
303 * Account to fetch last time for.
306 * The timestamp when the user last viewed this node, if the user has
307 * previously viewed the node; otherwise HISTORY_READ_LIMIT.
309 protected function lastVisit($nid, AccountInterface $account) {
310 if (empty($this->history[$nid])) {
311 $result = $this->connection->select('history', 'h')
312 ->fields('h', ['nid', 'timestamp'])
313 ->condition('uid', $account->id())
315 foreach ($result as $t) {
316 $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
319 return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT;
323 * Provides the last post information for the given forum tid.
329 * The last post for the given forum.
331 protected function getLastPost($tid) {
332 if (!empty($this->lastPostData[$tid])) {
333 return $this->lastPostData[$tid];
335 // Query "Last Post" information for this forum.
336 $query = $this->connection->select('node_field_data', 'n');
337 $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $tid]);
338 $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'");
339 $query->join('users_field_data', 'u', 'ces.last_comment_uid = u.uid AND u.default_langcode = 1');
340 $query->addExpression('CASE ces.last_comment_uid WHEN 0 THEN ces.last_comment_name ELSE u.name END', 'last_comment_name');
343 ->fields('ces', ['last_comment_timestamp', 'last_comment_uid'])
344 ->condition('n.status', 1)
345 ->orderBy('last_comment_timestamp', 'DESC')
347 ->addTag('node_access')
351 // Build the last post information.
352 $last_post = new \stdClass();
353 if (!empty($topic->last_comment_timestamp)) {
354 $last_post->created = $topic->last_comment_timestamp;
355 $last_post->name = $topic->last_comment_name;
356 $last_post->uid = $topic->last_comment_uid;
359 $this->lastPostData[$tid] = $last_post;
364 * Provides statistics for a forum.
369 * @return \stdClass|null
370 * Statistics for the given forum if statistics exist, else NULL.
372 protected function getForumStatistics($tid) {
373 if (empty($this->forumStatistics)) {
374 // Prime the statistics.
375 $query = $this->connection->select('node_field_data', 'n');
376 $query->join('comment_entity_statistics', 'ces', "n.nid = ces.entity_id AND ces.field_name = 'comment_forum' AND ces.entity_type = 'node'");
377 $query->join('forum', 'f', 'n.vid = f.vid');
378 $query->addExpression('COUNT(n.nid)', 'topic_count');
379 $query->addExpression('SUM(ces.comment_count)', 'comment_count');
380 $this->forumStatistics = $query
381 ->fields('f', ['tid'])
382 ->condition('n.status', 1)
383 ->condition('n.default_langcode', 1)
385 ->addTag('node_access')
387 ->fetchAllAssoc('tid');
390 if (!empty($this->forumStatistics[$tid])) {
391 return $this->forumStatistics[$tid];
398 public function getChildren($vid, $tid) {
399 if (!empty($this->forumChildren[$tid])) {
400 return $this->forumChildren[$tid];
403 $_forums = $this->entityManager->getStorage('taxonomy_term')->loadTree($vid, $tid, NULL, TRUE);
404 foreach ($_forums as $forum) {
405 // Merge in the topic and post counters.
406 if (($count = $this->getForumStatistics($forum->id()))) {
407 $forum->num_topics = $count->topic_count;
408 $forum->num_posts = $count->topic_count + $count->comment_count;
411 $forum->num_topics = 0;
412 $forum->num_posts = 0;
415 // Merge in last post details.
416 $forum->last_post = $this->getLastPost($forum->id());
417 $forums[$forum->id()] = $forum;
420 $this->forumChildren[$tid] = $forums;
427 public function getIndex() {
432 $vid = $this->configFactory->get('forum.settings')->get('vocabulary');
433 $index = $this->entityManager->getStorage('taxonomy_term')->create([
441 // Load the tree below.
442 $index->forums = $this->getChildren($vid, 0);
443 $this->index = $index;
450 public function resetCache() {
460 public function getParents($tid) {
461 return $this->entityManager->getStorage('taxonomy_term')->loadAllParents($tid);
467 public function checkNodeType(NodeInterface $node) {
468 // Fetch information about the forum field.
469 $field_definitions = $this->entityManager->getFieldDefinitions('node', $node->bundle());
470 return !empty($field_definitions['taxonomy_forums']);
476 public function unreadTopics($term, $uid) {
477 $query = $this->connection->select('node_field_data', 'n');
478 $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', [':tid' => $term]);
479 $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', [':uid' => $uid]);
480 $query->addExpression('COUNT(n.nid)', 'count');
482 ->condition('status', 1)
483 // @todo This should be actually filtering on the desired node status
484 // field language and just fall back to the default language.
485 ->condition('n.default_langcode', 1)
486 ->condition('n.created', HISTORY_READ_LIMIT, '>')
488 ->addTag('node_access')
496 public function __sleep() {
497 $vars = $this->defaultSleep();
498 // Do not serialize static cache.
499 unset($vars['history'], $vars['index'], $vars['lastPostData'], $vars['forumChildren'], $vars['forumStatistics']);
506 public function __wakeup() {
507 $this->defaultWakeup();
508 // Initialize static cache.
510 $this->lastPostData = [];
511 $this->forumChildren = [];
512 $this->forumStatistics = [];