You are here

ListUsageController.php in Entity Usage 8.3

File

src/Controller/ListUsageController.php
View source
<?php

namespace Drupal\entity_usage\Controller;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\entity_usage\EntityUsageInterface;
use Drupal\entity_usage\EntityUsageSourceLevel;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Controller for our pages.
 */
class ListUsageController extends ControllerBase {

  /**
   * Number of items per group to use when nothing was configured.
   */
  const ITEMS_PER_GROUP_DEFAULT = 25;

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * The EntityUsage service.
   *
   * @var \Drupal\entity_usage\EntityUsageInterface
   */
  protected $entityUsage;

  /**
   * The Entity Usage settings config object.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $entityUsageConfig;

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * ListUsageController constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   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, 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->database = $database;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('entity_type.manager'), $container
      ->get('entity_field.manager'), $container
      ->get('entity_usage.usage'), $container
      ->get('config.factory'), $container
      ->get('database'));
  }

  /**
   * Lists the usage of a given entity.
   *
   * @param string $entity_type
   *   The entity type.
   * @param int $entity_id
   *   The entity ID.
   *
   * @return array
   *   The page build to be rendered.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
   */
  public function listUsagePage($entity_type, $entity_id) {

    // Alert users that there may be hidden info in past revisions, if that's
    // the case.
    $target_has_hidden_usages = $this
      ->checkHiddenUsages($entity_type, $entity_id);
    $items_per_group = $this->entityUsageConfig
      ->get('usage_controller_items_per_group') ?: self::ITEMS_PER_GROUP_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)) {
      if ($target_has_hidden_usages) {
        $empty_message = $this
          ->t('There are no recorded usages for entity of type: @type with id: @id, but some usages were found in past versions (revisions) of some nodes.', [
          '@type' => $entity_type,
          '@id' => $entity_id,
        ]);
      }
      else {
        $empty_message = $this
          ->t('There are no recorded usages for entity of type: @type with id: @id', [
          '@type' => $entity_type,
          '@id' => $entity_id,
        ]);
      }
      return [
        '#markup' => $empty_message,
      ];
    }
    $header = [
      $this
        ->t('Entity'),
      $this
        ->t('Type'),
      $this
        ->t('Language'),
      $this
        ->t('Status'),
    ];
    $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 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,
        $published,
      ];
    }
    $build[] = [
      '#theme' => 'table',
      '#rows' => $rows,
      '#header' => $header,
    ];
    $build[] = [
      '#type' => 'pager',
    ];
    if ($target_has_hidden_usages) {
      $build[] = [
        '#markup' => $this
          ->t('Note: This page only includes usages in the current revisions of referencing entities. This entity is also being used in nodes that no longer reference it on their current revision.'),
      ];
    }
    return $build;
  }

  /**
   * Query the DB for the next page of items to display.
   *
   * @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|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 getPageRows($target_type, $target_id, $start, $items_per_page, $count_only = FALSE) {
    $rows = [];
    $top_level_types = EntityUsageSourceLevel::getTopLevelEntityTypes();
    foreach ($top_level_types as $source_type) {
      $source_definition = \Drupal::entityTypeManager()
        ->getDefinition($source_type);
      $source_base_table = $source_definition
        ->getBaseTable();
      $source_revision_key = $source_definition
        ->hasKey('revision') ? $source_definition
        ->getKey('revision') : NULL;
      $query = $this->database
        ->select('entity_usage', 'eu')
        ->fields('eu', [
        'source_id',
        'source_id_string',
        'source_langcode',
      ])
        ->orderBy('source_id', 'DESC')
        ->condition('target_type', $target_type)
        ->condition('target_id', $target_id);

      // If the source is revisionable, join so we only include default
      // revisions in the results.
      if (!empty($source_base_table) && !empty($source_revision_key)) {
        $query
          ->innerJoin($source_base_table, 'sbt', "sbt.{$source_revision_key} = eu.source_vid");
      }
      if ($count_only) {
        return (int) $query
          ->countQuery()
          ->execute()
          ->fetchField();
      }
      $db_rows = $query
        ->range($start, $items_per_page)
        ->execute()
        ->fetchAll();
      foreach ($db_rows as $db_row) {
        $rows[] = [
          'source_type' => $source_type,
          'source_id' => $db_row->source_id ?? $db_row->source_id_string,
          'source_langcode' => $db_row->source_langcode,
        ];
      }
    }

    // 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;
  }

  /**
   * Checks whether there are hidden (past-revisions-only) usages.
   *
   * @param string $target_type
   *   The entity type name.
   * @param string $target_id
   *   The entity ID.
   *
   * @return bool
   *   TRUE if there are usages recorded in past (non-default) revisions which
   *   ARE NOT present in current (default) revisions, FALSE otherwise.
   */
  protected function checkHiddenUsages($target_type, $target_id) {

    // For now deal only with nodes.
    if (!$this
      ->moduleHandler()
      ->moduleExists('node') || !in_array('node', EntityUsageSourceLevel::getTopLevelEntityTypes())) {
      return FALSE;
    }

    // @todo Use the DB API for this, and remove workarounds.
    // What we want here is to check whether there are records in the
    // entity_usage table that match this target type/id, and are NOT present
    // in the node table, which means they are only present in past revisions.
    $query_string = "SELECT DISTINCT node.vid FROM {entity_usage}\n       LEFT JOIN {node}\n       ON entity_usage.source_id = node.nid\n       WHERE entity_usage.source_vid NOT IN (SELECT node.vid FROM {node})\n       AND entity_usage.target_type = :target_type\n       AND entity_usage.target_id = :target_id";
    try {
      $result = $this->database
        ->query($query_string, [
        ':target_type' => $target_type,
        ':target_id' => $target_id,
      ])
        ->fetchField();
    } catch (DatabaseExceptionWrapper $e) {
      return FALSE;
    }
    return !empty($result);
  }

  /**
   * Retrieve a link to the source entity on its canonical page.
   *
   * @param \Drupal\Core\Entity\EntityInterface $source_entity
   *   The source entity.
   *
   * @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.
   */
  protected function getSourceEntityCanonicalLink(EntityInterface $source_entity, $text = NULL) {
    $entity_label = $source_entity
      ->access('view label') ? $source_entity
      ->label() : $this
      ->t('- Restricted access -');

    // Prevent 404s by exposing the label unlinked if the user has no access
    // to view the entity.
    if ($source_entity
      ->hasLinkTemplate('canonical') && $source_entity
      ->access('view')) {
      return $source_entity
        ->toLink();
    }
    else {
      return $entity_label;
    }
  }

  /**
   * Checks access based on whether the user can view the current entity.
   *
   * @param string $entity_type
   *   The entity type.
   * @param int $entity_id
   *   The entity ID.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  public function checkAccess($entity_type, $entity_id) {
    $entity = $this->entityTypeManager
      ->getStorage($entity_type)
      ->load($entity_id);
    if (!$entity || !$entity
      ->access('view')) {
      return AccessResult::forbidden();
    }
    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');
  }

}

Classes

Namesort descending Description
ListUsageController Controller for our pages.