annotate core/modules/comment/src/CommentStorage.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\comment;
Chris@0 4
Chris@0 5 use Drupal\Core\Cache\CacheBackendInterface;
Chris@17 6 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
Chris@0 7 use Drupal\Core\Database\Connection;
Chris@18 8 use Drupal\Core\Entity\EntityFieldManagerInterface;
Chris@18 9 use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
Chris@18 10 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
Chris@0 11 use Drupal\Core\Entity\EntityTypeInterface;
Chris@0 12 use Drupal\Core\Entity\EntityInterface;
Chris@18 13 use Drupal\Core\Entity\EntityTypeManagerInterface;
Chris@0 14 use Drupal\Core\Entity\FieldableEntityInterface;
Chris@0 15 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
Chris@0 16 use Drupal\Core\Session\AccountInterface;
Chris@0 17 use Drupal\Core\Language\LanguageManagerInterface;
Chris@0 18 use Symfony\Component\DependencyInjection\ContainerInterface;
Chris@0 19
Chris@0 20 /**
Chris@0 21 * Defines the storage handler class for comments.
Chris@0 22 *
Chris@0 23 * This extends the Drupal\Core\Entity\Sql\SqlContentEntityStorage class,
Chris@0 24 * adding required special handling for comment entities.
Chris@0 25 */
Chris@0 26 class CommentStorage extends SqlContentEntityStorage implements CommentStorageInterface {
Chris@0 27
Chris@0 28 /**
Chris@0 29 * The current user.
Chris@0 30 *
Chris@0 31 * @var \Drupal\Core\Session\AccountInterface
Chris@0 32 */
Chris@0 33 protected $currentUser;
Chris@0 34
Chris@0 35 /**
Chris@0 36 * Constructs a CommentStorage object.
Chris@0 37 *
Chris@0 38 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_info
Chris@0 39 * An array of entity info for the entity type.
Chris@0 40 * @param \Drupal\Core\Database\Connection $database
Chris@0 41 * The database connection to be used.
Chris@18 42 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
Chris@18 43 * The entity field manager.
Chris@0 44 * @param \Drupal\Core\Session\AccountInterface $current_user
Chris@0 45 * The current user.
Chris@0 46 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
Chris@0 47 * Cache backend instance to use.
Chris@0 48 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
Chris@0 49 * The language manager.
Chris@17 50 * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
Chris@18 51 * The memory cache.*
Chris@18 52 * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
Chris@18 53 * The entity type bundle info.
Chris@18 54 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
Chris@18 55 * The entity type manager.
Chris@18 56 * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository
Chris@18 57 * The entity last installed schema repository.
Chris@0 58 */
Chris@18 59 public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, EntityTypeManagerInterface $entity_type_manager = NULL, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository = NULL) {
Chris@18 60 parent::__construct($entity_info, $database, $entity_field_manager, $cache, $language_manager, $memory_cache, $entity_type_bundle_info, $entity_type_manager, $entity_last_installed_schema_repository);
Chris@0 61 $this->currentUser = $current_user;
Chris@0 62 }
Chris@0 63
Chris@0 64 /**
Chris@0 65 * {@inheritdoc}
Chris@0 66 */
Chris@0 67 public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_info) {
Chris@0 68 return new static(
Chris@0 69 $entity_info,
Chris@0 70 $container->get('database'),
Chris@18 71 $container->get('entity_field.manager'),
Chris@0 72 $container->get('current_user'),
Chris@0 73 $container->get('cache.entity'),
Chris@17 74 $container->get('language_manager'),
Chris@18 75 $container->get('entity.memory_cache'),
Chris@18 76 $container->get('entity_type.bundle.info'),
Chris@18 77 $container->get('entity_type.manager'),
Chris@18 78 $container->get('entity.last_installed_schema.repository')
Chris@0 79 );
Chris@0 80 }
Chris@0 81
Chris@0 82 /**
Chris@0 83 * {@inheritdoc}
Chris@0 84 */
Chris@0 85 public function getMaxThread(CommentInterface $comment) {
Chris@17 86 $query = $this->database->select($this->getDataTable(), 'c')
Chris@0 87 ->condition('entity_id', $comment->getCommentedEntityId())
Chris@0 88 ->condition('field_name', $comment->getFieldName())
Chris@0 89 ->condition('entity_type', $comment->getCommentedEntityTypeId())
Chris@0 90 ->condition('default_langcode', 1);
Chris@0 91 $query->addExpression('MAX(thread)', 'thread');
Chris@0 92 return $query->execute()
Chris@0 93 ->fetchField();
Chris@0 94 }
Chris@0 95
Chris@0 96 /**
Chris@0 97 * {@inheritdoc}
Chris@0 98 */
Chris@0 99 public function getMaxThreadPerThread(CommentInterface $comment) {
Chris@17 100 $query = $this->database->select($this->getDataTable(), 'c')
Chris@0 101 ->condition('entity_id', $comment->getCommentedEntityId())
Chris@0 102 ->condition('field_name', $comment->getFieldName())
Chris@0 103 ->condition('entity_type', $comment->getCommentedEntityTypeId())
Chris@0 104 ->condition('thread', $comment->getParentComment()->getThread() . '.%', 'LIKE')
Chris@0 105 ->condition('default_langcode', 1);
Chris@0 106 $query->addExpression('MAX(thread)', 'thread');
Chris@0 107 return $query->execute()
Chris@0 108 ->fetchField();
Chris@0 109 }
Chris@0 110
Chris@0 111 /**
Chris@0 112 * {@inheritdoc}
Chris@0 113 */
Chris@0 114 public function getDisplayOrdinal(CommentInterface $comment, $comment_mode, $divisor = 1) {
Chris@0 115 // Count how many comments (c1) are before $comment (c2) in display order.
Chris@0 116 // This is the 0-based display ordinal.
Chris@17 117 $data_table = $this->getDataTable();
Chris@17 118 $query = $this->database->select($data_table, 'c1');
Chris@17 119 $query->innerJoin($data_table, 'c2', 'c2.entity_id = c1.entity_id AND c2.entity_type = c1.entity_type AND c2.field_name = c1.field_name');
Chris@0 120 $query->addExpression('COUNT(*)', 'count');
Chris@0 121 $query->condition('c2.cid', $comment->id());
Chris@0 122 if (!$this->currentUser->hasPermission('administer comments')) {
Chris@0 123 $query->condition('c1.status', CommentInterface::PUBLISHED);
Chris@0 124 }
Chris@0 125
Chris@0 126 if ($comment_mode == CommentManagerInterface::COMMENT_MODE_FLAT) {
Chris@0 127 // For rendering flat comments, cid is used for ordering comments due to
Chris@0 128 // unpredictable behavior with timestamp, so we make the same assumption
Chris@0 129 // here.
Chris@0 130 $query->condition('c1.cid', $comment->id(), '<');
Chris@0 131 }
Chris@0 132 else {
Chris@0 133 // For threaded comments, the c.thread column is used for ordering. We can
Chris@0 134 // use the sorting code for comparison, but must remove the trailing
Chris@0 135 // slash.
Chris@0 136 $query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) - 1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) - 1))');
Chris@0 137 }
Chris@0 138
Chris@0 139 $query->condition('c1.default_langcode', 1);
Chris@0 140 $query->condition('c2.default_langcode', 1);
Chris@0 141
Chris@0 142 $ordinal = $query->execute()->fetchField();
Chris@0 143
Chris@0 144 return ($divisor > 1) ? floor($ordinal / $divisor) : $ordinal;
Chris@0 145 }
Chris@0 146
Chris@0 147 /**
Chris@0 148 * {@inheritdoc}
Chris@0 149 */
Chris@0 150 public function getNewCommentPageNumber($total_comments, $new_comments, FieldableEntityInterface $entity, $field_name) {
Chris@0 151 $field = $entity->getFieldDefinition($field_name);
Chris@0 152 $comments_per_page = $field->getSetting('per_page');
Chris@17 153 $data_table = $this->getDataTable();
Chris@0 154
Chris@0 155 if ($total_comments <= $comments_per_page) {
Chris@0 156 // Only one page of comments.
Chris@0 157 $count = 0;
Chris@0 158 }
Chris@0 159 elseif ($field->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) {
Chris@0 160 // Flat comments.
Chris@0 161 $count = $total_comments - $new_comments;
Chris@0 162 }
Chris@0 163 else {
Chris@0 164 // Threaded comments.
Chris@0 165
Chris@0 166 // 1. Find all the threads with a new comment.
Chris@17 167 $unread_threads_query = $this->database->select($data_table, 'comment')
Chris@0 168 ->fields('comment', ['thread'])
Chris@0 169 ->condition('entity_id', $entity->id())
Chris@0 170 ->condition('entity_type', $entity->getEntityTypeId())
Chris@0 171 ->condition('field_name', $field_name)
Chris@0 172 ->condition('status', CommentInterface::PUBLISHED)
Chris@0 173 ->condition('default_langcode', 1)
Chris@0 174 ->orderBy('created', 'DESC')
Chris@0 175 ->orderBy('cid', 'DESC')
Chris@0 176 ->range(0, $new_comments);
Chris@0 177
Chris@0 178 // 2. Find the first thread.
Chris@0 179 $first_thread_query = $this->database->select($unread_threads_query, 'thread');
Chris@0 180 $first_thread_query->addExpression('SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 'torder');
Chris@0 181 $first_thread = $first_thread_query
Chris@0 182 ->fields('thread', ['thread'])
Chris@0 183 ->orderBy('torder')
Chris@0 184 ->range(0, 1)
Chris@0 185 ->execute()
Chris@0 186 ->fetchField();
Chris@0 187
Chris@0 188 // Remove the final '/'.
Chris@0 189 $first_thread = substr($first_thread, 0, -1);
Chris@0 190
Chris@0 191 // Find the number of the first comment of the first unread thread.
Chris@17 192 $count = $this->database->query('SELECT COUNT(*) FROM {' . $data_table . '} WHERE entity_id = :entity_id
Chris@0 193 AND entity_type = :entity_type
Chris@0 194 AND field_name = :field_name
Chris@0 195 AND status = :status
Chris@0 196 AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread
Chris@0 197 AND default_langcode = 1', [
Chris@0 198 ':status' => CommentInterface::PUBLISHED,
Chris@0 199 ':entity_id' => $entity->id(),
Chris@0 200 ':field_name' => $field_name,
Chris@0 201 ':entity_type' => $entity->getEntityTypeId(),
Chris@0 202 ':thread' => $first_thread,
Chris@0 203 ])->fetchField();
Chris@0 204 }
Chris@0 205
Chris@0 206 return $comments_per_page > 0 ? (int) ($count / $comments_per_page) : 0;
Chris@0 207 }
Chris@0 208
Chris@0 209 /**
Chris@0 210 * {@inheritdoc}
Chris@0 211 */
Chris@0 212 public function getChildCids(array $comments) {
Chris@17 213 return $this->database->select($this->getDataTable(), 'c')
Chris@0 214 ->fields('c', ['cid'])
Chris@0 215 ->condition('pid', array_keys($comments), 'IN')
Chris@0 216 ->condition('default_langcode', 1)
Chris@0 217 ->execute()
Chris@0 218 ->fetchCol();
Chris@0 219 }
Chris@0 220
Chris@0 221 /**
Chris@0 222 * {@inheritdoc}
Chris@0 223 *
Chris@0 224 * To display threaded comments in the correct order we keep a 'thread' field
Chris@0 225 * and order by that value. This field keeps this data in
Chris@0 226 * a way which is easy to update and convenient to use.
Chris@0 227 *
Chris@0 228 * A "thread" value starts at "1". If we add a child (A) to this comment,
Chris@0 229 * we assign it a "thread" = "1.1". A child of (A) will have "1.1.1". Next
Chris@0 230 * brother of (A) will get "1.2". Next brother of the parent of (A) will get
Chris@0 231 * "2" and so on.
Chris@0 232 *
Chris@0 233 * First of all note that the thread field stores the depth of the comment:
Chris@0 234 * depth 0 will be "X", depth 1 "X.X", depth 2 "X.X.X", etc.
Chris@0 235 *
Chris@0 236 * Now to get the ordering right, consider this example:
Chris@0 237 *
Chris@0 238 * 1
Chris@0 239 * 1.1
Chris@0 240 * 1.1.1
Chris@0 241 * 1.2
Chris@0 242 * 2
Chris@0 243 *
Chris@0 244 * If we "ORDER BY thread ASC" we get the above result, and this is the
Chris@0 245 * natural order sorted by time. However, if we "ORDER BY thread DESC"
Chris@0 246 * we get:
Chris@0 247 *
Chris@0 248 * 2
Chris@0 249 * 1.2
Chris@0 250 * 1.1.1
Chris@0 251 * 1.1
Chris@0 252 * 1
Chris@0 253 *
Chris@0 254 * Clearly, this is not a natural way to see a thread, and users will get
Chris@0 255 * confused. The natural order to show a thread by time desc would be:
Chris@0 256 *
Chris@0 257 * 2
Chris@0 258 * 1
Chris@0 259 * 1.2
Chris@0 260 * 1.1
Chris@0 261 * 1.1.1
Chris@0 262 *
Chris@0 263 * which is what we already did before the standard pager patch. To achieve
Chris@0 264 * this we simply add a "/" at the end of each "thread" value. This way, the
Chris@0 265 * thread fields will look like this:
Chris@0 266 *
Chris@0 267 * 1/
Chris@0 268 * 1.1/
Chris@0 269 * 1.1.1/
Chris@0 270 * 1.2/
Chris@0 271 * 2/
Chris@0 272 *
Chris@0 273 * we add "/" since this char is, in ASCII, higher than every number, so if
Chris@0 274 * now we "ORDER BY thread DESC" we get the correct order. However this would
Chris@0 275 * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
Chris@0 276 * to consider the trailing "/" so we use a substring only.
Chris@0 277 */
Chris@0 278 public function loadThread(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0) {
Chris@17 279 $data_table = $this->getDataTable();
Chris@17 280 $query = $this->database->select($data_table, 'c');
Chris@0 281 $query->addField('c', 'cid');
Chris@0 282 $query
Chris@0 283 ->condition('c.entity_id', $entity->id())
Chris@0 284 ->condition('c.entity_type', $entity->getEntityTypeId())
Chris@0 285 ->condition('c.field_name', $field_name)
Chris@0 286 ->condition('c.default_langcode', 1)
Chris@0 287 ->addTag('entity_access')
Chris@0 288 ->addTag('comment_filter')
Chris@0 289 ->addMetaData('base_table', 'comment')
Chris@0 290 ->addMetaData('entity', $entity)
Chris@0 291 ->addMetaData('field_name', $field_name);
Chris@0 292
Chris@0 293 if ($comments_per_page) {
Chris@0 294 $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')
Chris@0 295 ->limit($comments_per_page);
Chris@0 296 if ($pager_id) {
Chris@0 297 $query->element($pager_id);
Chris@0 298 }
Chris@0 299
Chris@17 300 $count_query = $this->database->select($data_table, 'c');
Chris@0 301 $count_query->addExpression('COUNT(*)');
Chris@0 302 $count_query
Chris@0 303 ->condition('c.entity_id', $entity->id())
Chris@0 304 ->condition('c.entity_type', $entity->getEntityTypeId())
Chris@0 305 ->condition('c.field_name', $field_name)
Chris@0 306 ->condition('c.default_langcode', 1)
Chris@0 307 ->addTag('entity_access')
Chris@0 308 ->addTag('comment_filter')
Chris@0 309 ->addMetaData('base_table', 'comment')
Chris@0 310 ->addMetaData('entity', $entity)
Chris@0 311 ->addMetaData('field_name', $field_name);
Chris@0 312 $query->setCountQuery($count_query);
Chris@0 313 }
Chris@0 314
Chris@0 315 if (!$this->currentUser->hasPermission('administer comments')) {
Chris@0 316 $query->condition('c.status', CommentInterface::PUBLISHED);
Chris@0 317 if ($comments_per_page) {
Chris@0 318 $count_query->condition('c.status', CommentInterface::PUBLISHED);
Chris@0 319 }
Chris@0 320 }
Chris@0 321 if ($mode == CommentManagerInterface::COMMENT_MODE_FLAT) {
Chris@0 322 $query->orderBy('c.cid', 'ASC');
Chris@0 323 }
Chris@0 324 else {
Chris@0 325 // See comment above. Analysis reveals that this doesn't cost too
Chris@0 326 // much. It scales much much better than having the whole comment
Chris@0 327 // structure.
Chris@0 328 $query->addExpression('SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))', 'torder');
Chris@0 329 $query->orderBy('torder', 'ASC');
Chris@0 330 }
Chris@0 331
Chris@0 332 $cids = $query->execute()->fetchCol();
Chris@0 333
Chris@0 334 $comments = [];
Chris@0 335 if ($cids) {
Chris@0 336 $comments = $this->loadMultiple($cids);
Chris@0 337 }
Chris@0 338
Chris@0 339 return $comments;
Chris@0 340 }
Chris@0 341
Chris@0 342 /**
Chris@0 343 * {@inheritdoc}
Chris@0 344 */
Chris@0 345 public function getUnapprovedCount() {
Chris@17 346 return $this->database->select($this->getDataTable(), 'c')
Chris@0 347 ->condition('status', CommentInterface::NOT_PUBLISHED, '=')
Chris@0 348 ->condition('default_langcode', 1)
Chris@0 349 ->countQuery()
Chris@0 350 ->execute()
Chris@0 351 ->fetchField();
Chris@0 352 }
Chris@0 353
Chris@0 354 }