3 namespace Drupal\workspaces;
5 use Drupal\Core\DependencyInjection\ClassResolverInterface;
6 use Drupal\Core\Entity\EntityPublishedInterface;
7 use Drupal\Core\Entity\EntityTypeInterface;
8 use Drupal\Core\Entity\EntityTypeManagerInterface;
9 use Drupal\Core\Session\AccountProxyInterface;
10 use Drupal\Core\Site\Settings;
11 use Drupal\Core\State\StateInterface;
12 use Drupal\Core\StringTranslation\StringTranslationTrait;
13 use Psr\Log\LoggerInterface;
14 use Symfony\Component\HttpFoundation\RequestStack;
17 * Provides the workspace manager.
19 class WorkspaceManager implements WorkspaceManagerInterface {
21 use StringTranslationTrait;
24 * An array of entity type IDs that can not belong to a workspace.
26 * By default, only entity types which are revisionable and publishable can
27 * belong to a workspace.
31 protected $blacklist = [
32 'workspace_association',
39 * @var \Symfony\Component\HttpFoundation\RequestStack
41 protected $requestStack;
44 * The entity type manager.
46 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
48 protected $entityTypeManager;
53 * @var \Drupal\Core\Session\AccountProxyInterface
55 protected $currentUser;
60 * @var \Drupal\Core\State\StateInterface
67 * @var \Psr\Log\LoggerInterface
74 * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
76 protected $classResolver;
79 * The workspace negotiator service IDs.
83 protected $negotiatorIds;
86 * The current active workspace.
88 * @var \Drupal\workspaces\WorkspaceInterface
90 protected $activeWorkspace;
93 * Constructs a new WorkspaceManager.
95 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
97 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
98 * The entity type manager.
99 * @param \Drupal\Core\Session\AccountProxyInterface $current_user
101 * @param \Drupal\Core\State\StateInterface $state
103 * @param \Psr\Log\LoggerInterface $logger
105 * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
106 * The class resolver.
107 * @param array $negotiator_ids
108 * The workspace negotiator service IDs.
110 public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) {
111 $this->requestStack = $request_stack;
112 $this->entityTypeManager = $entity_type_manager;
113 $this->currentUser = $current_user;
114 $this->state = $state;
115 $this->logger = $logger;
116 $this->classResolver = $class_resolver;
117 $this->negotiatorIds = $negotiator_ids;
123 public function isEntityTypeSupported(EntityTypeInterface $entity_type) {
124 if (!isset($this->blacklist[$entity_type->id()])
125 && $entity_type->entityClassImplements(EntityPublishedInterface::class)
126 && $entity_type->isRevisionable()) {
129 $this->blacklist[$entity_type->id()] = $entity_type->id();
136 public function getSupportedEntityTypes() {
138 foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
139 if ($this->isEntityTypeSupported($entity_type)) {
140 $entity_types[$entity_type_id] = $entity_type;
143 return $entity_types;
149 public function getActiveWorkspace() {
150 if (!isset($this->activeWorkspace)) {
151 $request = $this->requestStack->getCurrentRequest();
152 foreach ($this->negotiatorIds as $negotiator_id) {
153 $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
154 if ($negotiator->applies($request)) {
155 if ($this->activeWorkspace = $negotiator->getActiveWorkspace($request)) {
162 // The default workspace negotiator always returns a valid workspace.
163 return $this->activeWorkspace;
169 public function setActiveWorkspace(WorkspaceInterface $workspace) {
170 // If the current user doesn't have access to view the workspace, they
171 // shouldn't be allowed to switch to it.
172 if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) {
173 $this->logger->error('Denied access to view workspace %workspace_label for user %uid', [
174 '%workspace_label' => $workspace->label(),
175 '%uid' => $this->currentUser->id(),
177 throw new WorkspaceAccessException('The user does not have permission to view that workspace.');
180 $this->activeWorkspace = $workspace;
182 // Set the workspace on the proper negotiator.
183 $request = $this->requestStack->getCurrentRequest();
184 foreach ($this->negotiatorIds as $negotiator_id) {
185 $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
186 if ($negotiator->applies($request)) {
187 $negotiator->setActiveWorkspace($workspace);
192 $supported_entity_types = $this->getSupportedEntityTypes();
193 foreach ($supported_entity_types as $supported_entity_type) {
194 $this->entityTypeManager->getStorage($supported_entity_type->id())->resetCache();
203 public function shouldAlterOperations(EntityTypeInterface $entity_type) {
204 return $this->isEntityTypeSupported($entity_type) && !$this->getActiveWorkspace()->isDefaultWorkspace();
210 public function purgeDeletedWorkspacesBatch() {
211 $deleted_workspace_ids = $this->state->get('workspace.deleted', []);
213 // Bail out early if there are no workspaces to purge.
214 if (empty($deleted_workspace_ids)) {
218 $batch_size = Settings::get('entity_update_batch_size', 50);
220 /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */
221 $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association');
223 // Get the first deleted workspace from the list and delete the revisions
224 // associated with it, along with the workspace_association entries.
225 $workspace_id = reset($deleted_workspace_ids);
226 $workspace_association_ids = $this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size);
228 if ($workspace_association_ids) {
229 $workspace_associations = $workspace_association_storage->loadMultipleRevisions(array_keys($workspace_association_ids));
230 foreach ($workspace_associations as $workspace_association) {
231 $associated_entity_storage = $this->entityTypeManager->getStorage($workspace_association->target_entity_type_id->value);
232 // Delete the associated entity revision.
233 if ($entity = $associated_entity_storage->loadRevision($workspace_association->target_entity_revision_id->value)) {
234 if ($entity->isDefaultRevision()) {
238 $associated_entity_storage->deleteRevision($workspace_association->target_entity_revision_id->value);
242 // Delete the workspace_association revision.
243 if ($workspace_association->isDefaultRevision()) {
244 $workspace_association->delete();
247 $workspace_association_storage->deleteRevision($workspace_association->getRevisionId());
252 // The purging operation above might have taken a long time, so we need to
253 // request a fresh list of workspace association IDs. If it is empty, we can
254 // go ahead and remove the deleted workspace ID entry from state.
255 if (!$this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size)) {
256 unset($deleted_workspace_ids[$workspace_id]);
257 $this->state->set('workspace.deleted', $deleted_workspace_ids);
262 * Gets a list of workspace association IDs to purge.
264 * @param string $workspace_id
265 * The ID of the workspace.
266 * @param int $batch_size
267 * The maximum number of records that will be purged.
270 * An array of workspace association IDs, keyed by their revision IDs.
272 protected function getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size) {
273 return $this->entityTypeManager->getStorage('workspace_association')
277 ->condition('workspace', $workspace_id)
278 ->sort('revision_id', 'ASC')
279 ->range(0, $batch_size)