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 all 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
108 changes: 91 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 @@ -84,7 +85,7 @@ class OgMembership extends ContentEntityBase implements OgMembershipInterface {
* {@inheritdoc}
*/
public function getCreatedTime(): int {
return $this->get('created')->value;
return $this->getFieldValue('created', 'value') ?: 0;
}

/**
Expand All @@ -107,8 +108,9 @@ 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;
$owner_id = $this->getFieldValue('uid', 'target_id');
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.'));
return $owner_id;
}

/**
Expand Down Expand Up @@ -141,24 +143,27 @@ 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;
$entity_type = $this->getFieldValue('entity_type', 'value') ?: '';
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.'));
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;
$bundle = $this->getFieldValue('entity_bundle', 'value') ?: '';
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.'));
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;
$entity_id = $this->getFieldValue('entity_id', 'value') ?: '';
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.'));
return $entity_id;
}

/**
Expand All @@ -179,16 +184,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($this->getFieldValue('entity_type', 'value')) &&
!empty($this->getFieldValue('entity_bundle', 'value')) &&
!empty($this->getFieldValue('entity_id', 'value'));
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 @@ -205,7 +213,7 @@ public function setState(string $state): OgMembershipInterface {
* {@inheritdoc}
*/
public function getState(): string {
return $this->get('state')->value;
return $this->getFieldValue('state', 'value') ?: '';
}

/**
Expand Down Expand Up @@ -287,9 +295,25 @@ 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_column($values, 'target_id');

// 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 Expand Up @@ -562,4 +586,54 @@ public function isOwner(): bool {
return $group instanceof EntityOwnerInterface && $group->getOwnerId() == $this->getOwnerId();
}

/**
* Gets the value of a specific property of a field.
*
* Only the first delta can be accessed with this method.
*
* @todo Remove this once issue #2580551 is fixed.
*
* @see https://www.drupal.org/project/drupal/issues/2580551
*
* @param string $field_name
* The name of the field.
* @param string $property
* The field property, "value" for many field types.
*
* @return mixed
* The value.
*/
public function getFieldValue($field_name, $property) {
// Attempt to get the value from the values directly if the field is not
// initialized yet.
if (!isset($this->fields[$field_name])) {
$field_values = NULL;
if (isset($this->values[$field_name][$this->activeLangcode])) {
$field_values = $this->values[$field_name][$this->activeLangcode];
}
elseif (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
$field_values = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
}

if ($field_values !== NULL) {
// If there are field values, try to get the property value.
// Configurable/Multi-value fields are stored differently, try accessing
// with delta and property first, then without delta and last, if the
// value is a scalar, just return that.
if (isset($field_values[0][$property]) && is_array($field_values[0])) {
return $field_values[0][$property];
}
elseif (isset($field_values[$property]) && is_array($field_values)) {
return $field_values[$property];
}
elseif (!is_array($field_values)) {
return $field_values;
}
}
}

// Fall back to access the property through the field object.
return $this->get($field_name)->$property;
}

}
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