og-8.x-1.x-dev/src/Entity/OgRole.php
src/Entity/OgRole.php
<?php
declare(strict_types=1);
namespace Drupal\og\Entity;
use Drupal\Core\Config\Action\Attribute\ActionMethod;
use Drupal\Core\Config\ConfigValueException;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\og\Exception\OgRoleException;
use Drupal\og\GroupTypeManagerInterface;
use Drupal\og\OgAccessInterface;
use Drupal\og\OgRoleAccessControlHandler;
use Drupal\og\OgRoleInterface;
use Drupal\og\OgRoleStorage;
use Drupal\og_ui\Form\OgRoleDeleteForm;
use Drupal\og_ui\Form\OgRoleForm;
/**
* Defines the OG user role entity class.
*
* @ConfigEntityType(
* id = "og_role",
* label = @Translation("OG role"),
* label_collection = @Translation("OG roles"),
* label_singular = @Translation("OG role"),
* label_plural = @Translation("OG roles"),
* label_count = @PluralTranslation(
* singular = "@count OG role",
* plural = "@count OG roles",
* ),
* handlers = {
* "storage" = "Drupal\og\OgRoleStorage",
* "access" = "Drupal\og\OgRoleAccessControlHandler",
* "list_builder" = "Drupal\og\Entity\OgRoleListBuilder",
* "form" = {
* "default" = "Drupal\og_ui\Form\OgRoleForm",
* "delete" = "Drupal\og_ui\Form\OgRoleDeleteForm",
* }
* },
* admin_permission = "administer organic groups",
* config_prefix = "og_role",
* static_cache = TRUE,
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "weight" = "weight",
* },
* links = {
* "collection" = "/admin/config/group/roles/{entity_type_id}/{bundle_id}",
* "add-form" = "/admin/config/group/roles/{entity_type_id}/{bundle_id}/add",
* "edit-form" = "/admin/config/group/role/{og_role}/edit",
* "delete-form" = "/admin/config/group/role/{og_role}/delete",
* },
* config_export = {
* "id",
* "label",
* "weight",
* "is_admin",
* "group_type",
* "group_bundle",
* "group_id",
* "permissions",
* "role_type",
* }
* )
*/
#[ConfigEntityType(
id: 'og_role',
label: new TranslatableMarkup('OG role'),
label_collection: new TranslatableMarkup('OG roles'),
label_singular: new TranslatableMarkup('OG role'),
label_plural: new TranslatableMarkup('OG roles'),
config_prefix: 'og_role',
static_cache: TRUE,
entity_keys: [
'id' => 'id',
'label' => 'label',
'weight' => 'weight',
],
handlers: [
'storage' => OgRoleStorage::class,
'access' => OgRoleAccessControlHandler::class,
'list_builder' => OgRoleListBuilder::class,
'form' => [
'default' => OgRoleForm::class,
'delete' => OgRoleDeleteForm::class,
],
],
links: [
'collection' => '/admin/config/group/roles/{entity_type_id}/{bundle_id}',
'add-form' => '/admin/config/group/roles/{entity_type_id}/{bundle_id}/add',
'edit-form' => '/admin/config/group/role/{og_role}/edit',
'delete-form' => '/admin/config/group/role/{og_role}/delete',
],
admin_permission: 'administer organic groups',
label_count: [
'singular' => '@count OG role',
'plural' => '@count OG roles',
],
config_export: [
'id',
'label',
'weight',
'is_admin',
'group_type',
'group_bundle',
'group_id',
'permissions',
'role_type',
],
)]
class OgRole extends ConfigEntityBase implements OgRoleInterface {
/**
* The machine name of this role.
*/
protected string $id;
/**
* The human-readable label of this role.
*/
protected ?string $label;
/**
* The role name.
*/
protected string $name;
/**
* Whether the parent entity we depend on is being removed.
*/
protected bool $parentEntityIsBeingRemoved = FALSE;
/**
* The weight of this role in administrative listings.
*/
protected ?int $weight = 0;
/**
* The permissions belonging to this role.
*
* @var string[]
*/
protected array $permissions = [];
/**
* An indicator whether the role has all permissions.
*/
protected ?bool $is_admin = FALSE;
/**
* Constructs an OgRole object.
*
* @param array $values
* An array of values to set, keyed by property name.
*/
public function __construct(array $values) {
parent::__construct($values, 'og_role');
}
/**
* {@inheritdoc}
*/
public function getPermissions(): array {
if ($this->isAdmin()) {
return [];
}
return $this->permissions;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->get('weight');
}
/**
* {@inheritdoc}
*/
public function setWeight($weight): static {
$this->set('weight', $weight);
return $this;
}
/**
* {@inheritdoc}
*/
public function hasPermission($permission): bool {
if ($this->isAdmin()) {
return TRUE;
}
return in_array($permission, $this->permissions);
}
/**
* {@inheritdoc}
*/
#[ActionMethod(adminLabel: new TranslatableMarkup('Add permission to role'))]
public function grantPermission($permission): static {
if ($this->isAdmin()) {
return $this;
}
if (!$this->hasPermission($permission)) {
$this->permissions[] = $permission;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function revokePermission($permission): static {
if ($this->isAdmin()) {
return $this;
}
$this->permissions = array_diff($this->permissions, [$permission]);
return $this;
}
/**
* {@inheritdoc}
*/
public function isAdmin(): bool {
return (bool) $this->is_admin;
}
/**
* {@inheritdoc}
*/
public function setIsAdmin($is_admin): static {
$this->is_admin = $is_admin;
return $this;
}
/**
* {@inheritdoc}
*/
public function setId($id): static {
$this->set('id', $id);
return $this;
}
/**
* {@inheritdoc}
*/
public function getLabel(): string {
return $this->get('label');
}
/**
* {@inheritdoc}
*/
public function setLabel(string $label): static {
$this->set('label', $label);
return $this;
}
/**
* {@inheritdoc}
*/
public function getGroupType(): ?string {
return $this->get('group_type');
}
/**
* {@inheritdoc}
*/
public function setGroupType(string $group_type): static {
$this->set('group_type', $group_type);
return $this;
}
/**
* {@inheritdoc}
*/
public function getGroupBundle(): string {
return $this->get('group_bundle');
}
/**
* {@inheritdoc}
*/
public function setGroupBundle($group_bundle): static {
$this->set('group_bundle', $group_bundle);
return $this;
}
/**
* {@inheritdoc}
*/
public function getRoleType(): string {
return $this->get('role_type') ?: OgRoleInterface::ROLE_TYPE_STANDARD;
}
/**
* {@inheritdoc}
*/
public function setRoleType(string $role_type): static {
if (!in_array($role_type, [
self::ROLE_TYPE_REQUIRED,
self::ROLE_TYPE_STANDARD,
])) {
throw new \InvalidArgumentException("'$role_type' is not a valid role type.");
}
return $this->set('role_type', $role_type);
}
/**
* {@inheritdoc}
*/
public function isLocked(): bool {
return $this->get('role_type') !== OgRoleInterface::ROLE_TYPE_STANDARD;
}
/**
* {@inheritdoc}
*/
public function getName(): ?string {
// If the name is not set yet, try to derive it from the ID.
if (empty($this->name) && $this->id() && $this->getGroupType() && $this->getGroupBundle()) {
// Check if the ID matches the pattern '{entity type}-{bundle}-{name}'.
$pattern = preg_quote("{$this->getGroupType()}-{$this->getGroupBundle()}-");
preg_match("/$pattern(.+)/", $this->id(), $matches);
if (!empty($matches[1])) {
$this->setName($matches[1]);
}
}
return $this->get('name');
}
/**
* {@inheritdoc}
*/
public function setName(string $name): static {
$this->name = $name;
return $this;
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageInterface $storage, array &$entities): void {
parent::postLoad($storage, $entities);
// Sort the queried roles by their weight.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
uasort($entities, [static::class, 'sort']);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage): void {
parent::preSave($storage);
if (!isset($this->weight) && ($roles = $storage->loadMultiple())) {
// Set a role weight to make this new role last.
$max = array_reduce($roles, function ($max, $role) {
return max($max, $role->weight);
});
$this->weight = $max + 1;
}
if (!$this->isSyncing() && $this->hasTrustedData()) {
// Permissions are always ordered alphabetically to avoid conflicts in the
// exported configuration. If the save is not trusted then the
// configuration will be sorted by StorableConfigBase.
sort($this->permissions);
}
}
/**
* {@inheritdoc}
*/
public static function loadByGroupAndName(ContentEntityInterface $group, string $name): ?static {
$role_id = "{$group->getEntityTypeId()}-{$group->bundle()}-$name";
return self::load($role_id);
}
/**
* {@inheritdoc}
*/
public static function loadByGroupType(string $group_entity_type_id, string $group_bundle_id): array {
$properties = [
'group_type' => $group_entity_type_id,
'group_bundle' => $group_bundle_id,
];
return \Drupal::entityTypeManager()->getStorage('og_role')->loadByProperties($properties);
}
/**
* {@inheritdoc}
*/
public function save() {
// The ID of a new OgRole has to consist of the entity type ID, bundle ID
// and role name, separated by dashes.
if ($this->isNew() && $this->id()) {
$pattern = preg_quote("{$this->getGroupType()}-{$this->getGroupBundle()}-{$this->getName()}");
if (!preg_match("/$pattern/", $this->id())) {
throw new ConfigValueException('The ID should consist of the group entity type ID, group bundle ID and role name, separated by dashes.');
}
}
// If a new OgRole is saved and the ID is not set, construct the ID from
// the entity type ID, bundle ID and role name.
if ($this->isNew() && !$this->id()) {
if (!$this->getGroupType()) {
throw new ConfigValueException('The group type can not be empty.');
}
if (!$this->getGroupBundle()) {
throw new ConfigValueException('The group bundle can not be empty.');
}
if (!$this->getName()) {
throw new ConfigValueException('The role name can not be empty.');
}
// When assigning a role to group we need to add a prefix to the ID in
// order to prevent duplicate IDs.
$prefix = $this->getGroupType() . '-' . $this->getGroupBundle() . '-';
$this->setId($prefix . $this->getName());
}
return parent::save();
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value) {
// Prevent the ID, role type, group ID, group entity type or bundle from
// being changed once they are set. These properties are required and
// shouldn't be tampered with.
$is_locked_property = in_array($property_name, [
'id',
'role_type',
'group_id',
'group_type',
'group_bundle',
]);
if (!$is_locked_property || $this->isNew()) {
return parent::set($property_name, $value);
}
if ($this->get($property_name) == $value) {
// Locked property hasn't changed, so we can return early.
return $this;
}
throw new OgRoleException("The $property_name cannot be changed.");
}
/**
* {@inheritdoc}
*/
public function delete(): void {
// The default roles are required. Prevent them from being deleted for as
// long as the group still exists, unless the group itself is in the process
// of being removed.
if (!$this->parentEntityIsBeingRemoved && $this->isRequired() && $this->groupTypeManager()->isGroup($this->getGroupType(), $this->getGroupBundle())) {
throw new OgRoleException('The default roles "non-member" and "member" cannot be deleted.');
}
parent::delete();
}
/**
* {@inheritdoc}
*/
public function isRequired(): bool {
return static::getRoleTypeByName($this->getName()) === OgRoleInterface::ROLE_TYPE_REQUIRED;
}
/**
* Maps role names to role types.
*
* The 'anonymous' and 'authenticated' roles should not be changed or deleted.
* All others are standard roles.
*
* @param string $role_name
* The role name for which to return the type.
*
* @return string
* The role type, either OgRoleInterface::ROLE_TYPE_REQUIRED or
* OgRoleInterface::ROLE_TYPE_STANDARD.
*/
public static function getRoleTypeByName(string $role_name): string {
return in_array($role_name, [
OgRoleInterface::ANONYMOUS,
OgRoleInterface::AUTHENTICATED,
]) ? OgRoleInterface::ROLE_TYPE_REQUIRED : OgRoleInterface::ROLE_TYPE_STANDARD;
}
/**
* {@inheritdoc}
*/
public static function getRole(string $entity_type_id, string $bundle, string $role_name): ?static {
return self::load($entity_type_id . '-' . $bundle . '-' . $role_name);
}
/**
* Gets the group manager.
*/
protected function groupTypeManager(): GroupTypeManagerInterface {
// Returning the group manager by calling the global factory method might
// seem less than ideal, but Entity classes are not designed to work with
// proper dependency injection. The ::create() method only accepts a $values
// array, which is not compatible with ContainerInjectionInterface.
// See for example Entity::uuidGenerator() in the base Entity class, it
// also uses this pattern.
return \Drupal::service('og.group_type_manager');
}
/**
* Gets the OG access service.
*/
protected function ogAccess(): OgAccessInterface {
return \Drupal::service('og.access');
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// OG doesn't need to validate the existence of each role-assigned
// permission.
// @see https://www.drupal.org/node/3193348
// Create a dependency on the group bundle.
$bundle_config_dependency = \Drupal::entityTypeManager()->getDefinition($this->getGroupType())->getBundleConfigDependency($this->getGroupBundle());
$this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
return $this;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies): bool {
// The parent entity we depend on is being removed. Set a flag so we can
// allow removal of required roles.
$this->parentEntityIsBeingRemoved = TRUE;
$changed = parent::onDependencyRemoval($dependencies);
// Load all permission definitions.
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
// Convert config and content entity dependencies to a list of names to make
// it easier to check.
foreach (['content', 'config'] as $type) {
$dependencies[$type] = array_keys($dependencies[$type]);
}
// Remove any permissions from the role that are dependent on anything being
// deleted or uninstalled.
foreach ($this->permissions as $key => $permission) {
if (!isset($permission_definitions[$permission])) {
// If the permission is not defined then there's nothing we can do.
continue;
}
if (in_array($permission_definitions[$permission]['provider'], $dependencies['module'], TRUE)) {
unset($this->permissions[$key]);
$changed = TRUE;
// Process the next permission.
continue;
}
if (isset($permission_definitions[$permission]['dependencies'])) {
foreach ($permission_definitions[$permission]['dependencies'] as $type => $list) {
if (array_intersect($list, $dependencies[$type])) {
unset($this->permissions[$key]);
$changed = TRUE;
// Process the next permission.
continue 2;
}
}
}
}
return $changed;
}
}
