Skip to content
This repository has been archived by the owner on Aug 18, 2024. It is now read-only.

Improve performance of working with large numbers of memberships #555

Merged
merged 13 commits into from
Aug 8, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/Cache/Context/OgRoleCacheContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\og\MembershipManagerInterface;
use Drupal\og\OgRoleInterface;

/**
* Defines a cache context service for the OG roles of the current user.
Expand Down Expand Up @@ -88,9 +87,13 @@ public function getContext() {
if (empty($this->hashes[$this->user->id()])) {
$memberships = [];
foreach ($this->membershipManager->getMemberships($this->user->id()) as $membership) {
$role_names = array_map(function (OgRoleInterface $role) {
return $role->getName();
}, $membership->getRoles());
// Derive the role names from the role IDs. This is faster than loading
// the OgRole object from the membership.
$role_names = array_map(function (string $role_id) use ($membership): string {
$pattern = preg_quote("{$membership->getGroupEntityType()}-{$membership->getGroupBundle()}-");
preg_match("/$pattern(.+)/", $role_id, $matches);
return $matches[1];
}, $membership->getRolesIds());
if ($role_names) {
$memberships[$membership->getGroupEntityType()][$membership->getGroupId()] = $role_names;
}
Expand Down
128 changes: 111 additions & 17 deletions src/Entity/OgMembership.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\user\UserInterface;
use Drupal\og\Og;
use Drupal\og\OgMembershipInterface;
Expand Down Expand Up @@ -83,7 +84,19 @@ class OgMembership extends ContentEntityBase implements OgMembershipInterface {
* {@inheritdoc}
*/
public function getCreatedTime(): int {
return $this->get('created')->value;
// Only use $this->get() if it is already populated. If it is not available
// then use the raw value. This field is not translatable so we do not need
// the slow field definition lookup from $this->getTranslatedField().
if (isset($this->fields['created'][LanguageInterface::LANGCODE_DEFAULT])) {
MPParsley marked this conversation as resolved.
Show resolved Hide resolved
$created_time = $this->get('created')->value;
}
else {
$created_time = $this->values['created'][LanguageInterface::LANGCODE_DEFAULT] ?? 0;
}
if (is_array($created_time)) {
return reset($created_time)['value'];
}
return $created_time;
}

