diff --git a/src/Controller/ListUsageController.php b/src/Controller/ListUsageController.php index 6a56494..6587945 100644 --- a/src/Controller/ListUsageController.php +++ b/src/Controller/ListUsageController.php @@ -8,9 +8,9 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\RevisionableInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Link; use Drupal\entity_usage\EntityUsageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -38,13 +38,6 @@ class ListUsageController extends ControllerBase { */ protected $entityUsage; - /** - * All source rows for this target entity. - * - * @var array - */ - protected $allRows; - /** * The Entity Usage settings config object. * @@ -53,11 +46,11 @@ class ListUsageController extends ControllerBase { protected $entityUsageConfig; /** - * The number of records per page this controller should output. + * The database connection. * - * @var int + * @var \Drupal\Core\Database\Connection */ - protected $itemsPerPage; + protected $database; /** * ListUsageController constructor. @@ -68,13 +61,17 @@ class ListUsageController extends ControllerBase { * The entity field manager. * @param \Drupal\entity_usage\EntityUsageInterface $entity_usage * The EntityUsage service. + * @param \Drupal\Core\Config\ImmutableConfig $config_factory + * The config factory. + * @param \Drupal\Core\Database\Connection $database + * The database connection. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EntityUsageInterface $entity_usage, ConfigFactoryInterface $config_factory) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EntityUsageInterface $entity_usage, ConfigFactoryInterface $config_factory, Connection $database) { $this->entityTypeManager = $entity_type_manager; $this->entityFieldManager = $entity_field_manager; $this->entityUsage = $entity_usage; $this->entityUsageConfig = $config_factory->get('entity_usage.settings'); - $this->itemsPerPage = $this->entityUsageConfig->get('usage_controller_items_per_page') ?: self::ITEMS_PER_PAGE_DEFAULT; + $this->database = $database; } /** @@ -85,7 +82,8 @@ class ListUsageController extends ControllerBase { $container->get('entity_type.manager'), $container->get('entity_field.manager'), $container->get('entity_usage.usage'), - $container->get('config.factory') + $container->get('config.factory'), + $container->get('database') ); } @@ -103,8 +101,12 @@ class ListUsageController extends ControllerBase { * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function listUsagePage($entity_type, $entity_id) { - $all_rows = $this->getRows($entity_type, $entity_id); - if (empty($all_rows)) { + $items_per_group = $this->entityUsageConfig->get('usage_controller_items_per_group') ?: self::ITEMS_PER_PAGE_DEFAULT; + $total = $this->getPageRows($entity_type, $entity_id, NULL, NULL, TRUE); + $page = pager_default_initialize($total, $items_per_group); + $value_rows = $this->getPageRows($entity_type, $entity_id, ($page * $items_per_group), $items_per_group); + + if (empty($value_rows)) { return [ '#markup' => $this->t('There are no recorded usages for entity of type: @type with id: @id', ['@type' => $entity_type, '@id' => $entity_id]), ]; @@ -116,30 +118,46 @@ class ListUsageController extends ControllerBase { $this->t('Language'), $this->t('Field name'), $this->t('Status'), - $this->t('Used in'), ]; - $total = count($all_rows); - $page = pager_default_initialize($total, $this->itemsPerPage); - $page_rows = $this->getPageRows($page, $this->itemsPerPage, $entity_type, $entity_id); - // If all rows on this page are of entities that have usage on their default - // revision, we don't need the "Used in" column. - $used_in_previous_revisions = FALSE; - foreach ($page_rows as $row) { - if ($row[5] == $this->t('Translations or previous revisions')) { - $used_in_previous_revisions = TRUE; - break; + $entity_types = $this->entityTypeManager->getDefinitions(); + $languages = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL); + + foreach ($value_rows as $row) { + $type_storage = $this->entityTypeManager->getStorage($row['source_type']); + $source_entity = $type_storage->load($row['source_id']); + if (!$source_entity) { + // If for some reason this record is broken, just skip it. + continue; } + + // Prepare the link to the source entity. + $source_link = $this->getSourceEntityCanonicalLink($source_entity); + + // Prepare the language name to display. + $lang_label = !empty($languages[$row['source_langcode']]) ? $languages[$row['source_langcode']]->getName() : '-'; + + // Prepare the field name. + $field_definitions = $this->entityFieldManager->getFieldDefinitions($row['source_type'], $source_entity->bundle()); + $field_label = isset($field_definitions[$row['field_name']]) ? $field_definitions[$row['field_name']]->getLabel() : $this->t('Unknown'); + // Prepare the status text. + $published = '-'; + if ($source_entity instanceof EntityPublishedInterface) { + $published = $source_entity->isPublished() ? $this->t('Published') : $this->t('Unpublished'); + } + + $rows[] = [ + $source_link, + $entity_types[$row['source_type']]->getLabel(), + $lang_label, + $field_label, + $published, + ]; } - if (!$used_in_previous_revisions) { - unset($header[5]); - array_walk($page_rows, function (&$row, $key) { - unset($row[5]); - }); - } + $build[] = [ '#theme' => 'table', - '#rows' => $page_rows, + '#rows' => $rows, '#header' => $header, ]; @@ -151,151 +169,71 @@ class ListUsageController extends ControllerBase { } /** - * Retrieve all usage rows for this target entity. + * Query the DB for the next page of items to display. * - * @param string $entity_type - * The type of the target entity. - * @param int|string $entity_id - * The ID of the target entity. + * @param string $target_type + * The target entity type. + * @param string $target_id + * The target entity ID. + * @param int $start + * The initial position to start the query range. + * @param int $items_per_page + * The number of items per page to use in the query range. + * @param bool $count_only + * (optional) Whether to return an integer with the total number of + * rows in the query, which can be used when calculating the pager output. + * Defaults to FALSE. * - * @return array - * An indexed array of rows that should be displayed as sources for this - * target entity. + * @return array|int + * An indexed array of source entities info, where values are: + * - source_type: The source entity type. + * - source_id: The ID of the source entity. + * - source_langcode: The langcode of the source entity. + * Will return an integer with the total rows for this query if the flag + * $count_only is passed in. */ - protected function getRows($entity_type, $entity_id) { - if (!empty($this->allRows)) { - return $this->allRows; - // @todo Cache this based on the target entity, invalidating the cached - // results every time records are added/removed to the same target entity. - } + protected function getPageRows($target_type, $target_id, $start, $items_per_page, $count_only = FALSE) { $rows = []; - $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); - if (!$entity) { - return $rows; - } - $entity_types = $this->entityTypeManager->getDefinitions(); - $languages = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL); - $all_usages = $this->entityUsage->listSources($entity); - foreach ($all_usages as $source_type => $ids) { - $type_storage = $this->entityTypeManager->getStorage($source_type); - foreach ($ids as $source_id => $records) { - // We will show a single row per source entity. If the target is not - // referenced on its default revision on the default language, we will - // just show indicate that in a specific column. - $source_entity = $type_storage->load($source_id); - if (!$source_entity) { - // If for some reason this record is broken, just skip it. - continue; - } - $field_definitions = $this->entityFieldManager->getFieldDefinitions($source_type, $source_entity->bundle()); - if ($source_entity instanceof RevisionableInterface) { - $default_revision_id = $source_entity->getRevisionId(); - $default_langcode = $source_entity->language()->getId(); - $used_in_default = FALSE; - $default_key = 0; - foreach ($records as $key => $record) { - if ($record['source_vid'] == $default_revision_id && $record['source_langcode'] == $default_langcode) { - $default_key = $key; - $used_in_default = TRUE; - break; - } - } - $used_in_text = $used_in_default ? $this->t('Default') : $this->t('Translations or previous revisions'); - } - $link = $this->getSourceEntityLink($source_entity); - // If the label is empty it means this usage shouldn't be shown - // on the UI, just skip this row. - if (empty($link)) { - continue; - } - $published = $this->getSourceEntityStatus($source_entity); - $field_label = isset($field_definitions[$records[$default_key]['field_name']]) ? $field_definitions[$records[$default_key]['field_name']]->getLabel() : $this->t('Unknown'); - $rows[] = [ - $link, - $entity_types[$source_type]->getLabel(), - $languages[$default_langcode]->getName(), - $field_label, - $published, - $used_in_text, - ]; - } - } - - $this->allRows = $rows; - return $this->allRows; - } + $query = $this->database->select('entity_usage', 'eu') + ->fields('eu', [ + 'source_id', + 'source_id_string', + 'source_type', + 'source_langcode', + 'field_name', + ]) + ->orderBy('source_id', 'DESC') + ->condition('target_type', $target_type) + ->condition('count', 0, '>') + ->condition('target_id', $target_id); - /** - * Get rows for a given page. - * - * @param int $page - * The page number to retrieve. - * @param int $num_per_page - * The number of rows we want to have on this page. - * @param string $entity_type - * The type of the target entity. - * @param int|string $entity_id - * The ID of the target entity. - * - * @return array - * An indexed array of rows representing the records for a given page. - */ - protected function getPageRows($page, $num_per_page, $entity_type, $entity_id) { - $offset = $page * $num_per_page; - return array_slice($this->getRows($entity_type, $entity_id), $offset, $num_per_page); - } - /** - * Title page callback. - * - * @param string $entity_type - * The entity type. - * @param int $entity_id - * The entity id. - * - * @return string - * The title to be used on this page. - */ - public function getTitle($entity_type, $entity_id) { - $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); - if ($entity) { - return $this->t('Entity usage information for %entity_label', ['%entity_label' => $entity->label()]); - } - return $this->t('Entity Usage List'); - } - - /** - * Retrieve the source entity's status. - * - * @param \Drupal\Core\Entity\EntityInterface $source_entity - * The source entity. - * - * @return string - * The entity's status. - */ - protected function getSourceEntityStatus(EntityInterface $source_entity) { - // Treat paragraph entities in a special manner. Paragraph entities - // should get their host (parent) entity's status. - if ($source_entity->getEntityTypeId() == 'paragraph') { - /** @var \Drupal\paragraphs\ParagraphInterface $source_entity */ - $parent = $source_entity->getParentEntity(); - if (!empty($parent)) { - return $this->getSourceEntityStatus($parent); - } - } - - if (isset($source_entity->status)) { - $published = !empty($source_entity->status->value) ? $this->t('Published') : $this->t('Unpublished'); + if ($count_only) { + return (int) $query->countQuery()->execute()->fetchField(); } else { - $published = ''; + $db_rows = $query + ->range($start, $items_per_page) + ->execute() + ->fetchAll(); + foreach ($db_rows as $db_row) { + $rows[] = [ + 'source_type' => $db_row->source_type, + 'source_id' => $db_row->source_id ?? $db_row->source_id_string, + 'source_langcode' => $db_row->source_langcode, + 'field_name' => $db_row->field_name, + ]; + } } - return $published; + // Sort by entity type ASC and then by entity ID DESC. + array_multisort( array_column($rows, 'source_type'), SORT_ASC, $rows); + array_multisort( array_column($rows, 'source_id'), SORT_DESC, $rows); + return $rows; } /** - * Retrieve a link to the source entity. + * Retrieve a link to the source entity on its canonical page. * * @param \Drupal\Core\Entity\EntityInterface $source_entity * The source entity. @@ -303,76 +241,18 @@ class ListUsageController extends ControllerBase { * (optional) The link text for the anchor tag as a translated string. * If NULL, it will use the entity's label. Defaults to NULL. * - * @return \Drupal\Core\Link|string|false + * @return \Drupal\Core\Link|string * A link to the entity, or its non-linked label, in case it was impossible - * to correctly build a link. Will return FALSE if this item should not be - * shown on the UI (for example when dealing with an orphan paragraph). - * Note that Paragraph entities are specially treated. This function will - * return the link to its parent entity, relying on the fact that paragraphs - * have only one single parent and don't have canonical template. + * to correctly build a link. */ - protected function getSourceEntityLink(EntityInterface $source_entity, $text = NULL) { - // Note that $paragraph_entity->label() will return a string of type: - // "{parent label} > {parent field}", which is actually OK for us. + protected function getSourceEntityCanonicalLink(EntityInterface $source_entity, $text = NULL) { $entity_label = $source_entity->access('view label') ? $source_entity->label() : $this->t('- Restricted access -'); - - $rel = NULL; - if ($source_entity->hasLinkTemplate('revision')) { - $rel = 'revision'; - } - elseif ($source_entity->hasLinkTemplate('canonical')) { - $rel = 'canonical'; + if ($source_entity->hasLinkTemplate('canonical') && $source_entity->access('view')) { + return $source_entity->toLink(); } - - if ($rel) { - $link_text = $text ?: $entity_label; - // Prevent 404s by exposing the text unlinked if the user has no access - // to view the entity. - return $source_entity->access('view') ? $source_entity->toLink($link_text, $rel) : $link_text; - } - - // Treat paragraph entities in a special manner. Normal paragraph entities - // only exist in the context of their host (parent) entity. For this reason - // we will use the link to the parent's entity label instead. - /** @var \Drupal\paragraphs\ParagraphInterface $source_entity */ - if ($source_entity->getEntityTypeId() == 'paragraph') { - // Paragraph items may be legitimately orphan, so even if this is a real - // usage, we will only show it on the UI if its parent is loadable and - // references the paragraph on its default revision. - // @todo This could probably be simplified once #2954039 lands. - $parent = $source_entity->getParentEntity(); - if (empty($parent)) { - $orphan = TRUE; - } - else { - $parent_field = $source_entity->get('parent_field_name')->value; - /** @var \Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList $values */ - $values = $parent->{$parent_field}; - if (empty($values->getValue())) { - // The field is empty or was removed. - $orphan = TRUE; - } - else { - // There are values in the field. Once paragraphs can have just been - // re-ordered, there is no other option apart from looping through all - // values and checking if any of them is this entity. - $orphan = TRUE; - foreach ($values as $value) { - if ($value->entity->id() == $source_entity->id()) { - $orphan = FALSE; - break; - } - } - } - } - if ($orphan) { - return FALSE; - } - return $this->getSourceEntityLink($parent, $entity_label); + else { + return $entity_label; } - - // As a fallback just return a non-linked label. - return $entity_label; } /** @@ -394,4 +274,23 @@ class ListUsageController extends ControllerBase { return AccessResult::allowed(); } + /** + * Title page callback. + * + * @param string $entity_type + * The entity type. + * @param int $entity_id + * The entity id. + * + * @return string + * The title to be used on this page. + */ + public function getTitle($entity_type, $entity_id) { + $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); + if ($entity) { + return $this->t('Entity usage information for %entity_label', ['%entity_label' => $entity->label()]); + } + return $this->t('Entity Usage List'); + } + }