diff -u b/modules/ggroup/src/Graph/SqlGroupGraphStorage.php b/modules/ggroup/src/Graph/SqlGroupGraphStorage.php --- b/modules/ggroup/src/Graph/SqlGroupGraphStorage.php +++ b/modules/ggroup/src/Graph/SqlGroupGraphStorage.php @@ -11,6 +11,46 @@ class SqlGroupGraphStorage implements GroupGraphStorageInterface { /** + * Static cache for ancestor lookup. + * + * This array allow us to retrieve the ancestors faster. + * + * @var int[][] + * An nested array containing all ancestor group IDs for a group. + */ + protected $ancestors; + + /** + * Static cache for descendant lookup. + * + * This array allow us to retrieve the ancestors faster. + * + * @var int[][] + * An nested array containing all ancestor group IDs for a group. + */ + protected $descendants; + + /** + * Static cache for direct ancestor lookup. + * + * This array allow us to retrieve the ancestors faster. + * + * @var int[][] + * An nested array containing all ancestor group IDs for a group. + */ + protected $directAncestors; + + /** + * Static cache for direct descendant lookup. + * + * This array allow us to retrieve the ancestors faster. + * + * @var int[][] + * An nested array containing all ancestor group IDs for a group. + */ + protected $directDescendants; + + /** * The database connection. * * @var \Drupal\Core\Database\Connection @@ -25,6 +65,27 @@ */ public function __construct(Connection $connection) { $this->connection = $connection; + $this->updateStaticCache(); + } + + /** + * Fetch all records from graph and cache descendants and ancestors. + * + * This is mostly done for performance reason. When having lost of groups, + * getting/checking the ancestors for each one in a seperate query is a lot + * slower. + */ + protected function updateStaticCache() { + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['start_vertex', 'end_vertex']); + $this->descendants = $query->execute()->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP); + $this->ancestors = $query->execute()->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP, 1); + + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['start_vertex', 'end_vertex']); + $query->condition('hops', 0); + $this->directDescendants = $query->execute()->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP); + $this->directAncestors = $query->execute()->fetchAll(\PDO::FETCH_COLUMN | \PDO::FETCH_GROUP, 1); } /** @@ -219,6 +280,8 @@ $this->insertEdgesParentToChildOutgoing($new_edge_id, $parent_group_id, $child_group_id); $this->insertEdgesParentIncomingToChildOutgoing($new_edge_id, $parent_group_id, $child_group_id); + $this->updateStaticCache(); + return $new_edge_id; } @@ -268,44 +331,36 @@ $this->connection->delete('group_graph') ->condition('id', $edges_to_delete, 'IN') ->execute(); + + $this->updateStaticCache(); } /** * {@inheritdoc} */ public function getDescendants($group_id) { - $query = $this->connection->select('group_graph', 'gg') - ->fields('gg', ['end_vertex']); - $query->condition('start_vertex', $group_id); - return $query->execute()->fetchAll(\PDO::FETCH_COLUMN); + return isset($this->descendants[$group_id]) ? $this->descendants[$group_id] : []; } /** * {@inheritdoc} */ public function getAncestors($group_id) { - $query = $this->connection->select('group_graph', 'gg') - ->fields('gg', ['start_vertex']); - $query->condition('end_vertex', $group_id); - return $query->execute()->fetchAll(\PDO::FETCH_COLUMN); + return isset($this->ancestors[$group_id]) ? $this->ancestors[$group_id] : []; } /** * {@inheritdoc} */ - public function isAncestor($a, $b) { - return $this->isDescendant($b, $a); + public function isDescendant($a, $b) { + return isset($this->descendants[$b]) ? in_array($a, $this->descendants[$b]) : FALSE; } /** * {@inheritdoc} */ - public function isDescendant($a, $b) { - $query = $this->connection->select('group_graph', 'gg'); - $query->addExpression('COUNT(gg.id)'); - $query->condition('start_vertex', $b); - $query->condition('end_vertex', $a); - return $query->execute()->fetchField() > 0; + public function isAncestor($a, $b) { + return isset($this->ancestors[$b]) ? in_array($a, $this->ancestors[$b]) : FALSE; } /** @@ -333,11 +388,7 @@ $child_id = $queue->dequeue(); // Get parents for child in queue. - $query = $this->connection->select('group_graph', 'gg') - ->fields('gg', ['start_vertex']); - $query->condition('end_vertex', $child_id); - $query->condition('hops', 0); - $parent_ids = $query->execute()->fetchAll(\PDO::FETCH_COLUMN); + $parent_ids = $this->directAncestors[$child_id]; foreach ($parent_ids as $parent_id) { if ((int) $parent_id === (int) $parent_group_id) { diff -u b/modules/ggroup/src/GroupHierarchyManager.php b/modules/ggroup/src/GroupHierarchyManager.php --- b/modules/ggroup/src/GroupHierarchyManager.php +++ b/modules/ggroup/src/GroupHierarchyManager.php @@ -41,13 +41,13 @@ protected $subgroupContentTypeConfig = []; /** - * Static cache of a user's inherited group role IDs. + * Static cache of all group content types for subgroup group content. * - * This nested array is keyed by user ID and group ID. + * This nested array is keyed by subgroup ID and group ID. * - * @var \Drupal\group\Entity\GroupRoleInterface[][] + * @var string[][] */ - protected $inheritedUserGroupRoleIds = []; + protected $subgroupGroupContentTypes = []; /** * Constructs a new GroupHierarchyManager. @@ -152,38 +152,40 @@ * {@inheritdoc} */ public function getInheritedGroupRoles(GroupInterface $group, AccountInterface $account) { - if (isset($inheritedUserGroupRoleIds[$account->id()][$group->id()])) { - return $inheritedUserGroupRoleIds[$account->id()][$group->id()]; - } - $role_ids = []; $memberships = $this->loadUserDirectMemberships($account); foreach ($memberships as $membership) { - $account_group = $membership->getGroup(); + $account_group_gid = $membership->getGroupContent()->gid->target_id; $account_group_role_ids = $this->getMembershipRoles($membership); // Return loaded roles if user is a direct member of the group. - if ($account_group->id() === $group->id()) { + if ($account_group_gid === $group->id()) { $role_ids = array_merge($account_group_role_ids); continue; } - $role_ids = array_merge($this->getInheritedSupergroupRoleIds($account_group, $group, $account_group_role_ids), $role_ids); - $role_ids = array_merge($this->getInheritedSubgroupRoleIds($account_group, $group, $account_group_role_ids), $role_ids); + $role_ids = array_merge($this->getInheritedSupergroupRoleIds($account_group_gid, $group, $account_group_role_ids), $role_ids); + $role_ids = array_merge($this->getInheritedSubgroupRoleIds($account_group_gid, $group, $account_group_role_ids), $role_ids); } - return $inheritedUserGroupRoleIds[$account->id()][$group->id()] = $this->entityTypeManager->getStorage('group_role')->loadMultiple($role_ids); + return $this->entityTypeManager->getStorage('group_role')->loadMultiple($role_ids); } /** - * Get the plugin config for all installed subgroup content types. + * Get the plugin config for a relation between a group and a subgroup. + * + * @param int $group_id + * The group for which to get the configuration. + * @param int $subgroup_id + * The subgroup for which to get the configuration. * * @return array[] - * A nested array with all configuration values for each subgroup content - * type. The array is keyed by subgroup content type ID. + * A nested array with configuration values. */ - protected function getSubGroupContentTypeConfig() { + protected function getSubGroupContentTypeConfig($group_id, $subgroup_id) { + // We create a static cache for all subgroup group content type + // configuration for performance reasons. if (!$this->subgroupContentTypeConfig) { foreach ($this->entityTypeManager->getStorage('group_type')->loadMultiple() as $group_type) { $plugin_id = 'subgroup:' . $group_type->id(); @@ -194,7 +196,24 @@ } } } - return $this->subgroupContentTypeConfig; + + // We create a static cache for all subgroup group content since fetching + // each relation independently is a huge performance issue when a user has + // a lot of group memberships. + if (!$this->subgroupGroupContentTypes) { + // Get all type between the supergroup and subgroup. + $group_contents = $this->entityTypeManager->getStorage('group_content') + ->loadByProperties([ + 'type' => array_keys($this->subgroupContentTypeConfig), + ]); + $this->subgroupGroupContentTypes = []; + foreach ($group_contents as $group_content) { + $this->subgroupGroupContentTypes[$group_content->entity_id->target_id][$group_content->gid->target_id] = $group_content->bundle(); + } + } + + $type = $this->subgroupGroupContentTypes[$subgroup_id][$group_id]; + return $this->subgroupContentTypeConfig[$type]; } /** @@ -256,8 +275,12 @@ $ids[] = $group_role_ref->target_id; } - // Add the implied group role IDs. - $ids[] = $membership->getGroup()->getGroupType()->getMemberRoleId(); + // We add the implied member role. Usually we should get this from the + // membership $membership->getGroup()->getGrouptype()->getMemberRoleID(), + // but since this means the whole Group and GroupType entities need to be + // loaded, this causes huge performance issues. + // @todo: Fix this hacky solution! + $ids[] = str_replace('-group_membership', '', $membership->getGroupContent()->bundle()) . '-member'; return $ids; } @@ -269,49 +292,39 @@ * original group and the supergroup. The result contains a list of all roles * we have inherited from 1 or more supergroups. * - * @param \Drupal\group\Entity\GroupInterface $supergroup - * The group for which inherited roles will be loaded. + * @param int $supergroup_id + * The group ID from which inherited roles will be loaded. * @param \Drupal\group\Entity\GroupInterface $group - * An account to map only the roles for a specific user. + * The group for which inherited roles will be loaded. * @param string[] $group_roles_to_map * An array of group roles we want to down from the supergroup. * * @return string[] * An array of group roles IDs for the group. */ - protected function getInheritedSupergroupRoleIds(GroupInterface $supergroup, GroupInterface $group, array $group_roles_to_map) { - $subgroup_content_type_configurations = $this->getSubGroupContentTypeConfig(); - + protected function getInheritedSupergroupRoleIds($supergroup_id, GroupInterface $group, array $group_roles_to_map) { $role_ids = []; - $paths = $this->groupGraphStorage->getPath($supergroup->id(), $group->id()); + $paths = $this->groupGraphStorage->getPath($supergroup_id, $group->id()); foreach ($paths as $path) { $inherited_role_ids = $group_roles_to_map; // Reverse the path since the list we want to start from the // supergroups. $reversed_path = array_reverse($path); - foreach ($reversed_path as $key => $supergroup_id) { + foreach ($reversed_path as $key => $path_supergroup_id) { // We reached the end of the path, return mapped role IDs for group. - if ($supergroup_id === $group->id()) { + if ($path_supergroup_id === $group->id()) { $role_ids = array_merge($role_ids, $inherited_role_ids); break; } // Get the subgroup ID from the next element. - $subgroup_id = isset($reversed_path[$key + 1]) ? $reversed_path[$key + 1] : NULL; - - // Get relation type between the supergroup and subgroup. - $group_content = $this->entityTypeManager->getStorage('group_content') - ->loadByProperties([ - 'type' => array_keys($subgroup_content_type_configurations), - 'gid' => $supergroup_id, - 'entity_id' => $subgroup_id, - ]); - $group_content = array_shift($group_content); + $path_subgroup_id = isset($reversed_path[$key + 1]) ? $reversed_path[$key + 1] : NULL; // Get mapped roles for relation type. Filter array to remove // unmapped roles. - $mapped_parent_roles = array_filter($subgroup_content_type_configurations[$group_content->bundle()]['parent_role_mapping']); + $plugin_config = $this->getSubGroupContentTypeConfig($path_supergroup_id, $path_subgroup_id); + $mapped_parent_roles = array_filter($plugin_config['parent_role_mapping']); // Check if we have a role to inherit and save a list of inherited // subgroup roles for the next iteration. @@ -335,46 +348,36 @@ * original group and the subgroup. The result contains a list of all roles * we have inherited from 1 or more subgroups. * - * @param \Drupal\group\Entity\GroupInterface $subgroup - * The group for which inherited roles will be loaded. + * @param int $subgroup_id + * The group ID from which inherited roles will be loaded. * @param \Drupal\group\Entity\GroupInterface $group - * An account to map only the roles for a specific user. + * The group for which inherited roles will be loaded. * @param string[] $group_roles_to_map * An array of group roles we want to up from the subgroup. * * @return string[] * An array of group roles IDs for the group. */ - protected function getInheritedSubgroupRoleIds(GroupInterface $subgroup, GroupInterface $group, array $group_roles_to_map) { - $subgroup_content_type_configurations = $this->getSubGroupContentTypeConfig(); - + protected function getInheritedSubgroupRoleIds($subgroup_id, GroupInterface $group, array $group_roles_to_map) { $role_ids = []; - $paths = $this->groupGraphStorage->getPath($group->id(), $subgroup->id()); + $paths = $this->groupGraphStorage->getPath($group->id(), $subgroup_id); foreach ($paths as $path) { $inherited_role_ids = $group_roles_to_map; - foreach ($path as $key => $subgroup_id) { + foreach ($path as $key => $path_subgroup_id) { // We reached the end of the path, store mapped role IDs. - if ($subgroup_id === $group->id()) { + if ($path_subgroup_id === $group->id()) { $role_ids = array_merge($role_ids, $inherited_role_ids); break; } // Get the subgroup ID from the next element. - $supergroup_id = isset($path[$key + 1]) ? $path[$key + 1] : NULL; - - // Get relation type between the supergroup and subgroup. - $group_content = $this->entityTypeManager->getStorage('group_content') - ->loadByProperties([ - 'type' => array_keys($subgroup_content_type_configurations), - 'gid' => $supergroup_id, - 'entity_id' => $subgroup_id, - ]); - $group_content = array_shift($group_content); + $path_supergroup_id = isset($path[$key + 1]) ? $path[$key + 1] : NULL; // Get mapped roles for relation type. Filter array to remove // unmapped roles. - $mapped_child_roles = array_filter($subgroup_content_type_configurations[$group_content->bundle()]['child_role_mapping']); + $plugin_config = $this->getSubGroupContentTypeConfig($path_supergroup_id, $path_subgroup_id); + $mapped_child_roles = array_filter($plugin_config['child_role_mapping']); // Check if we have a role to inherit and save a list of inherited // supergroup roles for the next iteration.