Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/subgroup
  • issue/subgroup-3160761
  • issue/subgroup-3198179
  • issue/subgroup-3223610
  • issue/subgroup-3347570
  • issue/subgroup-3368899
  • issue/subgroup-3381010
  • issue/subgroup-3406997
  • issue/subgroup-3434818
  • issue/subgroup-3449429
  • issue/subgroup-3357760
  • issue/subgroup-3170683
12 results
Show changes
Commits on Source (11)
Showing
with 351 additions and 127 deletions
# Not copying all comments from template, original found here:
# https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/gitlab-ci/template.gitlab-ci.yml
include:
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
variables:
OPT_IN_TEST_MAX_PHP: '1'
OPT_IN_TEST_NEXT_MINOR: '1'
......@@ -2,7 +2,7 @@
"name": "drupal/subgroup",
"type": "drupal-module",
"description": "Allows you to structure groups into a hierarchical tree with permissions inheriting up or down",
"homepage": "http://drupal.org/project/subgroup",
"homepage": "https://drupal.org/project/subgroup",
"authors": [
{
"name": "Kristiaan Van den Eynde",
......@@ -17,7 +17,10 @@
"license": "GPL-2.0-or-later",
"minimum-stability": "dev",
"require": {
"drupal/core": "^9 || ^10",
"drupal/group": "^2.0"
"drupal/core": "^10.3 || ^11",
"drupal/group": "^3.0"
},
"require-dev": {
"jangregor/phpstan-prophecy": "^1.0"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="drupal-project">
<description>Default PHP CodeSniffer configuration for Drupal project.</description>
<rule ref="vendor/drupal/coder/coder_sniffer/Drupal/ruleset.xml">
<exclude name="Drupal.Semantics.FunctionT.NotLiteralString"/>
</rule>
<rule ref="Drupal.NamingConventions.ValidFunctionName.ScopeNotCamelCaps">
<exclude-pattern>./tests/src/Unit/SubgroupHandlerBaseTest</exclude-pattern>
<exclude-pattern>./tests/src/Kernel/RoleInheritanceStorageTest</exclude-pattern>
<exclude-pattern>./tests/src/Kernel/GroupSubgroupHandlerTest</exclude-pattern>
</rule>
<exclude-pattern>/.ddev</exclude-pattern>
<exclude-pattern>/.lando</exclude-pattern>
<!-- https://www.drupal.org/drupalorg/docs/drupal-ci/using-coderphpcs-in-drupalci -->
<arg name="extensions" value="php,inc,module,install,info,test,profile,theme"/>
</ruleset>
parameters:
ignoreErrors:
-
message: "#^Cannot access property \\$data on false\\.$#"
count: 1
path: tests/src/Kernel/RoleInheritanceTest.php
includes:
- phpstan-baseline.neon
parameters:
level: 2
ignoreErrors:
# new static() is a best practice in Drupal, so we cannot fix that.
- "#^Unsafe usage of new static#"
# Entity property $original is common in Drupal.
- "#^Access to an undefined property [a-zA-Z0-9\\\\]+\\:\\:\\$original.#"
# Can only remove use of membership loader in 4.0.0
- "#has typehint with deprecated interface Drupal\\\\group\\\\GroupMembershipLoaderInterface\\:#"
......@@ -3,9 +3,11 @@
namespace Drupal\subgroup\Access;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Session\AccountInterface;
use Drupal\flexible_permissions\CalculatedPermissionsItem;
use Drupal\flexible_permissions\PermissionCalculatorBase;
use Drupal\flexible_permissions\RefinableCalculatedPermissionsInterface;
use Drupal\group\Entity\GroupRoleInterface;
use Drupal\group\Entity\GroupTypeInterface;
use Drupal\group\GroupMembershipLoaderInterface;
......@@ -51,6 +53,7 @@ class InheritedGroupPermissionCalculator extends PermissionCalculatorBase {
*/
public function calculatePermissions(AccountInterface $account, $scope) {
$calculated_permissions = parent::calculatePermissions($account, $scope);
assert($calculated_permissions instanceof RefinableCalculatedPermissionsInterface);
if ($scope !== PermissionScopeInterface::INDIVIDUAL_ID) {
return $calculated_permissions;
......@@ -58,7 +61,7 @@ class InheritedGroupPermissionCalculator extends PermissionCalculatorBase {
// The inherited permissions need to be recalculated whenever the user is
// added to or removed from a group.
$calculated_permissions->addCacheTags(['group_content_list:plugin:group_membership:entity:' . $account->id()]);
$calculated_permissions->addCacheTags(['group_relationship_list:plugin:group_membership:entity:' . $account->id()]);
$group_types = $this->entityTypeManager->getStorage('group_type')->loadMultiple();
$group_handler = $this->entityTypeManager->getHandler('group', 'subgroup');
......@@ -124,7 +127,8 @@ class InheritedGroupPermissionCalculator extends PermissionCalculatorBase {
}
// Add the individual roles assigned to the member to the list.
foreach ($group_membership->getGroupRelationship()->group_roles as $group_role_ref) {
foreach ($group_membership->getGroupRelationship()->get('group_roles') as $group_role_ref) {
assert($group_role_ref instanceof EntityReferenceItem);
$role_ids[] = $group_role_ref->target_id;
}
......
......@@ -2,8 +2,12 @@
namespace Drupal\subgroup\Entity;
use Drupal\Core\Database\Transaction;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageException;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\group\Entity\GroupInterface;
use Drupal\subgroup\InvalidParentException;
use Drupal\subgroup\InvalidRootException;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -13,6 +17,11 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class GroupSubgroupHandler extends SubgroupHandlerBase {
/**
* Lock name for the database transaction to mass update a tree.
*/
const LOCK_NAME = 'subgroup_group_tree_update';
/**
* The GroupType subgroup handler.
*
......@@ -21,19 +30,38 @@ class GroupSubgroupHandler extends SubgroupHandlerBase {
protected $groupTypeHandler;
/**
* The time service.
* The field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $fieldManager;
/**
* Active database connection.
*
* @var \Drupal\Component\Datetime\TimeInterface
* @var \Drupal\Core\Database\Connection
*/
protected $time;
protected $database;
/**
* The lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
$instance = parent::createInstance($container, $entity_type);
if (!$instance->storage instanceof SqlEntityStorageInterface) {
throw new SqlContentEntityStorageException('GroupSubgroupHandler only works with SQL backends.');
}
$instance->groupTypeHandler = $container->get('entity_type.manager')->getHandler('group_type', 'subgroup');
$instance->time = $container->get('datetime.time');
$instance->fieldManager = $container->get('entity_field.manager');
$instance->database = $container->get('database');
$instance->lock = $container->get('lock');
return $instance;
}
......@@ -87,19 +115,229 @@ class GroupSubgroupHandler extends SubgroupHandlerBase {
* {@inheritdoc}
*/
protected function doAddLeaf(EntityInterface $parent, EntityInterface $child) {
// We should never be able to add groups that have existed for a while as
// leafs as you should only be able to create subgroups, not add existing
// groups as subgroups. 60 seconds seems plenty for a request that created
// the group to get to this point of adding it as a subgroup.
/** @var \Drupal\group\Entity\GroupInterface $child */
$leaf_lifetime = $this->time->getCurrentTime() - $child->getCreatedTime();
assert($leaf_lifetime <= 60);
assert($parent instanceof GroupInterface);
assert($child instanceof GroupInterface);
if ($this->groupTypeHandler->getParent($child->getGroupType())->id() !== $parent->bundle()) {
throw new InvalidParentException('Provided group cannot be added as a leaf to the parent (incompatible group types).');
}
parent::doAddLeaf($parent, $child);
// We do not call the parent here but instead write a few giant queries to
// avoid performance issues when updating large trees of group entities.
$this->acquireLock();
// Get the live parent values to avoid race condition issues.
$parent_values = $this->getLiveLeafValues($parent);
// Get all of the IDs that will be affected before we do anything.
$ids_to_update = $this->storage->getQuery()
->condition($this->getRightPropertyName(), $parent_values[SUBGROUP_RIGHT_FIELD], '>=')
->condition($this->getTreePropertyName(), $parent_values[SUBGROUP_TREE_FIELD])
->accessCheck(FALSE)
->execute();
$transaction = NULL;
try {
// Start a transaction for the tree updates.
$transaction = $this->database->startTransaction(self::LOCK_NAME);
// Run a few update queries to make room for the new child.
$this->makeRoomForNewChild($parent_values[SUBGROUP_TREE_FIELD], $parent_values[SUBGROUP_RIGHT_FIELD]);
// And finally add the new leaf (Using the original right value to work
// out a 2 unit span).
$this->writeLeafData(
$child,
$parent_values[SUBGROUP_DEPTH_FIELD] + 1,
$parent_values[SUBGROUP_RIGHT_FIELD],
$parent_values[SUBGROUP_RIGHT_FIELD] + 1,
$parent_values[SUBGROUP_TREE_FIELD]
);
// Update the leaf data on the passed in parent so that code using the old
// object reference still gets the new values.
$parent->set(SUBGROUP_RIGHT_FIELD, $parent_values[SUBGROUP_RIGHT_FIELD] + 2);
}
// Something went wrong: We roll back the transaction and release the lock.
catch (\Exception $e) {
if ($transaction instanceof Transaction) {
$transaction->rollBack();
}
$this->releaseLock();
throw $e;
}
// All the affected entities need to be cleared from the storage's cache.
$this->storage->resetCache($ids_to_update);
$this->releaseLock();
}
/**
* {@inheritdoc}
*/
protected function doRemoveLeaf(EntityInterface $entity, $save) {
$leaf = $this->wrapLeaf($entity);
// If the left and right values are 2 and 3 respectively it means we might
// be removing the last child of a tree root. In this case, we unset the
// tree altogether.
if ($leaf->getLeft() === 2 && $leaf->getRight() === 3 && $this->getDescendantCount($root = $this->storage->load($leaf->getTree())) === 1) {
$this->clearLeafData($root, TRUE);
return;
}
// We do not call the parent here but instead write a few giant queries to
// avoid performance issues when updating large trees of group entities.
$this->acquireLock();
// Keep local copies of the tree values before we clear it.
$leaf_right = $leaf->getRight();
$leaf_tree = $leaf->getTree();
// Get all of the IDs that will be affected before we do anything.
$ids_to_update = $this->storage->getQuery()
->condition($this->getRightPropertyName(), $leaf_right, '>=')
->condition($this->getTreePropertyName(), $leaf_tree)
->accessCheck(FALSE)
->execute();
$transaction = NULL;
try {
// Start a transaction for the tree updates.
$transaction = $this->database->startTransaction(self::LOCK_NAME);
// We can now remove the leaf from the tree.
$this->clearLeafData($entity, $save);
// Run a few update queries to clean up excessive space after removal.
$this->cleanUpTreeAfterRemoval($leaf_tree, $leaf_right);
}
// Something went wrong: We roll back the transaction and release the lock.
catch (\Exception $e) {
if ($transaction instanceof Transaction) {
$transaction->rollBack();
}
$this->releaseLock();
throw $e;
}
// All the affected entities need to be cleared from the storage's cache.
$this->storage->resetCache($ids_to_update);
$this->releaseLock();
}
/**
* Retrieves the live leaf values directly from the database.
*
* @param \Drupal\group\Entity\GroupInterface $group
* The group to retrieve the leaf values for.
*
* @return array
* The leaf values, keyed by their field names.
*/
protected function getLiveLeafValues(GroupInterface $group) {
assert($this->storage instanceof SqlEntityStorageInterface);
$table_mapping = $this->storage->getTableMapping();
$field_storage = $this->fieldManager->getFieldStorageDefinitions('group');
$query = $this->database->select($table_mapping->getFieldTableName(SUBGROUP_DEPTH_FIELD), 'd');
$query->condition('d.entity_id', $group->id());
$query->innerJoin($table_mapping->getFieldTableName(SUBGROUP_LEFT_FIELD), 'l', 'l.entity_id = d.entity_id');
$query->innerJoin($table_mapping->getFieldTableName(SUBGROUP_RIGHT_FIELD), 'r', 'r.entity_id = d.entity_id');
$query->innerJoin($table_mapping->getFieldTableName(SUBGROUP_TREE_FIELD), 't', 't.entity_id = d.entity_id');
$query->addField('d', $table_mapping->getFieldColumnName($field_storage[SUBGROUP_DEPTH_FIELD], 'value'), SUBGROUP_DEPTH_FIELD);
$query->addField('l', $table_mapping->getFieldColumnName($field_storage[SUBGROUP_LEFT_FIELD], 'value'), SUBGROUP_LEFT_FIELD);
$query->addField('r', $table_mapping->getFieldColumnName($field_storage[SUBGROUP_RIGHT_FIELD], 'value'), SUBGROUP_RIGHT_FIELD);
$query->addField('t', $table_mapping->getFieldColumnName($field_storage[SUBGROUP_TREE_FIELD], 'value'), SUBGROUP_TREE_FIELD);
return $query->execute()->fetchAssoc();
}
/**
* Executes a few update queries to make room for a new child.
*
* @param int $tree_id
* The tree ID.
* @param int $parent_right
* The parent's current right value.
*/
protected function makeRoomForNewChild($tree_id, $parent_right) {
$this->updateAllLeavesAfterRightValue($tree_id, $parent_right, '+ 2');
}
/**
* Executes a few update queries to remove a child from a tree.
*
* @param int $tree_id
* The tree ID.
* @param int $child_right
* The removed child's right value.
*/
protected function cleanUpTreeAfterRemoval($tree_id, $child_right) {
$this->updateAllLeavesAfterRightValue($tree_id, $child_right, '- 2');
}
/**
* Updates all leaves of a tree beyond a certain right value.
*
* @param int $tree_id
* The tree ID.
* @param int $right_value
* The right value that serves as the cutoff point.
* @param int $change
* The change to enact on the rows: Probably + 2 or - 2.
*/
protected function updateAllLeavesAfterRightValue($tree_id, $right_value, $change) {
assert($this->storage instanceof SqlEntityStorageInterface);
$table_mapping = $this->storage->getTableMapping();
$field_storage = $this->fieldManager->getFieldStorageDefinitions('group');
$tree_table = $table_mapping->getFieldTableName(SUBGROUP_TREE_FIELD);
$tree_value_field = $table_mapping->getFieldColumnName($field_storage[SUBGROUP_TREE_FIELD], 'value');
// Normally you check for 'right >= parent_right' and 'left > parent_right',
// But seeing as how a left value should never be identical to a right value
// we can simplify this by simply checking for >= on both queries.
$queries = [];
foreach ([SUBGROUP_RIGHT_FIELD, SUBGROUP_LEFT_FIELD] as $field_name) {
foreach ($table_mapping->getAllFieldTableNames($field_name) as $table_name) {
$value_field = $table_mapping->getFieldColumnName($field_storage[$field_name], 'value');
$queries[] = "
UPDATE {" . $table_name . "} t1
INNER JOIN {" . $tree_table . "} t2 ON t1.entity_id = t2.entity_id
SET t1.{$value_field} = t1.{$value_field} {$change}
WHERE t1.{$value_field} >= {$right_value}
AND t2.{$tree_value_field} = {$tree_id}
";
}
}
foreach ($queries as $query) {
$this->database->query($query);
}
}
/**
* Locks the database for mass tree updates.
*/
protected function acquireLock() {
// Use a database lock to avoid two processes updating a tree at the same
// time. This will use the default lock backend waiting time of 30 seconds.
$acquired_lock = FALSE;
do {
if ($this->lock->lockMayBeAvailable(self::LOCK_NAME) && $this->lock->acquire(self::LOCK_NAME)) {
$acquired_lock = TRUE;
}
} while (!$acquired_lock);
}
/**
* Release the lock used for mass tree updates.
*/
protected function releaseLock() {
$this->lock->release(self::LOCK_NAME);
}
/**
......
......@@ -128,88 +128,4 @@ class GroupTypeSubgroupHandler extends SubgroupHandlerBase {
return 'third_party_settings.subgroup.' . SUBGROUP_TREE_SETTING;
}
/**
* {@inheritdoc}
*/
public function getAncestors(EntityInterface $entity) {
$this->verify($entity);
if ($this->isRoot($entity)) {
throw new InvalidLeafException('Trying to get the ancestors of a root leaf.');
}
$leaf = $this->wrapLeaf($entity);
$entity_ids = $this->storage->getQuery()
->condition($this->getLeftPropertyName(), $leaf->getLeft(), '<')
->condition($this->getRightPropertyName(), $leaf->getRight(), '>')
->condition($this->getTreePropertyName(), $leaf->getTree())
->accessCheck(FALSE)
->execute();
assert(count($entity_ids) >= 1);
return $this->sortByLeftProperty($entity_ids);
}
/**
* {@inheritdoc}
*/
public function getChildren(EntityInterface $entity) {
$this->verify($entity);
$leaf = $this->wrapLeaf($entity);
$entity_ids = $this->storage->getQuery()
->condition($this->getDepthPropertyName(), $leaf->getDepth() + 1)
->condition($this->getLeftPropertyName(), $leaf->getLeft(), '>')
->condition($this->getRightPropertyName(), $leaf->getRight(), '<')
->condition($this->getTreePropertyName(), $leaf->getTree())
->accessCheck(FALSE)
->execute();
return $this->sortByLeftProperty($entity_ids);
}
/**
* {@inheritdoc}
*/
public function getDescendants(EntityInterface $entity) {
$this->verify($entity);
$leaf = $this->wrapLeaf($entity);
$entity_ids = $this->storage->getQuery()
->condition($this->getLeftPropertyName(), $leaf->getLeft(), '>')
->condition($this->getRightPropertyName(), $leaf->getRight(), '<')
->condition($this->getTreePropertyName(), $leaf->getTree())
->accessCheck(FALSE)
->execute();
return $this->sortByLeftProperty($entity_ids);
}
/**
* Work-around for a core bug regarding dotted path sorting.
*
* @param string[] $entity_ids
* Group type IDs to load and sort by left property.
*
* @return \Drupal\group\Entity\GroupTypeInterface[]
* A list of sorted group types.
*
* @todo Remove this along with ::getAncestors(), ::getChildren() and
* ::getDescendants() as soon as the core fix is released.
*
* @see https://www.drupal.org/project/drupal/issues/2942569
*/
private function sortByLeftProperty(array $entity_ids) {
$group_types = [];
if (!empty($entity_ids)) {
$group_types = $this->storage->loadMultiple($entity_ids);
uasort($group_types, function (GroupTypeInterface $a, GroupTypeInterface $b) {
return $a->getThirdPartySetting('subgroup', 'left') <=> $b->getThirdPartySetting('subgroup', 'left');
});
}
return $group_types;
}
}
......@@ -148,6 +148,7 @@ class RoleInheritance extends ConfigEntityBase implements RoleInheritanceInterfa
parent::calculateDependencies();
$this->addDependency('config', $this->getSource()->getConfigDependencyName());
$this->addDependency('config', $this->getTarget()->getConfigDependencyName());
return $this;
}
}
......@@ -2,8 +2,8 @@
namespace Drupal\subgroup\Event;
use Drupal\Component\EventDispatcher\Event;
use Drupal\group\Entity\GroupInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Defines the event for group leaf status changes.
......
......@@ -2,8 +2,8 @@
namespace Drupal\subgroup\Event;
use Drupal\Component\EventDispatcher\Event;
use Drupal\group\Entity\GroupTypeInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Defines the event for group type leaf status changes.
......
......@@ -62,7 +62,7 @@ class GroupTypeLeafSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
public static function getSubscribedEvents(): array {
$events[LeafEvents::GROUP_TYPE_LEAF_ADD] = 'onAddLeaf';
$events[LeafEvents::GROUP_TYPE_LEAF_IMPORT] = 'onImportLeaf';
$events[LeafEvents::GROUP_TYPE_LEAF_REMOVE] = 'onRemoveLeaf';
......@@ -86,7 +86,7 @@ class GroupTypeLeafSubscriber implements EventSubscriberInterface {
$parent = $this->subgroupHandler->getParent($group_type);
/** @var \Drupal\group\Entity\Storage\GroupRelationshipTypeStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('group_content_type');
$storage = $this->entityTypeManager->getStorage('group_relationship_type');
$storage->save($storage->createFromPlugin($parent, 'subgroup:' . $group_type->id()));
}
}
......@@ -118,7 +118,7 @@ class GroupTypeLeafSubscriber implements EventSubscriberInterface {
$role_inheritance_storage->deleteForGroupType($group_type, $this->subgroupHandler->wrapLeaf($original)->getTree());
/** @var \Drupal\group\Entity\Storage\GroupRelationshipTypeStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('group_content_type');
$storage = $this->entityTypeManager->getStorage('group_relationship_type');
$storage->delete($storage->loadByPluginId('subgroup:' . $group_type->id()));
$this->fieldManager->deleteFields($group_type->id());
......
......@@ -52,7 +52,7 @@ class TreeCacheTagInvalidator implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
public static function getSubscribedEvents(): array {
$events[LeafEvents::GROUP_LEAF_ADD] = 'onAddGroupLeaf';
$events[LeafEvents::GROUP_LEAF_REMOVE] = 'onRemoveGroupLeaf';
$events[LeafEvents::GROUP_TYPE_LEAF_ADD] = 'onAddGroupTypeLeaf';
......
......@@ -8,8 +8,10 @@ use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\group\Entity\GroupRoleInterface;
use Drupal\group\Entity\GroupTypeInterface;
use Drupal\group\PermissionScopeInterface;
use Drupal\subgroup\Entity\SubgroupHandlerInterface;
use Drupal\user\RoleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -133,6 +135,7 @@ class SubgroupSettingsForm extends FormBase {
$form['not_available']['#markup'] = $this->t('<p>You cannot create a new tree because there are fewer than 2 group types available that are not already part of a tree.</p>');
}
else {
$options = [];
$storage = $this->entityTypeManager->getStorage('group_type');
foreach ($storage->loadMultiple($group_type_ids) as $group_type_id => $group_type) {
$options[$group_type_id] = $group_type->label();
......@@ -262,18 +265,19 @@ class SubgroupSettingsForm extends FormBase {
$parent_id = array_keys($current_parents)[count($current_parents) - 2];
$relationship_type_ids = $this->entityTypeManager
->getStorage('group_content_type')
->getStorage('group_relationship_type')
->getQuery()
->condition('group_type', $parent_id)
->condition('content_plugin', "subgroup:$id")
->accessCheck(FALSE)
->execute();
$form['table'][$id]['configure'] = [
'#type' => 'link',
'#title' => $this->t('Configure plugin'),
'#url' => Url::fromRoute(
'entity.group_content_type.edit_form',
['group_content_type' => reset($relationship_type_ids)],
'entity.group_relationship_type.edit_form',
['group_relationship_type' => reset($relationship_type_ids)],
['query' => ['destination' => Url::fromRoute('subgroup.settings')->toString()]]
),
'#attributes' => ['class' => ['button']],
......@@ -361,6 +365,7 @@ class SubgroupSettingsForm extends FormBase {
'#required' => TRUE,
];
$options = [];
$storage = $this->entityTypeManager->getStorage('group_type');
foreach ($storage->loadMultiple($group_type_ids) as $group_type_id => $group_type) {
$options[$group_type_id] = $group_type->label();
......@@ -531,7 +536,9 @@ class SubgroupSettingsForm extends FormBase {
]);
$group_roles = array_merge($member_roles, $individual_roles);
$options = [];
foreach ($group_roles as $group_role_id => $group_role) {
assert($group_role instanceof GroupRoleInterface);
$options[$group_role_id] = $group_role->getGroupType()->label() . ' - ' . $group_role->label();
}
......@@ -579,6 +586,7 @@ class SubgroupSettingsForm extends FormBase {
->getQuery()
->condition('source', $source)
->condition('target', $target)
->accessCheck(FALSE)
->count()
->execute();
if ($exists) {
......@@ -588,9 +596,11 @@ class SubgroupSettingsForm extends FormBase {
$storage = $this->entityTypeManager->getStorage('group_role');
$source_group_role = $storage->load($source);
$target_group_role = $storage->load($target);
assert($source_group_role instanceof GroupRoleInterface);
assert($target_group_role instanceof GroupRoleInterface);
/** @var \Drupal\subgroup\Entity\SubgroupHandlerInterface $subgroup_handler */
$subgroup_handler = $this->entityTypeManager->getHandler('group_type', 'subgroup');
assert($subgroup_handler instanceof SubgroupHandlerInterface);
if (!$subgroup_handler->areVerticallyRelated($source_group_role->getGroupType(), $target_group_role->getGroupType())) {
$form_state->setErrorByName(implode('][', $parents), $this->t('Source and target are not ancestors or descendants of one another.'));
}
......
......@@ -2,6 +2,7 @@
namespace Drupal\subgroup;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\group\Entity\GroupInterface;
/**
......@@ -82,7 +83,9 @@ class GroupLeaf implements LeafInterface {
* The type-safe value of the field. In this implementation, an integer.
*/
protected function getTypeSafeValue($field_name) {
return $this->group->get($field_name)->first()->get('value')->getCastedValue();
$value = $this->group->get($field_name)->first()->get('value');
assert($value instanceof PrimitiveInterface);
return $value->getCastedValue();
}
}
......@@ -2,8 +2,8 @@
namespace Drupal\subgroup\Plugin\Group\Relation;
use Drupal\group\Plugin\Group\Relation\GroupRelationBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\group\Plugin\Group\Relation\GroupRelationBase;
/**
* Provides a group relation type for subgroups.
......
......@@ -2,8 +2,8 @@
namespace Drupal\subgroup\Plugin\Group\Relation;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\subgroup\Entity\SubgroupHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -30,7 +30,7 @@ class SubgroupDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* Constructs a new SubgroupDeriver.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage
* The group type storage.
* @param \Drupal\subgroup\Entity\SubgroupHandlerInterface $subgroup_handler
* The group type subgroup handler.
......
......@@ -2,7 +2,7 @@ name: 'Subgroup'
description: 'Allows you to structure groups into a hierarchical tree with permissions inheriting up or down'
package: 'Group'
type: 'module'
core_version_requirement: ^9 || ^10
core_version_requirement: ^10.3 || ^11
configure: 'subgroup.settings'
dependencies:
- 'group:group'
......@@ -12,13 +12,14 @@ use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Session\AccountInterface;
use Drupal\group\Entity\GroupInterface;
use Drupal\group\Entity\GroupTypeInterface;
use Drupal\subgroup\GroupLeaf;
use Drupal\subgroup\GroupTypeLeaf;
use Drupal\subgroup\Entity\GroupSubgroupHandler;
use Drupal\subgroup\Entity\GroupTypeSubgroupHandler;
use Drupal\subgroup\Entity\SubgroupHandlerInterface;
use Drupal\subgroup\Event\GroupLeafEvent;
use Drupal\subgroup\Event\GroupTypeLeafEvent;
use Drupal\subgroup\Event\LeafEvents;
use Drupal\subgroup\GroupLeaf;
use Drupal\subgroup\GroupTypeLeaf;
/**
* The name of the leaf depth bundle field.
......@@ -86,7 +87,7 @@ function subgroup_group_update(GroupInterface $group) {
// Find out whether the group was added to or removed from a tree and dispatch
// the appropriate event.
if ($current_is_leaf !== $original_is_leaf) {
/** @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher */
/** @var \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher */
$event_dispatcher = \Drupal::service('event_dispatcher');
$event = new GroupLeafEvent($group);
if ($current_is_leaf) {
......@@ -109,7 +110,7 @@ function subgroup_group_type_insert(GroupTypeInterface $group_type) {
// Subgroup metadata, we need to fire the import event so we can, for example,
// clear the plugin definitions cache.
if ($subgroup_handler->isLeaf($group_type) && $group_type->isSyncing()) {
/** @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher */
/** @var \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher */
$event_dispatcher = \Drupal::service('event_dispatcher');
$event = new GroupTypeLeafEvent($group_type);
$event_dispatcher->dispatch($event, LeafEvents::GROUP_TYPE_LEAF_IMPORT);
......@@ -131,7 +132,7 @@ function subgroup_group_type_update(GroupTypeInterface $group_type) {
// Find out whether the group type was added to or removed from a tree and
// dispatch the appropriate event.
if ($current_is_leaf !== $original_is_leaf) {
/** @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $event_dispatcher */
/** @var \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher */
$event_dispatcher = \Drupal::service('event_dispatcher');
$event = new GroupTypeLeafEvent($group_type);
if ($group_type->isSyncing()) {
......@@ -150,11 +151,11 @@ function subgroup_group_type_update(GroupTypeInterface $group_type) {
* Implements hook_ENTITY_TYPE_create_access().
*/
function subgroup_group_create_access(AccountInterface $account, array $context, $entity_bundle) {
/** @var \Drupal\group\Entity\GroupInterface $entity */
$group_type = \Drupal::entityTypeManager()->getStorage('group_type')->load($entity_bundle);
assert($group_type instanceof GroupTypeInterface);
/** @var \Drupal\subgroup\Entity\SubgroupHandlerInterface $group_type_handler */
$group_type_handler = \Drupal::entityTypeManager()->getHandler('group_type', 'subgroup');
assert($group_type_handler instanceof SubgroupHandlerInterface);
if ($group_type_handler->isLeaf($group_type) && !$group_type_handler->isRoot($group_type)) {
$access = AccessResult::forbidden('Cannot create a group globally if its group type is a non-root leaf of a tree.');
......@@ -215,7 +216,7 @@ function subgroup_entity_delete(EntityInterface $entity) {
/**
* Implements hook_ENTITY_TYPE_access().
*/
function subgroup_group_content_access(EntityInterface $entity, $operation, AccountInterface $account) {
function subgroup_group_relationship_access(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\group\Entity\GroupRelationshipInterface $entity */
if ($operation === 'delete' && $entity->getPlugin()->getBaseId() === 'subgroup') {
return AccessResult::forbidden('Cannot delete a subgroup group relationship entity directly.')->setCacheMaxAge(Cache::PERMANENT);
......@@ -226,7 +227,7 @@ function subgroup_group_content_access(EntityInterface $entity, $operation, Acco
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function subgroup_group_content_insert(EntityInterface $entity) {
function subgroup_group_relationship_insert(EntityInterface $entity) {
/** @var \Drupal\group\Entity\GroupRelationshipInterface $entity */
if ($entity->getPlugin()->getBaseId() === 'subgroup') {
$parent = $entity->getGroup();
......@@ -244,7 +245,7 @@ function subgroup_group_content_insert(EntityInterface $entity) {
/**
* Implements hook_ENTITY_TYPE_predelete().
*/
function subgroup_group_content_predelete(EntityInterface $entity) {
function subgroup_group_relationship_predelete(EntityInterface $entity) {
/** @var \Drupal\group\Entity\GroupRelationshipInterface $entity */
if ($entity->getPlugin()->getBaseId() === 'subgroup') {
if ($entity->getEntity()) {
......
......@@ -2,6 +2,6 @@ name: 'Subgroup test'
description: 'Support module for Subgroup tests.'
package: 'Testing'
type: 'module'
core_version_requirement: ^8.8 || ^9
core_version_requirement: ^9.5 || ^10
dependencies:
- 'subgroup:subgroup'