annotate core/modules/workspaces/src/WorkspaceManager.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@17 1 <?php
Chris@17 2
Chris@17 3 namespace Drupal\workspaces;
Chris@17 4
Chris@17 5 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
Chris@17 6 use Drupal\Core\DependencyInjection\ClassResolverInterface;
Chris@17 7 use Drupal\Core\Entity\EntityPublishedInterface;
Chris@17 8 use Drupal\Core\Entity\EntityTypeInterface;
Chris@17 9 use Drupal\Core\Entity\EntityTypeManagerInterface;
Chris@17 10 use Drupal\Core\Session\AccountProxyInterface;
Chris@17 11 use Drupal\Core\Site\Settings;
Chris@17 12 use Drupal\Core\State\StateInterface;
Chris@17 13 use Drupal\Core\StringTranslation\StringTranslationTrait;
Chris@17 14 use Psr\Log\LoggerInterface;
Chris@17 15 use Symfony\Component\HttpFoundation\RequestStack;
Chris@17 16
Chris@17 17 /**
Chris@17 18 * Provides the workspace manager.
Chris@17 19 */
Chris@17 20 class WorkspaceManager implements WorkspaceManagerInterface {
Chris@17 21
Chris@17 22 use StringTranslationTrait;
Chris@17 23
Chris@17 24 /**
Chris@17 25 * An array of entity type IDs that can not belong to a workspace.
Chris@17 26 *
Chris@17 27 * By default, only entity types which are revisionable and publishable can
Chris@17 28 * belong to a workspace.
Chris@17 29 *
Chris@17 30 * @var string[]
Chris@17 31 */
Chris@17 32 protected $blacklist = [
Chris@17 33 'workspace_association' => 'workspace_association',
Chris@17 34 'workspace' => 'workspace',
Chris@17 35 ];
Chris@17 36
Chris@17 37 /**
Chris@17 38 * The request stack.
Chris@17 39 *
Chris@17 40 * @var \Symfony\Component\HttpFoundation\RequestStack
Chris@17 41 */
Chris@17 42 protected $requestStack;
Chris@17 43
Chris@17 44 /**
Chris@17 45 * The entity type manager.
Chris@17 46 *
Chris@17 47 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
Chris@17 48 */
Chris@17 49 protected $entityTypeManager;
Chris@17 50
Chris@17 51 /**
Chris@17 52 * The entity memory cache service.
Chris@17 53 *
Chris@17 54 * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
Chris@17 55 */
Chris@17 56 protected $entityMemoryCache;
Chris@17 57
Chris@17 58 /**
Chris@17 59 * The current user.
Chris@17 60 *
Chris@17 61 * @var \Drupal\Core\Session\AccountProxyInterface
Chris@17 62 */
Chris@17 63 protected $currentUser;
Chris@17 64
Chris@17 65 /**
Chris@17 66 * The state service.
Chris@17 67 *
Chris@17 68 * @var \Drupal\Core\State\StateInterface
Chris@17 69 */
Chris@17 70 protected $state;
Chris@17 71
Chris@17 72 /**
Chris@17 73 * A logger instance.
Chris@17 74 *
Chris@17 75 * @var \Psr\Log\LoggerInterface
Chris@17 76 */
Chris@17 77 protected $logger;
Chris@17 78
Chris@17 79 /**
Chris@17 80 * The class resolver.
Chris@17 81 *
Chris@17 82 * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
Chris@17 83 */
Chris@17 84 protected $classResolver;
Chris@17 85
Chris@17 86 /**
Chris@17 87 * The workspace negotiator service IDs.
Chris@17 88 *
Chris@17 89 * @var array
Chris@17 90 */
Chris@17 91 protected $negotiatorIds;
Chris@17 92
Chris@17 93 /**
Chris@17 94 * The current active workspace.
Chris@17 95 *
Chris@17 96 * @var \Drupal\workspaces\WorkspaceInterface
Chris@17 97 */
Chris@17 98 protected $activeWorkspace;
Chris@17 99
Chris@17 100 /**
Chris@17 101 * Constructs a new WorkspaceManager.
Chris@17 102 *
Chris@17 103 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
Chris@17 104 * The request stack.
Chris@17 105 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
Chris@17 106 * The entity type manager.
Chris@17 107 * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $entity_memory_cache
Chris@17 108 * The entity memory cache service.
Chris@17 109 * @param \Drupal\Core\Session\AccountProxyInterface $current_user
Chris@17 110 * The current user.
Chris@17 111 * @param \Drupal\Core\State\StateInterface $state
Chris@17 112 * The state service.
Chris@17 113 * @param \Psr\Log\LoggerInterface $logger
Chris@17 114 * A logger instance.
Chris@17 115 * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
Chris@17 116 * The class resolver.
Chris@17 117 * @param array $negotiator_ids
Chris@17 118 * The workspace negotiator service IDs.
Chris@17 119 */
Chris@17 120 public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) {
Chris@17 121 $this->requestStack = $request_stack;
Chris@17 122 $this->entityTypeManager = $entity_type_manager;
Chris@17 123 $this->entityMemoryCache = $entity_memory_cache;
Chris@17 124 $this->currentUser = $current_user;
Chris@17 125 $this->state = $state;
Chris@17 126 $this->logger = $logger;
Chris@17 127 $this->classResolver = $class_resolver;
Chris@17 128 $this->negotiatorIds = $negotiator_ids;
Chris@17 129 }
Chris@17 130
Chris@17 131 /**
Chris@17 132 * {@inheritdoc}
Chris@17 133 */
Chris@17 134 public function isEntityTypeSupported(EntityTypeInterface $entity_type) {
Chris@17 135 // First, check if we already determined whether this entity type is
Chris@17 136 // supported or not.
Chris@17 137 if (isset($this->blacklist[$entity_type->id()])) {
Chris@17 138 return FALSE;
Chris@17 139 }
Chris@17 140
Chris@17 141 if ($entity_type->entityClassImplements(EntityPublishedInterface::class) && $entity_type->isRevisionable()) {
Chris@17 142 return TRUE;
Chris@17 143 }
Chris@17 144
Chris@17 145 // This entity type can not belong to a workspace, add it to the blacklist.
Chris@17 146 $this->blacklist[$entity_type->id()] = $entity_type->id();
Chris@17 147 return FALSE;
Chris@17 148 }
Chris@17 149
Chris@17 150 /**
Chris@17 151 * {@inheritdoc}
Chris@17 152 */
Chris@17 153 public function getSupportedEntityTypes() {
Chris@17 154 $entity_types = [];
Chris@17 155 foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
Chris@17 156 if ($this->isEntityTypeSupported($entity_type)) {
Chris@17 157 $entity_types[$entity_type_id] = $entity_type;
Chris@17 158 }
Chris@17 159 }
Chris@17 160 return $entity_types;
Chris@17 161 }
Chris@17 162
Chris@17 163 /**
Chris@17 164 * {@inheritdoc}
Chris@17 165 */
Chris@17 166 public function getActiveWorkspace() {
Chris@17 167 if (!isset($this->activeWorkspace)) {
Chris@17 168 $request = $this->requestStack->getCurrentRequest();
Chris@17 169 foreach ($this->negotiatorIds as $negotiator_id) {
Chris@17 170 $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
Chris@17 171 if ($negotiator->applies($request)) {
Chris@17 172 if ($this->activeWorkspace = $negotiator->getActiveWorkspace($request)) {
Chris@17 173 break;
Chris@17 174 }
Chris@17 175 }
Chris@17 176 }
Chris@17 177 }
Chris@17 178
Chris@17 179 // The default workspace negotiator always returns a valid workspace.
Chris@17 180 return $this->activeWorkspace;
Chris@17 181 }
Chris@17 182
Chris@17 183 /**
Chris@17 184 * {@inheritdoc}
Chris@17 185 */
Chris@17 186 public function setActiveWorkspace(WorkspaceInterface $workspace) {
Chris@17 187 $this->doSwitchWorkspace($workspace);
Chris@17 188
Chris@17 189 // Set the workspace on the proper negotiator.
Chris@17 190 $request = $this->requestStack->getCurrentRequest();
Chris@17 191 foreach ($this->negotiatorIds as $negotiator_id) {
Chris@17 192 $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
Chris@17 193 if ($negotiator->applies($request)) {
Chris@17 194 $negotiator->setActiveWorkspace($workspace);
Chris@17 195 break;
Chris@17 196 }
Chris@17 197 }
Chris@17 198
Chris@17 199 return $this;
Chris@17 200 }
Chris@17 201
Chris@17 202 /**
Chris@17 203 * Switches the current workspace.
Chris@17 204 *
Chris@17 205 * @param \Drupal\workspaces\WorkspaceInterface $workspace
Chris@17 206 * The workspace to set as active.
Chris@17 207 *
Chris@17 208 * @throws \Drupal\workspaces\WorkspaceAccessException
Chris@17 209 * Thrown when the current user doesn't have access to view the workspace.
Chris@17 210 */
Chris@17 211 protected function doSwitchWorkspace(WorkspaceInterface $workspace) {
Chris@17 212 // If the current user doesn't have access to view the workspace, they
Chris@17 213 // shouldn't be allowed to switch to it.
Chris@17 214 if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) {
Chris@17 215 $this->logger->error('Denied access to view workspace %workspace_label for user %uid', [
Chris@17 216 '%workspace_label' => $workspace->label(),
Chris@17 217 '%uid' => $this->currentUser->id(),
Chris@17 218 ]);
Chris@17 219 throw new WorkspaceAccessException('The user does not have permission to view that workspace.');
Chris@17 220 }
Chris@17 221
Chris@17 222 $this->activeWorkspace = $workspace;
Chris@17 223
Chris@17 224 // Clear the static entity cache for the supported entity types.
Chris@17 225 $cache_tags_to_invalidate = array_map(function ($entity_type_id) {
Chris@17 226 return 'entity.memory_cache:' . $entity_type_id;
Chris@17 227 }, array_keys($this->getSupportedEntityTypes()));
Chris@17 228 $this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
Chris@17 229 }
Chris@17 230
Chris@17 231 /**
Chris@17 232 * {@inheritdoc}
Chris@17 233 */
Chris@17 234 public function executeInWorkspace($workspace_id, callable $function) {
Chris@17 235 /** @var \Drupal\workspaces\WorkspaceInterface $workspace */
Chris@17 236 $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
Chris@17 237
Chris@17 238 if (!$workspace) {
Chris@17 239 throw new \InvalidArgumentException('The ' . $workspace_id . ' workspace does not exist.');
Chris@17 240 }
Chris@17 241
Chris@17 242 $previous_active_workspace = $this->getActiveWorkspace();
Chris@17 243 $this->doSwitchWorkspace($workspace);
Chris@17 244 $result = $function();
Chris@17 245 $this->doSwitchWorkspace($previous_active_workspace);
Chris@17 246
Chris@17 247 return $result;
Chris@17 248 }
Chris@17 249
Chris@17 250 /**
Chris@17 251 * {@inheritdoc}
Chris@17 252 */
Chris@17 253 public function shouldAlterOperations(EntityTypeInterface $entity_type) {
Chris@17 254 return $this->isEntityTypeSupported($entity_type) && !$this->getActiveWorkspace()->isDefaultWorkspace();
Chris@17 255 }
Chris@17 256
Chris@17 257 /**
Chris@17 258 * {@inheritdoc}
Chris@17 259 */
Chris@17 260 public function purgeDeletedWorkspacesBatch() {
Chris@17 261 $deleted_workspace_ids = $this->state->get('workspace.deleted', []);
Chris@17 262
Chris@17 263 // Bail out early if there are no workspaces to purge.
Chris@17 264 if (empty($deleted_workspace_ids)) {
Chris@17 265 return;
Chris@17 266 }
Chris@17 267
Chris@17 268 $batch_size = Settings::get('entity_update_batch_size', 50);
Chris@17 269
Chris@17 270 /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */
Chris@17 271 $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association');
Chris@17 272
Chris@17 273 // Get the first deleted workspace from the list and delete the revisions
Chris@17 274 // associated with it, along with the workspace_association entries.
Chris@17 275 $workspace_id = reset($deleted_workspace_ids);
Chris@17 276 $workspace_association_ids = $this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size);
Chris@17 277
Chris@17 278 if ($workspace_association_ids) {
Chris@17 279 $workspace_associations = $workspace_association_storage->loadMultipleRevisions(array_keys($workspace_association_ids));
Chris@17 280 foreach ($workspace_associations as $workspace_association) {
Chris@17 281 $associated_entity_storage = $this->entityTypeManager->getStorage($workspace_association->target_entity_type_id->value);
Chris@17 282 // Delete the associated entity revision.
Chris@17 283 if ($entity = $associated_entity_storage->loadRevision($workspace_association->target_entity_revision_id->value)) {
Chris@17 284 if ($entity->isDefaultRevision()) {
Chris@17 285 $entity->delete();
Chris@17 286 }
Chris@17 287 else {
Chris@17 288 $associated_entity_storage->deleteRevision($workspace_association->target_entity_revision_id->value);
Chris@17 289 }
Chris@17 290 }
Chris@17 291
Chris@17 292 // Delete the workspace_association revision.
Chris@17 293 if ($workspace_association->isDefaultRevision()) {
Chris@17 294 $workspace_association->delete();
Chris@17 295 }
Chris@17 296 else {
Chris@17 297 $workspace_association_storage->deleteRevision($workspace_association->getRevisionId());
Chris@17 298 }
Chris@17 299 }
Chris@17 300 }
Chris@17 301
Chris@17 302 // The purging operation above might have taken a long time, so we need to
Chris@17 303 // request a fresh list of workspace association IDs. If it is empty, we can
Chris@17 304 // go ahead and remove the deleted workspace ID entry from state.
Chris@17 305 if (!$this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size)) {
Chris@17 306 unset($deleted_workspace_ids[$workspace_id]);
Chris@17 307 $this->state->set('workspace.deleted', $deleted_workspace_ids);
Chris@17 308 }
Chris@17 309 }
Chris@17 310
Chris@17 311 /**
Chris@17 312 * Gets a list of workspace association IDs to purge.
Chris@17 313 *
Chris@17 314 * @param string $workspace_id
Chris@17 315 * The ID of the workspace.
Chris@17 316 * @param int $batch_size
Chris@17 317 * The maximum number of records that will be purged.
Chris@17 318 *
Chris@17 319 * @return array
Chris@17 320 * An array of workspace association IDs, keyed by their revision IDs.
Chris@17 321 */
Chris@17 322 protected function getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size) {
Chris@17 323 return $this->entityTypeManager->getStorage('workspace_association')
Chris@17 324 ->getQuery()
Chris@17 325 ->allRevisions()
Chris@17 326 ->accessCheck(FALSE)
Chris@17 327 ->condition('workspace', $workspace_id)
Chris@17 328 ->sort('revision_id', 'ASC')
Chris@17 329 ->range(0, $batch_size)
Chris@17 330 ->execute();
Chris@17 331 }
Chris@17 332
Chris@17 333 }