Drupal 8: Custom Ordering Of Comments

10th June 2018

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.

  1. <?php
  2.  
  3. namespace Drupal\custom_comment_module\Plugin\Field\FieldFormatter;
  4.  
  5. use Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter;
  6.  
  7. /**
  8.  * Provides a custom comment formatter to order comments by date.
  9.  *
  10.  * @FieldFormatter(
  11.  * id = "comment_orderbydate",
  12.  * module = "comment",
  13.  * label = @Translation("Custom comment list"),
  14.  * field_types = {
  15.  * "comment"
  16.  * },
  17.  * quickedit = {
  18.  * "editor" = "disabled"
  19.  * }
  20.  * )
  21.  */
  22. class CustomCommentFormatter extends CommentDefaultFormatter {
  23.  
  24. }

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.

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

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.

  1. <?php
  2.  
  3. namespace Drupal\custom_comment_module\Plugin\Field\FieldFormatter;
  4.  
  5. use Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter;
  6. use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
  7. use Drupal\comment\CommentInterface;
  8. use Drupal\Core\Entity\EntityInterface;
  9. use Drupal\Core\Field\FieldItemListInterface;
  10. use Symfony\Component\DependencyInjection\ContainerInterface;
  11. use Drupal\Core\Field\FieldDefinitionInterface;
  12. use Drupal\Core\Session\AccountInterface;
  13. use Drupal\Core\Entity\EntityManagerInterface;
  14. use Drupal\Core\Entity\EntityFormBuilderInterface;
  15. use Drupal\Core\Routing\RouteMatchInterface;
  16.  
  17. /**
  18.  * Provides a custom comment formatter to order comments by date.
  19.  *
  20.  * @FieldFormatter(
  21.  * id = "comment_orderbydate",
  22.  * module = "comment",
  23.  * label = @Translation("Custom comment list"),
  24.  * field_types = {
  25.  * "comment"
  26.  * },
  27.  * quickedit = {
  28.  * "editor" = "disabled"
  29.  * }
  30.  * )
  31.  */
  32. class CustomCommentFormatter extends CommentDefaultFormatter {
  33.  
  34. /**
  35.   * The entity form builder.
  36.   *
  37.   * @var \Drupal\Core\Database\Connection
  38.   */
  39. protected $database;
  40.  
  41. /**
  42.   * {@inheritdoc}
  43.   */
  44. public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  45. return new static(
  46. $plugin_id,
  47. $plugin_definition,
  48. $configuration['field_definition'],
  49. $configuration['settings'],
  50. $configuration['label'],
  51. $configuration['view_mode'],
  52. $configuration['third_party_settings'],
  53. $container->get('current_user'),
  54. $container->get('entity.manager'),
  55. $container->get('entity.form_builder'),
  56. $container->get('current_route_match'),
  57. $container->get('database')
  58. );
  59. }
  60.  
  61. /**
  62.   * CustomCommentFormatter constructor.
  63.   *
  64.   * @param string $plugin_id
  65.   * The plugin_id for the formatter.
  66.   * @param mixed $plugin_definition
  67.   * The plugin implementation definition.
  68.   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
  69.   * The definition of the field to which the formatter is associated.
  70.   * @param array $settings
  71.   * The formatter settings.
  72.   * @param string $label
  73.   * The formatter label display setting.
  74.   * @param string $view_mode
  75.   * The view mode.
  76.   * @param array $third_party_settings
  77.   * Third party settings.
  78.   * @param \Drupal\Core\Session\AccountInterface $current_user
  79.   * The current user.
  80.   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
  81.   * The entity manager
  82.   * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
  83.   * The entity form builder.
  84.   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
  85.   * The route match object.
  86.   * @param \Drupal\Core\Database\Connection $database
  87.   * The database connection to be used.
  88.   */
  89. 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) {
  90. parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $current_user, $entity_manager, $entity_form_builder, $route_match);
  91. $this->database = $database;
  92. }
  93. }

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. 

  1. /**
  2.   * Retrieves comments for a thread, sorted in an order suitable for display.
  3.   *
  4.   * @param \Drupal\Core\Entity\EntityInterface $entity
  5.   * The entity whose comment(s) needs rendering.
  6.   * @param string $field_name
  7.   * The field_name whose comment(s) needs rendering.
  8.   * @param int $mode
  9.   * The comment display mode: CommentManagerInterface::COMMENT_MODE_FLAT or
  10.   * CommentManagerInterface::COMMENT_MODE_THREADED.
  11.   * @param int $comments_per_page
  12.   * (optional) The amount of comments to display per page.
  13.   * Defaults to 0, which means show all comments.
  14.   * @param int $pager_id
  15.   * (optional) Pager id to use in case of multiple pagers on the one page.
  16.   * Defaults to 0; is only used when $comments_per_page is greater than zero.
  17.   *
  18.   * @return array
  19.   * Ordered array of comment objects, keyed by comment id.
  20.   */
  21. public function loadComments(EntityInterface $entity, $field_name, $mode, $comments_per_page = 0, $pager_id = 0) {
  22. $query = $this->database->select('comment_field_data', 'c');
  23. $query->addField('c', 'cid');
  24. $query
  25. ->condition('c.entity_id', $entity->id())
  26. ->condition('c.entity_type', $entity->getEntityTypeId())
  27. ->condition('c.field_name', $field_name)
  28. ->condition('c.default_langcode', 1)
  29. ->addTag('entity_access')
  30. ->addTag('comment_filter')
  31. ->addMetaData('base_table', 'comment')
  32. ->addMetaData('entity', $entity)
  33. ->addMetaData('field_name', $field_name);
  34.  
  35. if ($comments_per_page) {
  36. $query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender')
  37. ->limit($comments_per_page);
  38. if ($pager_id) {
  39. $query->element($pager_id);
  40. }
  41.  
  42. $count_query = $this->database->select('comment_field_data', 'c');
  43. $count_query->addExpression('COUNT(*)');
  44. $count_query
  45. ->condition('c.entity_id', $entity->id())
  46. ->condition('c.entity_type', $entity->getEntityTypeId())
  47. ->condition('c.field_name', $field_name)
  48. ->condition('c.default_langcode', 1)
  49. ->addTag('entity_access')
  50. ->addTag('comment_filter')
  51. ->addMetaData('base_table', 'comment')
  52. ->addMetaData('entity', $entity)
  53. ->addMetaData('field_name', $field_name);
  54. $query->setCountQuery($count_query);
  55. }
  56.  
  57. if (!$this->currentUser->hasPermission('administer comments')) {
  58. $query->condition('c.status', CommentInterface::PUBLISHED);
  59. if ($comments_per_page) {
  60. $count_query->condition('c.status', CommentInterface::PUBLISHED);
  61. }
  62. }
  63.  
  64. $query->orderBy('c.created', 'ASC');
  65.  
  66. $cids = $query->execute()->fetchCol();
  67.  
  68. $comments = [];
  69. if ($cids) {
  70. $comments = $this->storage->loadMultiple($cids);
  71. }
  72.  
  73. return $comments;
  74. }

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.

Add new comment

The content of this field is kept private and will not be shown publicly.