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

MembershipManager still returns a user's memberships after the user is deleted #480

Merged
merged 4 commits into from
Apr 25, 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
8 changes: 0 additions & 8 deletions og.module
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,6 @@ function og_invalidate_group_content_cache_tags(EntityInterface $entity) {
if ($is_group_content && $original) {
/** @var \Drupal\og\OgGroupAudienceHelperInterface $group_audience_helper */
$group_audience_helper = \Drupal::service('og.group_audience_helper');
$audience_has_changed = FALSE;
/** @var \Drupal\Core\Entity\FieldableEntityInterface $original */
foreach ($group_audience_helper->getAllGroupAudienceFields($entity->getEntityTypeId(), $entity->bundle()) as $field) {
$field_name = $field->getName();
Expand All @@ -342,15 +341,8 @@ function og_invalidate_group_content_cache_tags(EntityInterface $entity) {
foreach ($original_field_item_list->referencedEntities() as $old_group) {
$tags = Cache::mergeTags($tags, $old_group->getCacheTagsToInvalidate());
}
$audience_has_changed = TRUE;
}
}
if ($audience_has_changed) {
// If at least a group has changed, we clear the static cache of the
// membership manager which stores such relations. This is important
// when the membership manager was used previously in the same request.
$membership_manager->reset();
}
}

foreach ($membership_manager->getGroups($entity) as $group_entity_type_id => $groups) {
Expand Down
2 changes: 1 addition & 1 deletion og.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ services:
arguments: ['@config.factory', '@entity_type.manager', '@entity_type.bundle.info', '@event_dispatcher', '@cache.data', '@og.permission_manager', '@og.role_manager', '@router.builder', '@og.group_audience_helper']
og.membership_manager:
class: Drupal\og\MembershipManager
arguments: ['@entity_type.manager', '@og.group_audience_helper']
arguments: ['@entity_type.manager', '@og.group_audience_helper', '@cache.static']
og.permission_manager:
class: Drupal\og\PermissionManager
arguments: ['@event_dispatcher']
Expand Down
3 changes: 0 additions & 3 deletions src/Entity/OgMembership.php
Original file line number Diff line number Diff line change
Expand Up @@ -479,9 +479,6 @@ public function save() {
Og::reset();
\Drupal::service('og.access')->reset();

// Invalidate the group membership manager.
\Drupal::service('og.membership_manager')->reset();

return $result;
}

Expand Down
89 changes: 56 additions & 33 deletions src/MembershipManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
namespace Drupal\og;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
Expand All @@ -11,16 +14,16 @@
use Drupal\og\Entity\OgMembership;

/**
* Membership manager.
* Service for managing memberships and group content.
*/
class MembershipManager implements MembershipManagerInterface {

/**
* Static cache of the memberships and group association.
* The static cache backend.
*
* @var array
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
protected $staticCache;
Copy link
Collaborator

@claudiu-cristea claudiu-cristea May 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pfrenssen, I wonder why we're only doing memory caching here. This looks to me a good candidate to double the memory caching with persistent caching (database, Redis, etc.). All we need here is to declare a \Drupal\Core\Cache\BackendChain service and add a memory and a cache bin backends to it. Then replace the static cache service with this one. That would improve dramatically the performance.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened #485 to fix this.


/**
* The entity type manager.
Expand All @@ -43,10 +46,14 @@ class MembershipManager implements MembershipManagerInterface {
* The entity type manager.
* @param \Drupal\og\OgGroupAudienceHelperInterface $group_audience_helper
* The OG group audience helper.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The static cache backend.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, OgGroupAudienceHelperInterface $group_audience_helper) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, OgGroupAudienceHelperInterface $group_audience_helper, CacheBackendInterface $cache) {
assert($cache instanceof CacheTagsInvalidatorInterface, 'The cache backend must support cache tag invalidation.');
$this->entityTypeManager = $entity_type_manager;
$this->groupAudienceHelper = $group_audience_helper;
$this->staticCache = $cache;
}

/**
Expand Down Expand Up @@ -85,26 +92,26 @@ public function getMemberships(AccountInterface $user, array $states = [OgMember
// states.
$states = $this->prepareConditionArray($states, OgMembership::ALL_STATES);

$identifier = [
$cid = [
__METHOD__,
'user',
$user->id(),
implode('|', $states),
];
$identifier = implode(':', $identifier);
$cid = implode(':', $cid);

// Use cached result if it exists.
if (!isset($this->cache[$identifier])) {
if (!$membership_ids = $this->staticCache->get($cid)->data ?? []) {
$query = $this->entityTypeManager
->getStorage('og_membership')
->getQuery()
->condition('uid', $user->id())
->condition('state', $states, 'IN');

$this->cache[$identifier] = $query->execute();
$membership_ids = $query->execute();
$this->cacheMembershipIds($cid, $membership_ids);
}

return $this->loadMemberships($this->cache[$identifier]);
return $this->loadMemberships($membership_ids);
}

/**
Expand Down Expand Up @@ -140,16 +147,16 @@ public function getGroupMembershipIdsByRoleNames(EntityInterface $group, array $
$role_names = $this->prepareConditionArray($role_names);
$states = $this->prepareConditionArray($states, OgMembership::ALL_STATES);

$identifier = [
$cid = [
__METHOD__,
$group->id(),
implode('|', $role_names),
implode('|', $states),
];
$identifier = implode(':', $identifier);
$cid = implode(':', $cid);

// Only query the database if no cached result exists.
if (!isset($this->cache[$identifier])) {
if (!$membership_ids = $this->staticCache->get($cid)->data ?? []) {
$entity_type_id = $group->getEntityTypeId();

$query = $this->entityTypeManager
Expand All @@ -168,10 +175,11 @@ public function getGroupMembershipIdsByRoleNames(EntityInterface $group, array $
$query->condition('roles', $role_ids, 'IN');
}

$this->cache[$identifier] = $query->execute();
$membership_ids = $query->execute();
$this->cacheMembershipIds($cid, $membership_ids);
}

return $this->cache[$identifier];
return $membership_ids;
}

/**
Expand Down Expand Up @@ -205,29 +213,31 @@ public function getGroupIds(EntityInterface $entity, $group_type_id = NULL, $gro
throw new \InvalidArgumentException('\Drupal\og\MembershipManager::getGroupIds() cannot be used for user entities. Use \Drupal\og\MembershipManager::getUserGroups() instead.');
}

$identifier = [
$cid = [
__METHOD__,
$entity->getEntityTypeId(),
$entity->id(),
$group_type_id,
$group_bundle,
];

$identifier = implode(':', $identifier);
$cid = implode(':', $cid);

if (isset($this->cache[$identifier])) {
if ($group_ids = $this->staticCache->get($cid)->data ?? []) {
// Return cached values.
return $this->cache[$identifier];
return $group_ids;
}

$group_ids = [];
$tags = $entity->getCacheTagsToInvalidate();

$fields = $this->groupAudienceHelper->getAllGroupAudienceFields($entity->getEntityTypeId(), $entity->bundle(), $group_type_id, $group_bundle);
foreach ($fields as $field) {
$target_type = $field->getFieldStorageDefinition()->getSetting('target_type');
$target_type_id = $field->getFieldStorageDefinition()->getSetting('target_type');
$target_type_definition = $this->entityTypeManager->getDefinition($target_type_id);

// Optionally filter by group type.
if (!empty($group_type_id) && $group_type_id !== $target_type) {
if (!empty($group_type_id) && $group_type_id !== $target_type_id) {
continue;
}

Expand All @@ -249,9 +259,9 @@ public function getGroupIds(EntityInterface $entity, $group_type_id = NULL, $gro
// Query the database to get the actual list of groups. The target IDs may
// contain groups that no longer exist. Entity reference doesn't clean up
// orphaned target IDs.
$entity_type = $this->entityTypeManager->getDefinition($target_type);
$entity_type = $this->entityTypeManager->getDefinition($target_type_id);
$query = $this->entityTypeManager
->getStorage($target_type)
->getStorage($target_type_id)
->getQuery()
// Disable entity access check so fetching the groups related to group
// content are not affected by the current user. Furthermore, when
Expand All @@ -266,10 +276,14 @@ public function getGroupIds(EntityInterface $entity, $group_type_id = NULL, $gro
$query->condition($entity_type->getKey('bundle'), $group_bundle);
}

$group_ids = NestedArray::mergeDeep($group_ids, [$target_type => $query->execute()]);
// Add the list cache tags for the entity type, so that the cache gets
// invalidated if one of the group entities is deleted.
$tags = Cache::mergeTags($target_type_definition->getListCacheTags(), $tags);

$group_ids = NestedArray::mergeDeep($group_ids, [$target_type_id => $query->execute()]);
}

$this->cache[$identifier] = $group_ids;
$this->staticCache->set($cid, $group_ids, Cache::PERMANENT, $tags);

return $group_ids;
}
Expand Down Expand Up @@ -366,13 +380,6 @@ public function isMemberBlocked(EntityInterface $group, AccountInterface $user)
return $this->isMember($group, $user, [OgMembershipInterface::STATE_BLOCKED]);
}

/**
* {@inheritdoc}
*/
public function reset() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, This should have been deprecated instead of fully removed. I know we're not offering yet BC but still, given the fact that so many sites are already using OG...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we could have deprecated this.
It broke tests in #181.

$this->cache = [];
}

/**
* Prepares a conditional array for use in a cache identifier and query.
*
Expand All @@ -399,6 +406,22 @@ protected function prepareConditionArray(array $value, array $default = NULL) {
return array_unique($value);
}

/**
* Stores the given list of membership IDs in the static cache backend.
*
* @param string $cid
* The cache ID.
* @param array $membership_ids
* The list of membership IDs to store in the static cache.
*/
protected function cacheMembershipIds($cid, array $membership_ids) {
pfrenssen marked this conversation as resolved.
Show resolved Hide resolved
$tags = Cache::buildTags('og_membership', $membership_ids);
// Also invalidate the list cache tags so that if a new membership is
// created it will appear in this list.
$tags = Cache::mergeTags(['og_membership_list'], $tags);
$this->staticCache->set($cid, $membership_ids, Cache::PERMANENT, $tags);
}

/**
* Returns the full membership entities with the given memberships IDs.
*
Expand Down
5 changes: 0 additions & 5 deletions src/MembershipManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,4 @@ public function isMemberPending(EntityInterface $group, AccountInterface $user);
*/
public function isMemberBlocked(EntityInterface $group, AccountInterface $user);

/**
* Reset the internal cache.
*/
public function reset();

}
3 changes: 0 additions & 3 deletions src/Og.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,6 @@ public static function invalidateCache() {
\Drupal::entityTypeManager()->clearCachedDefinitions();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();

// Invalidate the group membership manager.
\Drupal::service('og.membership_manager')->reset();

// Let other OG modules know we invalidate cache.
\Drupal::moduleHandler()->invokeAll('og_invalidate_cache');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@ public function testCacheInvalidationOnGroupChange() {
$tags = Cache::buildTags('og-group-content', $group1->getCacheTagsToInvalidate());
$bin->set($cid, $this->randomString(), Cache::PERMANENT, $tags);

// Change the group content entity group. We're clearing first the static
// cache of membership manager because in a real application, usually, the
// static cache is warmed in a previous request, so that in the request
// where the audience is changed, is already empty.
$this->container->get('og.membership_manager')->reset();
$group_content
->set(OgGroupAudienceHelperInterface::DEFAULT_FIELD, $group2->id())
->save();
Expand Down
15 changes: 15 additions & 0 deletions tests/src/Kernel/Entity/GroupMembershipManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ protected function setUp() {
$this->installEntitySchema('og_membership');
$this->installEntitySchema('user');
$this->installSchema('system', 'sequences');
$this->installSchema('user', 'users_data');

$this->entityTypeManager = $this->container->get('entity_type.manager');
$this->membershipManager = $this->container->get('og.membership_manager');
Expand Down Expand Up @@ -566,6 +567,20 @@ protected function doTestGetGroupMembershipsByRoleNames($method_name, callable $
}
}
}

// Test that the correct memberships are returned when one of the users is
// deleted. We are using the second node group as a test case. This group
// has one pending administrator: the user with key '2'.
$group = $this->groups['node'][1];
$role_names = [OgRoleInterface::ADMINISTRATOR];
$states = [OgMembershipInterface::STATE_PENDING];
$memberships = $this->membershipManager->$method_name($group, $role_names, $states);
$this->assertCount(1, $memberships);

// Delete the user and check that it no longer appears in the result.
$this->users[2]->delete();
$memberships = $this->membershipManager->$method_name($group, $role_names, $states);
$this->assertCount(0, $memberships);
}

/**
Expand Down
11 changes: 10 additions & 1 deletion tests/src/Unit/CreateMembershipTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Drupal\Tests\og\Unit;

use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
Expand Down Expand Up @@ -43,6 +44,13 @@ class CreateMembershipTest extends UnitTestCase {
*/
protected $groupAudienceHelper;

/**
* The mocked memory cache backend.
*
* @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $staticCache;

/**
* The entity storage prophecy used in the test.
*
Expand Down Expand Up @@ -98,6 +106,7 @@ public function setUp() {
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
$this->entityTypeRepository = $this->prophesize(EntityTypeRepositoryInterface::class);
$this->groupAudienceHelper = $this->prophesize(OgGroupAudienceHelperInterface::class);
$this->staticCache = $this->prophesize(MemoryCacheInterface::class);

$this->entityTypeManager->getStorage('og_membership')
->willReturn($this->entityStorage->reveal());
Expand Down Expand Up @@ -139,7 +148,7 @@ public function setUp() {
* @covers ::createMembership
*/
public function testNewGroup() {
$membership_manager = new MembershipManager($this->entityTypeManager->reveal(), $this->groupAudienceHelper->reveal());
$membership_manager = new MembershipManager($this->entityTypeManager->reveal(), $this->groupAudienceHelper->reveal(), $this->staticCache->reveal());
$membership = $membership_manager->createMembership($this->group->reveal(), $this->user->reveal());
$this->assertInstanceOf(OgMembershipInterface::class, $membership);
}
Expand Down