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

Commit

Permalink
Merge pull request #555 from Gizra/do-not-load-role-objects
Browse files Browse the repository at this point in the history
Improve performance of working with large numbers of memberships
  • Loading branch information
pfrenssen authored Aug 8, 2019
2 parents 862f7d9 + 7bee878 commit c884815
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 31 deletions.
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

0 comments on commit c884815

Please sign in to comment.