/**
Expand All @@ -106,8 +119,22 @@ public function getOwner() {
* {@inheritdoc}
*/
public function getOwnerId() {
assert(!empty($this->get('uid')->entity), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the owner set.'));
return $this->get('uid')->target_id;
// Only use $this->get() if it is already populated. If it is not available
// then use the raw value. This field is not translatable so we do not need
// the slow field definition lookup from $this->getTranslatedField().
if (isset($this->fields['uid'][LanguageInterface::LANGCODE_DEFAULT])) {
$owner_id = $this->get('uid')->target_id;
}
else {
$owner_id = $this->values['uid'][LanguageInterface::LANGCODE_DEFAULT] ?? NULL;
}

assert(!empty($owner_id), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the owner set.'));

if (is_array($owner_id)) {
return reset($owner_id)['target_id'];
}
return $owner_id;
}

/**
Expand Down Expand Up @@ -140,24 +167,66 @@ public function setGroup(ContentEntityInterface $group): OgMembershipInterface {
* {@inheritdoc}
*/
public function getGroupEntityType(): string {
assert(!empty($this->get('entity_type')->value), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group type set.'));
return $this->get('entity_type')->value;
// Only use $this->get() if it is already populated. If it is not available
// then use the raw value. This field is not translatable so we do not need
// the slow field definition lookup from $this->getTranslatedField().
if (isset($this->fields['entity_type'][LanguageInterface::LANGCODE_DEFAULT])) {
$entity_type = $this->get('entity_type')->value;
}
else {
$entity_type = $this->values['entity_type'][LanguageInterface::LANGCODE_DEFAULT] ?? '';
}

assert(!empty($entity_type), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group type set.'));

if (is_array($entity_type)) {
return reset($entity_type)['value'];
}
return $entity_type;
}

/**
* {@inheritdoc}
*/
public function getGroupBundle(): string {
assert(!empty($this->get('entity_bundle')->value), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group bundle set.'));
return $this->get('entity_bundle')->value;
// Only use $this->get() if it is already populated. If it is not available
// then use the raw value. This field is not translatable so we do not need
// the slow field definition lookup from $this->getTranslatedField().
if (isset($this->fields['entity_bundle'][LanguageInterface::LANGCODE_DEFAULT])) {
$bundle = $this->get('entity_bundle')->value;
}
else {
$bundle = $this->values['entity_bundle'][LanguageInterface::LANGCODE_DEFAULT] ?? '';
}

assert(!empty($bundle), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group bundle set.'));

if (is_array($bundle)) {
return reset($bundle)['value'];
}
return $bundle;
}

/**
* {@inheritdoc}
*/
public function getGroupId(): string {
assert(!empty($this->get('entity_id')->value), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group ID set.'));
return $this->get('entity_id')->value;
// Only use $this->get() if it is already populated. If it is not available
// then use the raw value. This field is not translatable so we do not need
// the slow field definition lookup from $this->getTranslatedField().
if (isset($this->fields['entity_id'][LanguageInterface::LANGCODE_DEFAULT])) {
$entity_id = $this->get('entity_id')->value;
}
else {
$entity_id = $this->values['entity_id'][LanguageInterface::LANGCODE_DEFAULT] ?? '';
}

assert(!empty($entity_id), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group ID set.'));

if (is_array($entity_id)) {
return reset($entity_id)['value'];
}
return $entity_id;
}

/**
Expand All @@ -178,16 +247,19 @@ public function getGroupId(): string {
* Whether or not the group is already present.
*/
protected function hasGroup(): bool {
return !empty($this->get('entity_type')->value) && !empty($this->get('entity_bundle')->value) && !empty($this->get('entity_id')->value);
$has_group =
!empty(isset($this->fields['entity_type'][LanguageInterface::LANGCODE_DEFAULT]) ? $this->get('entity_type')->value : ($this->values['entity_type'][LanguageInterface::LANGCODE_DEFAULT] ?? NULL)) &&
!empty(isset($this->fields['entity_bundle'][LanguageInterface::LANGCODE_DEFAULT]) ? $this->get('entity_bundle')->value : ($this->values['entity_bundle'][LanguageInterface::LANGCODE_DEFAULT] ?? NULL)) &&
!empty(isset($this->fields['entity_id'][LanguageInterface::LANGCODE_DEFAULT]) ? $this->get('entity_id')->value : ($this->values['entity_id'][LanguageInterface::LANGCODE_DEFAULT] ?? NULL));
return $has_group;
}

/**
* {@inheritdoc}
*/
public function getGroup(): ?ContentEntityInterface {
assert(!empty($this->get('entity_type')->value) || !empty($this->get('entity_id')->value), new \LogicException(__METHOD__ . '() should only be called on loaded memberships, or on newly created memberships that already have the group set.'));
$entity_type = $this->get('entity_type')->value;
$entity_id = $this->get('entity_id')->value;
$entity_type = $this->getGroupEntityType();
$entity_id = $this->getGroupId();

return \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id);
}
Expand All @@ -204,7 +276,11 @@ public function setState(string $state): OgMembershipInterface {
* {@inheritdoc}
*/
public function getState(): string {
return $this->get('state')->value;
$state = $this->values['state'][LanguageInterface::LANGCODE_DEFAULT] ?? $this->get('state')->value;
pfrenssen marked this conversation as resolved.
Show resolved Hide resolved
if (is_array($state)) {
return reset($state)['value'];
}
return $state;
}

/**
Expand Down Expand Up @@ -286,9 +362,27 @@ public function setRoles(array $roles = []): OgMembershipInterface {
* {@inheritdoc}
*/
public function getRolesIds(): array {
return array_map(function (OgRole $role) {
return $role->id();
}, $this->getRoles());
// Only use $this->get() if it is already populated. If it is not available
// then use the raw value. This field is not translatable so we do not need
// the slow field definition lookup from $this->getTranslatedField().
if (isset($this->fields['roles'][LanguageInterface::LANGCODE_DEFAULT])) {
$values = $this->get('roles')->getValue();
}
else {
$values = $this->values['roles'][LanguageInterface::LANGCODE_DEFAULT] ?? [];
}

$roles_ids = array_map(function (array $role_data): string {
pfrenssen marked this conversation as resolved.
Show resolved Hide resolved
return $role_data['target_id'];
}, $values);

// Add the member role. This is only possible if a group has been set on the
// membership.
if ($this->hasGroup()) {
$roles_ids[] = "{$this->getGroupEntityType()}-{$this->getGroupBundle()}-" . OgRoleInterface::AUTHENTICATED;
}

return $roles_ids;
}

/**
Expand Down
16 changes: 6 additions & 10 deletions tests/src/Unit/Cache/Context/OgRoleCacheContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ public function testMembershipsWithOrphanedRole() {
// group entity type, but no roles.
/** @var \Drupal\og\OgMembershipInterface|\Prophecy\Prophecy\ObjectProphecy $membership */
$membership = $this->prophesize(OgMembershipInterface::class);
$membership->getGroupEntityType()->willReturn('test_entity');
$membership->getGroupId()->willReturn('test_id');
$membership->getRoles()->willReturn([]);
$membership->getRolesIds()->willReturn([]);

// The membership with the orphaned role will be returned by the membership
// manager.
Expand Down Expand Up @@ -137,19 +135,17 @@ public function testMemberships(array $group_memberships, array $expected_identi
$memberships[$user_id] = [];
foreach ($group_entity_type_ids as $group_entity_type_id => $group_ids) {
foreach ($group_ids as $group_id => $roles) {
// Mock the role objects that will be contained in the memberships.
$roles = array_map(function ($role_name) {
/** @var \Drupal\og\OgRoleInterface|\Prophecy\Prophecy\ObjectProphecy $role */
$role = $this->prophesize(OgRoleInterface::class);
$role->getName()->willReturn($role_name);
return $role->reveal();
// Construct the role IDs that will be returned by the membership.
$roles_ids = array_map(function (string $role_name) use ($group_entity_type_id) {
return "{$group_entity_type_id}-bundle-{$role_name}";
}, $roles);
// Mock the expected returns of method calls on the membership.
/** @var \Drupal\og\OgMembershipInterface|\Prophecy\Prophecy\ObjectProphecy $membership */
$membership = $this->prophesize(OgMembershipInterface::class);
$membership->getGroupEntityType()->willReturn($group_entity_type_id);
$membership->getGroupBundle()->willReturn('bundle');
$membership->getGroupId()->willReturn($group_id);
$membership->getRoles()->willReturn($roles);
$membership->getRolesIds()->willReturn($roles_ids);
$memberships[$user_id][++$membership_id] = $membership->reveal();
}
}
Expand Down