comparison core/modules/workspaces/src/WorkspaceManager.php @ 17:129ea1e6d783

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