Drupal 8: Custom Ordering Of Comments

Drupal 8's comment system has the ability to be threaded so that users can reply directly to other users comments and create threads of conversation. I have always found this difficult to use and difficult to read so I wanted to turn it off when I set up this site. The only issue I had was I could turn off the threading but couldn't alter the ordering of the comments.

The default ordering of comments in Drupal 8 is by thread. This means that even if you turn off threading the comments are always displayed in thread order. The CommentDefaultFormatter class is used by Drupal to display comments and contains this line of code.

$comments = $this->storage->loadThread($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id'));

There are no options to change this loadThread() to something else, which means that as long as Drupal uses this class to display comments it will always use this method.

To override this we can use a custom FieldFormatter plugin and re-order the comments using a custom formatter.

In a custom module create a class at src/Plugin/Field/FieldFormatter/CustomCommentFormatter.php. This is the plugin that will allow us to provide our own custom comment ordering. This class just extends the CommentDefaultFormatter class (which is the one we want to override) and so we only need to include the methods we need to override.

<?php

namespace Drupal\custom_comment_module\Plugin\Field\FieldFormatter;

use Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter;

/**
 * Provides a custom comment formatter to order comments by date.
 *
 * @FieldFormatter(
 *   id = "comment_orderbydate",
 *   module = "comment",
 *   label = @Translation("Custom comment list"),
 *   field_types = {
 *     "comment"
 *   },
 *   quickedit = {
 *     "editor" = "disabled"
 *   }
 * )
 */
class CustomCommentFormatter extends CommentDefaultFormatter {

}

Due to the way in which comments are retrieved and ordered in Drupal we would need to override the CommentStorage class in order to change how comments are loaded. This is possible to do by using hook_entity_type_alter() to change the storage handler of the Comment entity. This would be done in the following way.

/**
 * Implements hook_entity_type_alter().
 */
function custom_comment_entity_type_alter(array &$entity_types) {
  $entity_types['page']->setHandlerClass('storage', CustomCommentStorage::class);
}

An alternative (and perhaps simpler?) way is to replace the loadThread() method within the class with something that will load comments in the way we want. To do this I first needed to modify the CustomCommentFormatter class in order to load in the database though dependency injection. Although it looks like I have written a lot of code here I have, in fact just added the database handler, all other injected dependencies are already used by the CommentDefaultFormatter class and so need to be included.

<?php

namespace Drupal\custom_comment_module\Plugin\Field\FieldFormatter;

use Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\comment\CommentInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;

/**
 * Provides a custom comment formatter to order comments by date.
 *
 * @FieldFormatter(
 *   id = "comment_orderbydate",
 *   module = "comment",
 *   label = @Translation("Custom comment list"),
 *   field_types = {
 *     "comment"
 *   },
 *   quickedit = {
 *     "editor" = "disabled"
 *   }
 * )
 */
class CustomCommentFormatter extends CommentDefaultFormatter {

  /**
   * The entity form builder.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['label'],
      $configuration['view_mode'],
      $configuration['third_party_settings'],
      $container->get('current_user'),
      $container->get('entity.manager'),
      $container->get('entity.form_builder'),
      $container->get('current_route_match'),
      $container->get('database')
    );
  }

  /**
   * CustomCommentFormatter constructor.
   *
   * @param string $plugin_id
   *   The plugin_id for the formatter.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The definition of the field to which the formatter is associated.
   * @param array $settings
   *   The formatter settings.
   * @param string $label
   *   The formatter label display setting.
   * @param string $view_mode
   *   The view mode.
   * @param array $third_party_settings
   *   Third party settings.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager
   * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
   *   The entity form builder.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The route match object.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
   */
  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder, RouteMatchInterface $route_match, Connection $database) {
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $current_user, $entity_manager, $entity_form_builder, $route_match);
    $this->database = $database;
  }
}

With this in place we could then add a custom loadComments() method. This method is very similar to the loadThread() method, but in this case it loads comments by date, ordered ascending. 

  /**
   * Retrieves comments for a thread, sorted in an order suitable for display.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity whose comment(s) needs rendering.
   * @param string $field_name
   *   The field_name whose comment(s) needs rendering.
   * @param int $mode
   *   The comment display mode: CommentManagerInterface::COMMENT_MODE_FLAT or
   *   CommentManagerInterface::COMMENT_MODE_THREADED.
   * @param int $comments_per_page
   *   (optional) The amount of comments to display per page.
   *   Defaults to 0, which means show all comments.
   * @param int $pager_id
   *   (optional) Pager id to use in case of multiple pagers on the one page.
   *   Defaults to 0; is only used when $comments_per_page is greater than zero.
   *
   * @return array
   *   Ordered array of comment objects, keyed by comment id.
   */
  public function loadComments(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0) {
    $query = $this->database->select('comment_field_data', 'c');
    $query->addField('c', 'cid');
    $query
      ->condition('c.entity_id', $entity->id())
      ->condition('c.entity_type', $entity->getEntityTypeId())
      ->condition('c.field_name', $field_name)
      ->condition('c.default_langcode', 1)
      ->addTag('entity_access')
      ->addTag('comment_filter')
      ->addMetaData('base_table', 'comment')
      ->addMetaData('entity', $entity)
      ->addMetaData('field_name', $field_name);

    if ($comments_per_page) {
      $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')
        ->limit($comments_per_page);
      if ($pager_id) {
        $query->element($pager_id);
      }

      $count_query = $this->database->select('comment_field_data', 'c');
      $count_query->addExpression('COUNT(*)');
      $count_query
        ->condition('c.entity_id', $entity->id())
        ->condition('c.entity_type', $entity->getEntityTypeId())
        ->condition('c.field_name', $field_name)
        ->condition('c.default_langcode', 1)
        ->addTag('entity_access')
        ->addTag('comment_filter')
        ->addMetaData('base_table', 'comment')
        ->addMetaData('entity', $entity)
        ->addMetaData('field_name', $field_name);
      $query->setCountQuery($count_query);
    }

    if (!$this->currentUser->hasPermission('administer comments')) {
      $query->condition('c.status', CommentInterface::PUBLISHED);
      if ($comments_per_page) {
        $count_query->condition('c.status', CommentInterface::PUBLISHED);
      }
    }

    $query->orderBy('c.created', 'ASC');

    $cids = $query->execute()->fetchCol();

    $comments = [];
    if ($cids) {
      $comments = $this->storage->loadMultiple($cids);
    }

    return $comments;
  }

Now all that is needed is to alter the viewElements() method to that it uses the new loadComments() method instead of loadThread().

$comments = $this->loadComments($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id'));

After activating the module we can now select the formatter from the Manage display admin area. This can be found in the fields admin area for the content type that has comments enabled.

Custom comment formatter selection in Drupal 8 content type.

After saving the form the comments will be ordered in date order.

Comments

Thank you, this is the article I was looking for.

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
4 + 10 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.