/** * #post_render_cache callback; replaces placeholder with a dynamic thing. * * @param array $element * The renderable array that contains the to be replaced placeholder. * @param array $context * An array with the following keys: * - thing: a "thing" string * * @return array * A renderable array containing the comment form. */ public static function renderDynamicThing(array $element, array $context) { $callback = '\\Drupal\\filter_test\\Plugin\\Filter\\FilterTestPostRenderCache::renderDynamicThing'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $markup = format_string('This is a dynamic @thing.', array('@thing' => $context['thing'])); $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); return $element; }
/** * Overrides Drupal\Core\Entity\EntityViewBuilder::buildComponents(). * * In addition to modifying the content key on entities, this implementation * will also set the comment entity key which all comments carry. * * @throws \InvalidArgumentException * Thrown when a comment is attached to an entity that no longer exists. */ public function buildComponents(array &$build, array $entities, array $displays, $view_mode, $langcode = NULL) { /** @var \Drupal\comment\CommentInterface[] $entities */ if (empty($entities)) { return; } // Pre-load associated users into cache to leverage multiple loading. $uids = array(); foreach ($entities as $entity) { $uids[] = $entity->getOwnerId(); } $this->entityManager->getStorage('user')->loadMultiple(array_unique($uids)); parent::buildComponents($build, $entities, $displays, $view_mode, $langcode); // Load all the entities that have comments attached. $commented_entity_ids = array(); $commented_entities = array(); foreach ($entities as $entity) { $commented_entity_ids[$entity->getCommentedEntityTypeId()][] = $entity->getCommentedEntityId(); } // Load entities in bulk. This is more performant than using // $comment->getCommentedEntity() as we can load them in bulk per type. foreach ($commented_entity_ids as $entity_type => $entity_ids) { $commented_entities[$entity_type] = $this->entityManager->getStorage($entity_type)->loadMultiple($entity_ids); } foreach ($entities as $id => $entity) { if (isset($commented_entities[$entity->getCommentedEntityTypeId()][$entity->getCommentedEntityId()])) { $commented_entity = $commented_entities[$entity->getCommentedEntityTypeId()][$entity->getCommentedEntityId()]; } else { throw new \InvalidArgumentException(t('Invalid entity for comment.')); } $build[$id]['#entity'] = $entity; $build[$id]['#theme'] = 'comment__' . $entity->getFieldName() . '__' . $commented_entity->bundle(); $display = $displays[$entity->bundle()]; if ($display->getComponent('links')) { $callback = 'comment.post_render_cache:renderLinks'; $context = array('comment_entity_id' => $entity->id(), 'view_mode' => $view_mode, 'langcode' => $langcode, 'commented_entity_type' => $commented_entity->getEntityTypeId(), 'commented_entity_id' => $commented_entity->id(), 'in_preview' => !empty($entity->in_preview)); $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $build[$id]['links'] = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder); } if (!isset($build[$id]['#attached'])) { $build[$id]['#attached'] = array(); } $build[$id]['#attached']['library'][] = 'comment/drupal.comment-by-viewer'; if ($this->moduleHandler->moduleExists('history') && \Drupal::currentUser()->isAuthenticated()) { $build[$id]['#attached']['library'][] = 'comment/drupal.comment-new-indicator'; // Embed the metadata for the comment "new" indicators on this node. $build[$id]['#post_render_cache']['history_attach_timestamp'] = array(array('node_id' => $commented_entity->id())); } } }
/** * #post_render_cache callback; replaces placeholder with comment form. * * @param array $element * The renderable array that contains the to be replaced placeholder. * @param array $context * An array with the following keys: * - entity_type: an entity type * - entity_id: an entity ID * - field_name: a comment field name * * @return array * A renderable array containing the comment form. */ public function renderForm(array $element, array $context) { $field_name = $context['field_name']; $entity = $this->entityManager->getStorage($context['entity_type'])->load($context['entity_id']); $field_storage = FieldStorageConfig::loadByName($entity->getEntityTypeId(), $field_name); $values = array('entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'field_name' => $field_name, 'comment_type' => $field_storage->getSetting('bundle'), 'pid' => NULL); $comment = $this->entityManager->getStorage('comment')->create($values); $form = $this->entityFormBuilder->getForm($comment); $markup = drupal_render($form); $callback = 'comment.post_render_cache:renderForm'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); $element['#attached'] = drupal_merge_attached($element['#attached'], $form['#attached']); return $element; }
/** * #post_render_cache callback; replaces the placeholder with node links. * * Renders the links on a node. * * @param array $element * The renderable array that contains the to be replaced placeholder. * @param array $context * An array with the following keys: * - node_entity_id: a node entity ID * - view_mode: the view mode in which the node entity is being viewed * - langcode: in which language the node entity is being viewed * - in_preview: whether the node is currently being previewed * * @return array * A renderable array representing the node links. */ public static function renderLinks(array $element, array $context) { $callback = get_called_class() . '::renderLinks'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $links = array('#theme' => 'links__node', '#pre_render' => array('drupal_pre_render_links'), '#attributes' => array('class' => array('links', 'inline'))); if (!$context['in_preview']) { $entity = entity_load('node', $context['node_entity_id'])->getTranslation($context['langcode']); $links['node'] = static::buildLinks($entity, $context['view_mode']); // Allow other modules to alter the node links. $hook_context = array('view_mode' => $context['view_mode'], 'langcode' => $context['langcode']); \Drupal::moduleHandler()->alter('node_links', $links, $entity, $hook_context); } $markup = drupal_render($links); $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); return $element; }
/** * #post_render_cache callback; replaces placeholder with comment form. * * @param array $element * The renderable array that contains the to be replaced placeholder. * @param array $context * An array with the following keys: * - entity_type: an entity type * - entity_id: an entity ID * - field_name: a comment field name * * @return array * A renderable array containing the comment form. */ public function renderForm(array $element, array $context) { $field_name = $context['field_name']; $entity = $this->entityManager->getStorage($context['entity_type'])->load($context['entity_id']); $field_storage = FieldStorageConfig::loadByName($entity->getEntityTypeId(), $field_name); $values = array('entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'field_name' => $field_name, 'comment_type' => $field_storage->getSetting('bundle'), 'pid' => NULL); $comment = $this->entityManager->getStorage('comment')->create($values); $form = $this->entityFormBuilder->getForm($comment); // @todo: This only works as long as assets are still tracked in a global // static variable, see https://drupal.org/node/2238835 $markup = drupal_render($form, TRUE); $callback = 'comment.post_render_cache:renderForm'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); return $element; }
/** * Wraps drupal_render_cache_generate_placeholder(). */ protected function generatePlaceholder($callback, $context) { return drupal_render_cache_generate_placeholder($callback, $context); }
/** * Tests post-render cache-integrated 'render_cache_placeholder' child * element. */ function testDrupalRenderChildElementRenderCachePlaceholder() { $container = array('#type' => 'container'); $context = array('bar' => $this->randomContextValue()); $callback = 'common_test_post_render_cache_placeholder'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $test_element = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder, '#prefix' => '<foo>', '#suffix' => '</foo>'); $container['test_element'] = $test_element; $expected_output = '<div><foo><bar>' . $context['bar'] . '</bar></foo></div>' . "\n"; // #cache disabled. drupal_static_reset('_drupal_add_js'); $element = $container; $output = drupal_render($element); $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.'); // The cache system is turned off for POST requests. $request_method = \Drupal::request()->getMethod(); \Drupal::request()->setMethod('GET'); // GET request: #cache enabled, cache miss. drupal_static_reset('_drupal_add_js'); $element = $container; $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET'); $element['test_element']['#cache'] = array('cid' => 'render_cache_placeholder_test_child_GET'); // Simulate element rendering in a template, where sub-items of a renderable // can be sent to drupal_render() before the parent. $child =& $element['test_element']; $element['#children'] = drupal_render($child, TRUE); // Eventually, drupal_render() gets called on the root element. $output = drupal_render($element); $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); $this->assertTrue(isset($element['#printed']), 'No cache hit'); $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.'); // GET request: validate cached data for child element. $child_tokens = $element['test_element']['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token']; $parent_tokens = $element['#post_render_cache']['common_test_post_render_cache_placeholder'][0]['token']; $expected_token = $child_tokens; $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET')); $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data; // Parse unique token out of the cached markup. $dom = Html::load($cached_element['#markup']); $xpath = new \DOMXPath($dom); $nodes = $xpath->query('//*[@token]'); $this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup'); $token = ''; if ($nodes->length) { $token = $nodes->item(0)->getAttribute('token'); } $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element'); // Verify the token is in the cached element. $expected_element = array('#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered' => TRUE))); $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); // GET request: validate cached data (for the parent/entire render array). $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_GET')); $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data; // Parse unique token out of the cached markup. $dom = Html::load($cached_element['#markup']); $xpath = new \DOMXPath($dom); $nodes = $xpath->query('//*[@token]'); $this->assertTrue($nodes->length, 'The token attribute was found in the cached parent element markup'); $token = ''; if ($nodes->length) { $token = $nodes->item(0)->getAttribute('token'); } $this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element'); // Verify the token is in the cached element. $expected_element = array('#markup' => '<div><foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></foo></div>' . "\n", '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered' => TRUE))); $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the parent element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); // GET request: validate cached data. // Check the cache of the child element again after the parent has been // rendered. $element = array('#cache' => array('cid' => 'render_cache_placeholder_test_child_GET')); $cached_element = \Drupal::cache('render')->get(drupal_render_cid_create($element))->data; // Verify that the child element contains the correct // render_cache_placeholder markup. $expected_token = $child_tokens; $dom = Html::load($cached_element['#markup']); $xpath = new \DOMXPath($dom); $nodes = $xpath->query('//*[@token]'); $this->assertTrue($nodes->length, 'The token attribute was found in the cached child element markup'); $token = ''; if ($nodes->length) { $token = $nodes->item(0)->getAttribute('token'); } $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element'); // Verify the token is in the cached element. $expected_element = array('#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="' . $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array('common_test_post_render_cache_placeholder' => array($context)), '#cache' => array('tags' => array('rendered' => TRUE))); $this->assertIdentical($cached_element, $expected_element, 'The correct data is cached for the child element: the stored #markup and #attached properties are not affected by #post_render_cache callbacks.'); // GET request: #cache enabled, cache hit. drupal_static_reset('_drupal_add_js'); $element = $container; $element['#cache'] = array('cid' => 'render_cache_placeholder_test_GET'); // Simulate element rendering in a template, where sub-items of a renderable // can be sent to drupal_render before the parent. $child =& $element['test_element']; $element['#children'] = drupal_render($child, TRUE); $output = drupal_render($element); $this->assertIdentical($output, $expected_output, 'Placeholder was replaced in output'); $this->assertFalse(isset($element['#printed']), 'Cache hit'); $this->assertIdentical($element['#markup'], $expected_output, 'Placeholder was replaced in #markup.'); $settings = $this->parseDrupalSettings(drupal_get_js()); $this->assertIdentical($settings['common_test'], $context, '#attached is modified; JavaScript setting is added to page.'); // Restore the previous request method. \Drupal::request()->setMethod($request_method); }
/** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items) { $elements = array(); $output = array(); $field_name = $this->fieldDefinition->getName(); $entity = $items->getEntity(); $status = $items->status; if ($status != CommentItemInterface::HIDDEN && empty($entity->in_preview) && !in_array($this->viewMode, array('search_result', 'search_index'))) { $comment_settings = $this->getFieldSettings(); // Only attempt to render comments if the entity has visible comments. // Unpublished comments are not included in // $entity->get($field_name)->comment_count, but unpublished comments // should display if the user is an administrator. if ($entity->get($field_name)->comment_count && $this->currentUser->hasPermission('access comments') || $this->currentUser->hasPermission('administer comments')) { $mode = $comment_settings['default_mode']; $comments_per_page = $comment_settings['per_page']; $comments = $this->storage->loadThread($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id')); if ($comments) { comment_prepare_thread($comments); $build = $this->viewBuilder->viewMultiple($comments); $build['pager']['#theme'] = 'pager'; if ($this->getSetting('pager_id')) { $build['pager']['#element'] = $this->getSetting('pager_id'); } // The viewElements() method of entity field formatters is run // during the #pre_render phase of rendering an entity. A formatter // builds the content of the field in preparation for theming. // All entity cache tags must be available after the #pre_render phase. // This field formatter is highly exceptional: it renders *another* // entity and this referenced entity has its own #pre_render // callbacks. In order collect the cache tags associated with the // referenced entity it must be passed to drupal_render() so that its // #pre_render callbacks are invoked and its full build array is // assembled. Rendering the referenced entity in place here will allow // its cache tags to be bubbled up and included with those of the // main entity when cache tags are collected for a renderable array // in drupal_render(). drupal_render($build, TRUE); $output['comments'] = $build; } } // Append comment form if the comments are open and the form is set to // display below the entity. Do not show the form for the print view mode. if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == COMMENT_FORM_BELOW && $this->viewMode != 'print') { // Only show the add comment form if the user has permission. if ($this->currentUser->hasPermission('post comments')) { // All users in the "anonymous" role can use the same form: it is fine // for this form to be stored in the render cache. if ($this->currentUser->isAnonymous()) { $comment = $this->storage->create(array('entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'field_name' => $field_name, 'comment_type' => $this->getFieldSetting('comment_type'), 'pid' => NULL)); $output['comment_form'] = $this->entityFormBuilder->getForm($comment); } else { $callback = 'comment.post_render_cache:renderForm'; $context = array('entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'field_name' => $field_name); $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $output['comment_form'] = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder); } } } $elements[] = $output + array('#comment_type' => $this->getFieldSetting('comment_type'), '#comment_display_mode' => $this->getFieldSetting('default_mode'), 'comments' => array(), 'comment_form' => array()); } return $elements; }
/** * #post_render_cache callback for testDrupalRenderBubbling(). */ public static function bubblingPostRenderCache(array $element, array $context) { $callback = 'Drupal\\system\\Tests\\Common\\RenderTest::bubblingPostRenderCache'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $element['#markup'] = str_replace($placeholder, 'Post-render cache!' . $context['foo'] . $context['baz'], $element['#markup']); return $element; }
/** * {@inheritdoc} * * In addition to modifying the content key on entities, this implementation * will also set the comment entity key which all comments carry. * * @throws \InvalidArgumentException * Thrown when a comment is attached to an entity that no longer exists. */ public function buildComponents(array &$build, array $entities, array $displays, $view_mode, $langcode = NULL) { /** @var \Drupal\comment\CommentInterface[] $entities */ if (empty($entities)) { return; } // Pre-load associated users into cache to leverage multiple loading. $uids = array(); foreach ($entities as $entity) { $uids[] = $entity->getOwnerId(); } $this->entityManager->getStorage('user')->loadMultiple(array_unique($uids)); parent::buildComponents($build, $entities, $displays, $view_mode, $langcode); // A counter to track the indentation level. $current_indent = 0; foreach ($entities as $id => $entity) { if ($build[$id]['#comment_threaded']) { $comment_indent = count(explode('.', $entity->getThread())) - 1; if ($comment_indent > $current_indent) { // Set 1 to indent this comment from the previous one (its parent). // Set only one extra level of indenting even if the difference in // depth is higher. $build[$id]['#comment_indent'] = 1; $current_indent++; } else { // Set zero if this comment is on the same level as the previous one // or negative value to point an amount indents to close. $build[$id]['#comment_indent'] = $comment_indent - $current_indent; $current_indent = $comment_indent; } } // Commented entities already loaded after self::getBuildDefaults(). $commented_entity = $entity->getCommentedEntity(); $build[$id]['#entity'] = $entity; $build[$id]['#theme'] = 'comment__' . $entity->getFieldName() . '__' . $commented_entity->bundle(); $display = $displays[$entity->bundle()]; if ($display->getComponent('links')) { $callback = 'comment.post_render_cache:renderLinks'; $context = array('comment_entity_id' => $entity->id(), 'view_mode' => $view_mode, 'langcode' => $langcode, 'commented_entity_type' => $commented_entity->getEntityTypeId(), 'commented_entity_id' => $commented_entity->id(), 'in_preview' => !empty($entity->in_preview)); $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $build[$id]['links'] = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder); } if (!isset($build[$id]['#attached'])) { $build[$id]['#attached'] = array(); } $build[$id]['#attached']['library'][] = 'comment/drupal.comment-by-viewer'; if ($this->moduleHandler->moduleExists('history') && $this->currentUser->isAuthenticated()) { $build[$id]['#attached']['library'][] = 'comment/drupal.comment-new-indicator'; // Embed the metadata for the comment "new" indicators on this node. $build[$id]['#post_render_cache']['history_attach_timestamp'] = array(array('node_id' => $commented_entity->id())); } } if ($build[$id]['#comment_threaded']) { // The final comment must close up some hanging divs. $build[$id]['#comment_indent_final'] = $current_indent; } }
/** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items) { $elements = array(); $output = array(); $field_name = $this->fieldDefinition->getName(); $entity = $items->getEntity(); $status = $items->status; if ($status != CommentItemInterface::HIDDEN && empty($entity->in_preview) && !in_array($this->viewMode, array('search_result', 'search_index'))) { $comment_settings = $this->getFieldSettings(); // Only attempt to render comments if the entity has visible comments. // Unpublished comments are not included in // $entity->get($field_name)->comment_count, but unpublished comments // should display if the user is an administrator. $elements['#cache']['contexts'][] = 'user.permissions'; if ($this->currentUser->hasPermission('access comments') || $this->currentUser->hasPermission('administer comments')) { // This is a listing of Comment entities, so associate its list cache // tag for correct invalidation. $output['comments']['#cache']['tags'] = $this->entityManager->getDefinition('comment')->getListCacheTags(); if ($entity->get($field_name)->comment_count || $this->currentUser->hasPermission('administer comments')) { $mode = $comment_settings['default_mode']; $comments_per_page = $comment_settings['per_page']; $comments = $this->storage->loadThread($entity, $field_name, $mode, $comments_per_page, $this->getSetting('pager_id')); if ($comments) { $build = $this->viewBuilder->viewMultiple($comments); $build['pager']['#type'] = 'pager'; if ($this->getSetting('pager_id')) { $build['pager']['#element'] = $this->getSetting('pager_id'); } $output['comments'] += $build; } } } // Append comment form if the comments are open and the form is set to // display below the entity. Do not show the form for the print view mode. if ($status == CommentItemInterface::OPEN && $comment_settings['form_location'] == CommentItemInterface::FORM_BELOW && $this->viewMode != 'print') { // Only show the add comment form if the user has permission. $elements['#cache']['contexts'][] = 'user.roles'; if ($this->currentUser->hasPermission('post comments')) { // All users in the "anonymous" role can use the same form: it is fine // for this form to be stored in the render cache. if ($this->currentUser->isAnonymous()) { $comment = $this->storage->create(array('entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'field_name' => $field_name, 'comment_type' => $this->getFieldSetting('comment_type'), 'pid' => NULL)); $output['comment_form'] = $this->entityFormBuilder->getForm($comment); } else { $callback = 'comment.post_render_cache:renderForm'; $context = array('entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'field_name' => $field_name, 'comment_type' => $this->getFieldSetting('comment_type')); $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $output['comment_form'] = array('#post_render_cache' => array($callback => array($context)), '#markup' => $placeholder); } } } $elements[] = $output + array('#comment_type' => $this->getFieldSetting('comment_type'), '#comment_display_mode' => $this->getFieldSetting('default_mode'), 'comments' => array(), 'comment_form' => array()); } return $elements; }
/** * #post_render_cache callback; replaces the placeholder with comment links. * * Renders the links on a comment. * * @param array $element * The renderable array that contains the to be replaced placeholder. * @param array $context * An array with the following keys: * - comment_entity_id: a comment entity ID * - view_mode: the view mode in which the comment entity is being viewed * - langcode: in which language the comment entity is being viewed * - commented_entity_type: the entity type to which the comment is attached * - commented_entity_id: the entity ID to which the comment is attached * - in_preview: whether the comment is currently being previewed * * @return array * A renderable array representing the comment links. */ public static function renderLinks(array $element, array $context) { $callback = '\\Drupal\\comment\\CommentViewBuilder::renderLinks'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); $links = array('#theme' => 'links__comment', '#pre_render' => array('drupal_pre_render_links'), '#attributes' => array('class' => array('links', 'inline'))); if (!$context['in_preview']) { $entity = entity_load('comment', $context['comment_entity_id']); $commented_entity = entity_load($context['commented_entity_type'], $context['commented_entity_id']); $links['comment'] = self::buildLinks($entity, $commented_entity); // Allow other modules to alter the comment links. $hook_context = array('view_mode' => $context['view_mode'], 'langcode' => $context['langcode'], 'commented_entity' => $commented_entity); \Drupal::moduleHandler()->alter('comment_links', $links, $entity, $hook_context); } $markup = drupal_render($links); $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); return $element; }