/** * Ensures correct entity URLs with the method language-content-entity enabled. * * Test case with the method language-content-entity enabled and configured * with higher and also with lower priority than the method language-url. */ public function testEntityUrlLanguageWithLanguageContentEnabled() { // Define the method language-content-entity with a higher priority than // language-url. $config = $this->config('language.types'); $config->set('configurable', [LanguageInterface::TYPE_INTERFACE, LanguageInterface::TYPE_CONTENT]); $config->set('negotiation.language_content.enabled', [LanguageNegotiationContentEntity::METHOD_ID => 0, LanguageNegotiationUrl::METHOD_ID => 1]); $config->save(); // Without being on an content entity route the default entity URL tests // should still pass. $this->testEntityUrlLanguage(); // Now switching to an entity route, so that the URL links are generated // while being on an entity route. $this->setCurrentRequestForRoute('/entity_test/{entity_test}', 'entity.entity_test.canonical'); // The method language-content-entity should run before language-url and // append query parameter for the content language and prevent language-url // from overwriting the url. $this->assertTrue(strpos($this->entity->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=en') !== FALSE); $this->assertTrue(strpos($this->entity->getTranslation('es')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=es') !== FALSE); $this->assertTrue(strpos($this->entity->getTranslation('fr')->urlInfo('canonical')->toString(), '/en/entity_test/' . $this->entity->id() . '?' . LanguageNegotiationContentEntity::QUERY_PARAMETER . '=fr') !== FALSE); // Define the method language-url with a higher priority than // language-content-entity. This configuration should match the default one, // where the language-content-entity is turned off. $config->set('negotiation.language_content.enabled', [LanguageNegotiationUrl::METHOD_ID => 0, LanguageNegotiationContentEntity::METHOD_ID => 1]); $config->save(); // The default entity URL tests should pass again with the current // configuration. $this->testEntityUrlLanguage(); }
/** * Will test the redirects. */ public function testRedirects() { // Test alias normalization. $this->config->set('normalize_aliases', TRUE)->save(); $this->assertRedirect('node/' . $this->node->id(), 'test-node'); $this->assertRedirect('Test-node', 'test-node'); $this->config->set('normalize_aliases', FALSE)->save(); $this->assertRedirect('node/' . $this->node->id(), NULL, 'HTTP/1.1 200 OK'); $this->assertRedirect('Test-node', NULL, 'HTTP/1.1 200 OK'); // Test deslashing. $this->config->set('deslash', TRUE)->save(); $this->assertRedirect('test-node/', 'test-node'); $this->config->set('deslash', FALSE)->save(); $this->assertRedirect('test-node/', NULL, 'HTTP/1.1 200 OK'); // Test front page redirects. $this->config->set('frontpage_redirect', TRUE)->save(); $this->config('system.site')->set('page.front', '/node')->save(); $this->assertRedirect('node', '<front>'); // Test front page redirects with an alias. \Drupal::service('path.alias_storage')->save('/node', '/node-alias'); $this->assertRedirect('node-alias', '<front>'); $this->config->set('frontpage_redirect', FALSE)->save(); $this->assertRedirect('node', NULL, 'HTTP/1.1 200 OK'); $this->assertRedirect('node-alias', NULL, 'HTTP/1.1 200 OK'); // Test post request. $this->config->set('normalize_aliases', TRUE)->save(); $this->drupalPost('Test-node', 'application/json', array()); // Does not do a redirect, stays in the same path. $this->assertEqual(basename($this->getUrl()), 'Test-node'); // Test the access checking. $this->config->set('normalize_aliases', TRUE)->save(); $this->config->set('access_check', TRUE)->save(); $this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 403 Forbidden'); $this->config->set('access_check', FALSE)->save(); // @todo - here it seems that the access check runs prior to our redirecting // check why so and enable the test. //$this->assertRedirect('admin/config/system/site-information', 'site-info'); // Login as user with admin privileges. $this->drupalLogin($this->adminUser); // Test ignoring admin paths. $this->config->set('ignore_admin_path', FALSE)->save(); $this->assertRedirect('admin/config/system/site-information', 'site-info'); $this->config->set('ignore_admin_path', TRUE)->save(); $this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 200 OK'); }
/** * {@inheritdoc} */ protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { // The anonymous user account is saved with the fixed user ID of 0. // Therefore we need to check for NULL explicitly. if ($entity->id() === NULL) { $entity->uid->value = $this->database->nextId($this->database->query('SELECT MAX(uid) FROM {users}')->fetchField()); $entity->enforceIsNew(); } return parent::doSaveFieldItems($entity, $names); }
protected function getAddLink(ContentEntityInterface $host) { $link = ''; if ($host->access('update', \Drupal::currentUser())) { $link = '<ul class="action-links action-links-field-collection-add"><li>'; $link .= \Drupal::l(t('Add'), Url::FromRoute('field_collection_item.add_page', ['field_collection' => $this->fieldDefinition->getName(), 'host_type' => $host->getEntityTypeId(), 'host_id' => $host->id()])); $link .= '</li></ul>'; } return $link; }
/** * {@inheritdoc} */ public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) { $translations = $entity->getTranslationLanguages(); $field_type_manager = \Drupal::service('plugin.manager.field.field_type'); // If we have no information about what to sync to, if we are creating a new // entity, if we have no translations for the current entity and we are not // creating one, then there is nothing to synchronize. if (empty($sync_langcode) || $entity->isNew() || count($translations) < 2) { return; } // If the entity language is being changed there is nothing to synchronize. $entity_type = $entity->getEntityTypeId(); $entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id()); if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) { return; } /** @var \Drupal\Core\Field\FieldItemListInterface $items */ foreach ($entity as $field_name => $items) { $field_definition = $items->getFieldDefinition(); $field_type_definition = $field_type_manager->getDefinition($field_definition->getType()); $column_groups = $field_type_definition['column_groups']; // Sync if the field is translatable, not empty, and the synchronization // setting is enabled. if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && ($translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync'))) { // Retrieve all the untranslatable column groups and merge them into // single list. $groups = array_keys(array_diff($translation_sync, array_filter($translation_sync))); if (!empty($groups)) { $columns = array(); foreach ($groups as $group) { $info = $column_groups[$group]; // A missing 'columns' key indicates we have a single-column group. $columns = array_merge($columns, isset($info['columns']) ? $info['columns'] : array($group)); } if (!empty($columns)) { $values = array(); foreach ($translations as $langcode => $language) { $values[$langcode] = $entity->getTranslation($langcode)->get($field_name)->getValue(); } // If a translation is being created, the original values should be // used as the unchanged items. In fact there are no unchanged items // to check against. $langcode = $original_langcode ?: $sync_langcode; $unchanged_items = $entity_unchanged->getTranslation($langcode)->get($field_name)->getValue(); $this->synchronizeItems($values, $unchanged_items, $sync_langcode, array_keys($translations), $columns); foreach ($translations as $langcode => $language) { $entity->getTranslation($langcode)->get($field_name)->setValue($values[$langcode]); } } } } } }
/** * {@inheritdoc} */ protected function save(ContentEntityInterface $entity, array $old_destination_id_values = array()) { // Do not overwrite the root account password. if ($entity->id() != 1) { // Set the pre_hashed password so that the PasswordItem field does not hash // already hashed passwords. If the md5_passwords configuration option is // set we need to rehash the password and prefix with a U. // @see \Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem::preSave() $entity->pass->pre_hashed = TRUE; if (isset($this->configuration['md5_passwords'])) { $entity->pass->value = 'U' . $this->password->hash($entity->pass->value); } } return parent::save($entity, $old_destination_id_values); }
/** * Checks whether field languages are correctly stored for the given entity. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity fields are attached to. * @param string $message * (optional) A message to display with the assertion. */ protected function assertFieldStorageLangcode(ContentEntityInterface $entity, $message = '') { $status = TRUE; $entity_type = $entity->getEntityTypeId(); $id = $entity->id(); $langcode = $entity->getUntranslated()->language()->id; $fields = array($this->field_name, $this->untranslatable_field_name); foreach ($fields as $field_name) { $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name); $tables = array(ContentEntityDatabaseStorage::_fieldTableName($field_storage), ContentEntityDatabaseStorage::_fieldRevisionTableName($field_storage)); foreach ($tables as $table) { $record = \Drupal::database()->select($table, 'f')->fields('f')->condition('f.entity_id', $id)->condition('f.revision_id', $id)->execute()->fetchObject(); if ($record->langcode != $langcode) { $status = FALSE; break; } } } return $this->assertTrue($status, $message); }
/** * Getter for the entity id. * * @return int mixed * The entity id. */ public function getEntityId() { return $this->entity->id(); }
protected function sortById(ContentEntityInterface $leftEntity, ContentEntityInterface $rightEntity) { $leftId = (int) $leftEntity->id(); $rightId = (int) $rightEntity->id(); return $leftId < $rightId ? -1 : 1; }
/** * Check if the supplied entity layout has any content blocks. * * @param EntityLayoutInterface $entity_layout * The entity layout to check content blocks for. * * @return bool */ public function hasContentBlocks(EntityLayoutInterface $entity_layout, ContentEntityInterface $entity) { return (bool) $this->entityTypeManager->getStorage('entity_layout_block')->getQuery()->condition('entity_id', $entity->id())->condition('layout', $entity_layout->id())->count()->execute(); }
/** * {@inheritdoc} */ public function create(ContentEntityInterface $entity, $fields) { $query = $this->database->insert('comment_entity_statistics')->fields(array('entity_id', 'entity_type', 'field_name', 'cid', 'last_comment_timestamp', 'last_comment_name', 'last_comment_uid', 'comment_count')); foreach ($fields as $field_name => $detail) { // Skip fields that entity does not have. if (!$entity->hasField($field_name)) { continue; } // Get the user ID from the entity if it's set, or default to the // currently logged in user. $last_comment_uid = 0; if ($entity instanceof EntityOwnerInterface) { $last_comment_uid = $entity->getOwnerId(); } if (!isset($last_comment_uid)) { // Default to current user when entity does not implement // EntityOwnerInterface or author is not set. $last_comment_uid = $this->currentUser->id(); } // Default to REQUEST_TIME when entity does not have a changed property. $last_comment_timestamp = REQUEST_TIME; if ($entity instanceof EntityChangedInterface) { $last_comment_timestamp = $entity->getChangedTime(); } $query->values(array('entity_id' => $entity->id(), 'entity_type' => $entity->getEntityTypeId(), 'field_name' => $field_name, 'cid' => 0, 'last_comment_timestamp' => $last_comment_timestamp, 'last_comment_name' => NULL, 'last_comment_uid' => $last_comment_uid, 'comment_count' => 0)); } $query->execute(); }
/** * @param \Drupal\Core\Entity\ContentEntityInterface $entity * @return array */ protected function buildRecord(ContentEntityInterface $entity) { return array('entity_type_id' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'entity_uuid' => $entity->uuid(), 'revision_id' => $entity->getRevisionId(), 'deleted' => $entity->_deleted->value, 'rev' => $entity->_rev->value, 'seq' => $this->multiversionManager->newSequenceId(), 'local' => (bool) $entity->getEntityType()->get('local'), 'is_stub' => (bool) $entity->_rev->is_stub); }
/** * Check an entity value after reload. * * @param $entity_type_id * @param \Drupal\Core\Entity\ContentEntityInterface $entity * @param $field * @param $value */ protected function checkEntityValue($entity_type_id, ContentEntityInterface $entity, $field, $value) { $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); $storage->resetCache([$entity->id()]); $updated_entity = $storage->load($entity->id()); $this->assertEqual($updated_entity->get($field)->value, 1, $entity->label() . " $field = $value"); }
/** * Detect whether an entity is already reference within an ancestry ER array. * * @var \Drupal\Core\Entity\ContentEntityInterface $discussion * A discussion node entity. * @var array $ancestry * A discussion node entity, coming from field_ancestry->getValue(). */ protected function withinAncestry(ContentEntityInterface $entity, $ancestry) { $detected = FALSE; foreach ($ancestry as $ancestor) { if ($entity->id() == $ancestor['target_id']) { $detected = TRUE; } } return $detected; }
/** * Counts the number of revisions in the default language. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity. * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage * The entity storage. * * @return int * The number of revisions in the default language. */ protected function countDefaultLanguageRevisions(ContentEntityInterface $entity, EntityStorageInterface $entity_storage) { $entity_type = $entity->getEntityType(); $count = $entity_storage->getQuery()->allRevisions()->condition($entity_type->getKey('id'), $entity->id())->condition($entity_type->getKey('default_langcode'), 1)->count()->execute(); return $count; }
/** * Generate the HTML for our entity. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity we're rendering. * @param bool $use_default_css * TRUE if we should inject our default CSS otherwise FALSE. * @param bool $optimize_css * TRUE if we should compress the CSS otherwise FALSE. * * @return string * The generated HTML. * * @throws \Exception */ protected function getHtml(ContentEntityInterface $entity, $use_default_css, $optimize_css) { $render_controller = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId()); $render = ['#theme' => 'entity_print__' . $entity->getEntityTypeId() . '__' . $entity->id(), '#entity' => $entity, '#entity_array' => $render_controller->view($entity, 'pdf'), '#attached' => []]; // Inject some generic CSS across all templates. if ($use_default_css) { $render['#attached']['library'][] = 'entity_print/default'; } // Allow other modules to add their own CSS. $this->moduleHandler->alter('entity_print_css', $render, $entity); // Inject CSS from the theme info files and then render the CSS. $render = $this->addCss($render, $entity); $css_assets = $this->assetResolver->getCssAssets(AttachedAssets::createFromRenderArray($render), $optimize_css); $rendered_css = $this->cssRenderer->render($css_assets); $render['#entity_print_css'] = $this->renderer->render($rendered_css); return $this->renderer->render($render); }
/** * Builds a table row for overview form. * * @param array ContentEntityInterface $entity * Data needed to build the list row. * @param array $bundles * The array of bundles. * * @return array */ public function overviewRow(ContentEntityInterface $entity, array $bundles) { $label = $entity->label() ?: $this->t('@type: @id', array('@type' => $entity->getEntityTypeId(), '@id' => $entity->id())); // Get existing translations and current job items for the entity // to determine translation statuses $translations = $entity->getTranslationLanguages(); $source_lang = $entity->language()->getId(); $current_job_items = tmgmt_job_item_load_latest('content', $entity->getEntityTypeId(), $entity->id(), $source_lang); $row = array('id' => $entity->id(), 'title' => $entity->hasLinkTemplate('canonical') ? $entity->toLink($label, 'canonical')->toString() : ($entity->label() ?: $entity->id())); if (count($bundles) > 1) { $row['bundle'] = isset($bundles[$entity->bundle()]) ? $bundles[$entity->bundle()] : t('Unknown'); } // Load entity translation specific data. $manager = \Drupal::service('content_translation.manager'); foreach (\Drupal::languageManager()->getLanguages() as $langcode => $language) { $translation_status = 'current'; if ($langcode == $source_lang) { $translation_status = 'original'; } elseif (!isset($translations[$langcode])) { $translation_status = 'missing'; } elseif ($translation = $entity->getTranslation($langcode)) { $metadata = $manager->getTranslationMetadata($translation); if ($metadata->isOutdated()) { $translation_status = 'outofdate'; } } $build = $this->buildTranslationStatus($translation_status, isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL); $row['langcode-' . $langcode] = ['data' => \Drupal::service('renderer')->render($build), 'class' => array('langstatus-' . $langcode)]; } return $row; }
/** * {@inheritdoc} */ public function isLatestRevision(ContentEntityInterface $entity) { return $entity->getRevisionId() == $this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()); }
/** * Save the entity. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The content entity. * @param array $old_destination_id_values * An array of destination id values. * * @return array * An array containing the entity id. */ protected function save(ContentEntityInterface $entity, array $old_destination_id_values = array()) { $entity->save(); return array($entity->id()); }
/** * Get all previous revisions that have updates of the attached type. * * This function would be easier and more performant if this core issue with * Entity Query was fixed: https://www.drupal.org/node/2649268 Without this * fix can't filter query on type of update and whether they are active. So * therefore all previous revisions have to be loaded. * * @todo Help get that core issue fixed or rewrite this function query table * fields directly. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * * @return \Drupal\Core\Entity\ContentEntityInterface[] */ protected function getPreviousRevisionsWithUpdates(ContentEntityInterface $entity) { /** @var ContentEntityInterface[] $revisions */ $revisions = []; $type = $entity->getEntityType(); $query = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->getQuery(); $query->allRevisions()->condition($type->getKey('id'), $entity->id())->condition($type->getKey('revision'), $entity->getRevisionId(), '<')->sort($type->getKey('revision'), 'DESC'); if ($revision_ids = $query->execute()) { $revision_ids = array_keys($revision_ids); $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); foreach ($revision_ids as $revision_id) { /** @var ContentEntityInterface $revision */ $revision = $storage->loadRevision($revision_id); if ($update_ids = $this->getUpdateIdsOnEntity($revision)) { $revisions[$revision_id] = $revision; } } } return $revisions; }
/** * Build the render array for the block library. * * @param EntityLayoutInterface $entity_layout * The entity layout to show allowed blocks for. * @param ContentEntityInterface $content_entity * The content entity the block is being added to. * * @return array * The render array. */ protected function buildBlockLibrary(EntityLayoutInterface $entity_layout, ContentEntityInterface $content_entity = NULL) { $build['filter'] = ['#type' => 'search', '#title' => $this->t('Filter'), '#title_display' => 'invisible', '#size' => 30, '#placeholder' => $this->t('Filter by block name'), '#attributes' => ['class' => ['context-table-filter'], 'data-element' => '.block-add-table', 'title' => $this->t('Enter a part of the block name to filter by.')]]; $headers = [$this->t('Block'), $this->t('Category'), $this->t('Operations')]; $build['blocks'] = ['#type' => 'table', '#header' => $headers, '#rows' => [], '#empty' => $this->t('No blocks available for placement.'), '#attributes' => ['class' => ['block-add-table']]]; $blocks = $this->entityLayoutService->getSystemBlocks(); // Add each block definition to the table. foreach ($blocks as $block_id => $block) { if (!$entity_layout->blockIsAllowed($block_id)) { continue; } $bundle_entity_type = $this->entityLayoutService->getTargetBundleEntityType($entity_layout); // Use different routes depending on what kind of entity were adding // blocks for. if ($content_entity) { $entity_type_id = $content_entity->getEntityTypeId(); $block_add_url = Url::fromRoute("entity_layout.{$entity_type_id}.content.block.add", ['block_id' => $block_id, $entity_type_id => $content_entity->id()]); } else { $entity_type_id = $entity_layout->getTargetEntityType(); $block_add_url = Url::fromRoute("entity_layout.{$entity_type_id}.block.add", ['block_id' => $block_id, $bundle_entity_type => $entity_layout->getTargetBundle()]); } $links = ['add' => ['title' => $this->t('Place block'), 'url' => $block_add_url, 'attributes' => ['class' => ['use-ajax'], 'data-dialog-type' => 'modal', 'data-dialog-options' => Json::encode(['width' => 700])]]]; $build['blocks']['#rows'][] = ['title' => ['data' => ['#type' => 'inline_template', '#template' => '<div class="context-table-filter-text-source">{{ label }}</div>', '#context' => ['label' => $block['admin_label']]]], 'category' => ['data' => $block['category']], 'operations' => ['data' => ['#type' => 'operations', '#links' => $links]]]; } // @todo Create a filter behaviour for the table. //$build['#attached']['library'][] = 'context_ui/admin'; return $build; }
/** * Loads all revision IDs of an entity sorted by revision ID descending. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity. * * @return mixed[] */ protected function revisionIds(ContentEntityInterface $entity) { $entity_type = $entity->getEntityType(); $result = $this->entityTypeManager()->getStorage($entity_type->id())->getQuery()->allRevisions()->condition($entity_type->getKey('id'), $entity->id())->sort($entity_type->getKey('revision'), 'DESC')->execute(); return array_keys($result); }
/** * Get the directly previous revision. * * $entity->original will not ALWAYS be the previous revision. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * * @return \Drupal\Core\Entity\EntityInterface|null */ public function getPreviousRevision(ContentEntityInterface $entity) { $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); $query = $storage->getQuery(); $type = $entity->getEntityType(); $query->allRevisions() ->condition($type->getKey('id'), $entity->id()) ->condition($type->getKey('revision'), $entity->getRevisionId(), '<') ->sort($type->getKey('revision'), 'DESC') ->pager(1); $revision_ids = $query->execute(); if ($revision_ids) { $revision_id = array_keys($revision_ids)[0]; return $storage->loadRevision($revision_id); } return NULL; }
/** * {@inheritdoc} */ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); $is_deleted = $this->storageDefinitionIsDeleted($storage_definition); $table_mapping = $this->getTableMapping(); $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $is_deleted); $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $is_deleted); $revision_id = $this->entityType->isRevisionable() ? $entity->getRevisionId() : $entity->id(); $this->database->delete($table_name)->condition('revision_id', $revision_id)->execute(); if ($this->entityType->isRevisionable()) { $this->database->delete($revision_name)->condition('revision_id', $revision_id)->execute(); } }
/** * {@inheritdoc} */ public function getNewCommentPageNumber($total_comments, $new_comments, ContentEntityInterface $entity, $field_name = 'comment') { $instance = $entity->getFieldDefinition($field_name); $comments_per_page = $instance->getSetting('per_page'); if ($total_comments <= $comments_per_page) { // Only one page of comments. $count = 0; } elseif ($instance->getSetting('default_mode') == CommentManagerInterface::COMMENT_MODE_FLAT) { // Flat comments. $count = $total_comments - $new_comments; } else { // Threaded comments. // 1. Find all the threads with a new comment. $unread_threads_query = $this->database->select('comment_field_data', 'comment')->fields('comment', array('thread'))->condition('entity_id', $entity->id())->condition('entity_type', $entity->getEntityTypeId())->condition('field_name', $field_name)->condition('status', CommentInterface::PUBLISHED)->condition('default_langcode', 1)->orderBy('created', 'DESC')->orderBy('cid', 'DESC')->range(0, $new_comments); // 2. Find the first thread. $first_thread_query = $this->database->select($unread_threads_query, 'thread'); $first_thread_query->addExpression('SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 'torder'); $first_thread = $first_thread_query->fields('thread', array('thread'))->orderBy('torder')->range(0, 1)->execute()->fetchField(); // Remove the final '/'. $first_thread = substr($first_thread, 0, -1); // Find the number of the first comment of the first unread thread. $count = $this->database->query('SELECT COUNT(*) FROM {comment_field_data} WHERE entity_id = :entity_id AND entity_type = :entity_type AND field_name = :field_name AND status = :status AND SUBSTRING(thread, 1, (LENGTH(thread) - 1)) < :thread AND default_langcode = 1', array(':status' => CommentInterface::PUBLISHED, ':entity_id' => $entity->id(), ':field_name' => $field_name, ':entity_type' => $entity->getEntityTypeId(), ':thread' => $first_thread))->fetchField(); } return $comments_per_page > 0 ? (int) ($count / $comments_per_page) : 0; }