Commit 8677cd30 authored by Kristiaan Van den Eynde's avatar Kristiaan Van den Eynde
Browse files

Issue #3258942 by kristiaanvandeneynde: Optimize GroupContentStorage and calls to it

parent 805b4818
Loading
Loading
Loading
Loading
+24 −25
Original line number Diff line number Diff line
@@ -314,35 +314,34 @@ function group_entity_access(EntityInterface $entity, $operation, AccountInterfa
  foreach ($plugin_ids as $plugin_id) {
    $plugin_cache_tags[] = "group_content_list:plugin:$plugin_id";
  }

  // Load all of the group content for this entity.
  $group_contents = \Drupal::entityTypeManager()
    ->getStorage('group_content')
    ->loadByEntity($entity);

  // If the entity does not belong to any group, we have nothing to say.
  //
  // @todo There is a slight performance boost to be had here. If we have a
  // plugin that adds content without access and one that adds the same content
  // with access, then this check would fail for content that is only added
  // using the non-access plugin. The loop over the plugin IDs below would then
  // not return anything and we would still end up with a neutral result, albeit
  // with the user.group_permissions context and slightly worse performance. We
  // can fix this by only loading those group content entities that use the
  // plugins retrieved above.
  if (empty($group_contents)) {
    return AccessResult::neutral()->addCacheTags($plugin_cache_tags);
  }

  $access = AccessResult::neutral();
  foreach ($plugin_ids as $plugin_id) {
  $access = AccessResult::neutral()->addCacheTags($plugin_cache_tags);

  // Reduce the plugin IDs to those that are actually in use.
  $data_table = \Drupal::entityTypeManager()
    ->getDefinition('group_content')
    ->getDataTable();

  $plugin_ids_used = \Drupal::database()
    ->select($data_table, 'd')
    ->fields('d', ['plugin_id'])
    ->condition('entity_id', $entity->id())
    ->condition('plugin_id', $plugin_ids, 'IN')
    ->distinct(TRUE)
    ->execute()
    ->fetchCol();

  // Loop over the plugin handlers and add their access check to the result.
  foreach ($plugin_ids_used as $plugin_id) {
    $handler = $plugin_manager->getAccessControlHandler($plugin_id);
    $access = $access->orIf($handler->entityAccess($entity, $operation, $account, TRUE));

    // No need to continue if access is denied.
    if ($access->isForbidden()) {
      break;
    }
  }

  return $access
    ->addCacheTags($plugin_cache_tags)
    ->addCacheContexts(['user.group_permissions']);
  return $access;
}

/**
+9 −5
Original line number Diff line number Diff line
@@ -160,24 +160,28 @@ class Group extends EditorialContentEntityBase implements GroupInterface {
  /**
   * {@inheritdoc}
   */
  public function getContent($plugin_id = NULL, $filters = []) {
    return $this->groupContentStorage()->loadByGroup($this, $plugin_id, $filters);
  public function getContent($plugin_id = NULL) {
    return $this->groupContentStorage()->loadByGroup($this, $plugin_id);
  }

  /**
   * {@inheritdoc}
   */
  public function getContentByEntityId($plugin_id, $id) {
    return $this->getContent($plugin_id, ['entity_id' => $id]);
    return $this->groupContentStorage()->loadByProperties([
      'gid' => $this->id(),
      'plugin_id' => $plugin_id,
      'entity_id' => $id,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function getContentEntities($plugin_id = NULL, $filters = []) {
  public function getContentEntities($plugin_id = NULL) {
    $entities = [];

    foreach ($this->getContent($plugin_id, $filters) as $group_content) {
    foreach ($this->getContent($plugin_id) as $group_content) {
      $entities[] = $group_content->getEntity();
    }

+2 −8
Original line number Diff line number Diff line
@@ -50,14 +50,11 @@ interface GroupInterface extends ContentEntityInterface, EntityOwnerInterface, E
   *
   * @param string $plugin_id
   *   (optional) A group relation type ID to filter on.
   * @param array $filters
   *   (optional) An associative array of extra filters where the keys are
   *   property or field names and the values are the value to filter on.
   *
   * @return \Drupal\group\Entity\GroupContentInterface[]
   *   A list of GroupContent entities matching the criteria.
   */
  public function getContent($plugin_id = NULL, $filters = []);
  public function getContent($plugin_id = NULL);

  /**
   * Retrieves all GroupContent entities for a specific entity.
@@ -80,9 +77,6 @@ interface GroupInterface extends ContentEntityInterface, EntityOwnerInterface, E
   *
   * @param string $plugin_id
   *   (optional) A group relation type ID to filter on.
   * @param array $filters
   *   (optional) An associative array of extra filters where the keys are
   *   property or field names and the values are the value to filter on.
   *
   * @return \Drupal\Core\Entity\EntityInterface[]
   *   A list of entities matching the criteria. This list does not have keys
@@ -90,7 +84,7 @@ interface GroupInterface extends ContentEntityInterface, EntityOwnerInterface, E
   *
   * @see \Drupal\group\Entity\GroupInterface::getContent()
   */
  public function getContentEntities($plugin_id = NULL, $filters = []);
  public function getContentEntities($plugin_id = NULL);

  /**
   * Adds a user as a member of the group.
+65 −41
Original line number Diff line number Diff line
@@ -15,6 +15,13 @@ use Drupal\group\Entity\GroupInterface;
 */
class GroupContentStorage extends SqlContentEntityStorage implements GroupContentStorageInterface {

  /**
   * Static cache for looking up group content entities for groups.
   *
   * @var array
   */
  protected $loadByGroupCache = [];

  /**
   * Static cache for looking up group content entities for entities.
   *
@@ -22,6 +29,13 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten
   */
  protected $loadByEntityCache = [];

  /**
   * Static cache for looking up group content entities for plugins.
   *
   * @var array
   */
  protected $loadByPluginCache = [];

  /**
   * {@inheritdoc}
   */
@@ -69,60 +83,62 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten
  /**
   * {@inheritdoc}
   */
  public function loadByGroup(GroupInterface $group, $plugin_id = NULL, $filters = []) {
  public function loadByGroup(GroupInterface $group, $plugin_id = NULL) {
    // An unsaved group cannot have any content.
    if ($group->id() === NULL) {
      throw new EntityStorageException("Cannot load GroupContent entities for an unsaved group.");
    $group_id = $group->id();
    if ($group_id === NULL) {
      return [];
    }

    $properties = ['gid' => $group->id()] + $filters;
    $cache_key = $plugin_id ?: '---ALL---';
    if (!isset($this->loadByGroupCache[$group_id][$cache_key])) {
      $query = $this->database
        ->select($this->dataTable, 'd')
        ->fields('d', ['id'])
        ->condition('gid', $group_id);

    // If a plugin ID was provided, set the group content type ID for it.
    if (isset($plugin_id)) {
      /** @var \Drupal\group\Entity\Storage\GroupContentTypeStorageInterface $storage */
      $storage = $this->entityTypeManager->getStorage('group_content_type');
      $properties['type'] = $storage->getGroupContentTypeId($group->bundle(), $plugin_id);
      if ($plugin_id) {
        $query->condition('plugin_id', $plugin_id);
      }

      $this->loadByGroupCache[$group_id][$cache_key] = $query->execute()->fetchCol();
    }

    return $this->loadByProperties($properties);
    if (!empty($this->loadByGroupCache[$group_id][$cache_key])) {
      return $this->loadMultiple($this->loadByGroupCache[$group_id][$cache_key]);
    }
    else {
      return [];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function loadByEntity(ContentEntityInterface $entity) {
  public function loadByEntity(ContentEntityInterface $entity, $plugin_id = NULL) {
    // An unsaved entity cannot have any group content.
    $entity_id = $entity->id();
    if ($entity_id === NULL) {
      throw new EntityStorageException("Cannot load GroupContent entities for an unsaved entity.");
      return [];
    }

    $entity_type_id = $entity->getEntityTypeId();
    if (!isset($this->loadByEntityCache[$entity_type_id][$entity_id])) {
      /** @var \Drupal\group\Entity\Storage\GroupContentTypeStorageInterface $storage */
      $storage = $this->entityTypeManager->getStorage('group_content_type');
      $group_content_types = $storage->loadByEntityTypeId($entity_type_id);

      // Statically cache all group content IDs for the group content types.
      if (!empty($group_content_types)) {
        // Use an optimized plain query to avoid the overhead of entity and SQL
        // query builders.
        $query = "SELECT id from {{$this->dataTable}} WHERE entity_id = :entity_id AND type IN (:types[])";
        $this->loadByEntityCache[$entity_type_id][$entity_id] = $this->database
          ->query($query, [
            ':entity_id' => $entity_id,
            ':types[]' => array_keys($group_content_types),
          ])
          ->fetchCol();
      }
      // If no responsible group content types were found, we return nothing.
      else {
        $this->loadByEntityCache[$entity_type_id][$entity_id] = [];
    $cache_key = $plugin_id ?: '---ALL---';
    if (!isset($this->loadByEntityCache[$entity_type_id][$entity_id][$cache_key])) {
      $query = $this->database
        ->select($this->dataTable, 'd')
        ->fields('d', ['id'])
        ->condition('entity_id', $entity_id);

      if ($plugin_id) {
        $query->condition('plugin_id', $plugin_id);
      }

      $this->loadByEntityCache[$entity_type_id][$entity_id][$cache_key] = $query->execute()->fetchCol();
    }

    if (!empty($this->loadByEntityCache[$entity_type_id][$entity_id])) {
      return $this->loadMultiple($this->loadByEntityCache[$entity_type_id][$entity_id]);
    if (!empty($this->loadByEntityCache[$entity_type_id][$entity_id][$cache_key])) {
      return $this->loadMultiple($this->loadByEntityCache[$entity_type_id][$entity_id][$cache_key]);
    }
    else {
      return [];
@@ -133,15 +149,21 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten
   * {@inheritdoc}
   */
  public function loadByPluginId($plugin_id) {
    // If no responsible group content types were found, we return nothing.
    /** @var \Drupal\group\Entity\Storage\GroupContentTypeStorageInterface $storage */
    $storage = $this->entityTypeManager->getStorage('group_content_type');
    $group_content_types = $storage->loadByPluginId($plugin_id);
    if (empty($group_content_types)) {
      return [];
    if (!isset($this->loadByPluginCache[$plugin_id])) {
      $query = $this->database
        ->select($this->dataTable, 'd')
        ->fields('d', ['id'])
        ->condition('plugin_id', $plugin_id);

      $this->loadByPluginCache[$plugin_id] = $query->execute()->fetchCol();
    }

    return $this->loadByProperties(['type' => array_keys($group_content_types)]);
    if (!empty($this->loadByPluginCache[$plugin_id])) {
      return $this->loadMultiple($this->loadByPluginCache[$plugin_id]);
    }
    else {
      return [];
    }
  }

  /**
@@ -149,7 +171,9 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten
   */
  public function resetCache(array $ids = NULL) {
    parent::resetCache($ids);
    $this->loadByGroupCache = [];
    $this->loadByEntityCache = [];
    $this->loadByPluginCache = [];
  }

}
+4 −5
Original line number Diff line number Diff line
@@ -35,25 +35,24 @@ interface GroupContentStorageInterface extends ContentEntityStorageInterface {
   *   The group entity to load the group content entities for.
   * @param string $plugin_id
   *   (optional) A group relation type ID to filter on.
   * @param array $filters
   *   (optional) An associative array of extra filters where the keys are
   *   property or field names and the values are the value to filter on.
   *
   * @return \Drupal\group\Entity\GroupContentInterface[]
   *   A list of GroupContent entities matching the criteria.
   */
  public function loadByGroup(GroupInterface $group, $plugin_id = NULL, $filters = []);
  public function loadByGroup(GroupInterface $group, $plugin_id = NULL);

  /**
   * Retrieves all GroupContent entities that represent a given entity.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   An entity which may be within one or more groups.
   * @param string $plugin_id
   *   (optional) A group relation type ID to filter on.
   *
   * @return \Drupal\group\Entity\GroupContentInterface[]
   *   A list of GroupContent entities which refer to the given entity.
   */
  public function loadByEntity(ContentEntityInterface $entity);
  public function loadByEntity(ContentEntityInterface $entity, $plugin_id = NULL);

  /**
   * Retrieves all GroupContent entities by their responsible plugin ID.
Loading