Mercurial > hg > isophonics-drupal-site
comparison core/modules/workspaces/src/EntityOperations.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 | af1871eacc83 |
comparison
equal
deleted
inserted
replaced
16:c2387f117808 | 17:129ea1e6d783 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\workspaces; | |
4 | |
5 use Drupal\Core\DependencyInjection\ContainerInjectionInterface; | |
6 use Drupal\Core\Entity\EntityInterface; | |
7 use Drupal\Core\Entity\EntityTypeManagerInterface; | |
8 use Drupal\Core\Entity\RevisionableInterface; | |
9 use Drupal\Core\Form\FormStateInterface; | |
10 use Drupal\Core\StringTranslation\StringTranslationTrait; | |
11 use Symfony\Component\DependencyInjection\ContainerInterface; | |
12 | |
13 /** | |
14 * Defines a class for reacting to entity events. | |
15 * | |
16 * @internal | |
17 */ | |
18 class EntityOperations implements ContainerInjectionInterface { | |
19 | |
20 use StringTranslationTrait; | |
21 | |
22 /** | |
23 * The entity type manager service. | |
24 * | |
25 * @var \Drupal\Core\Entity\EntityTypeManagerInterface | |
26 */ | |
27 protected $entityTypeManager; | |
28 | |
29 /** | |
30 * The workspace manager service. | |
31 * | |
32 * @var \Drupal\workspaces\WorkspaceManagerInterface | |
33 */ | |
34 protected $workspaceManager; | |
35 | |
36 /** | |
37 * Constructs a new EntityOperations instance. | |
38 * | |
39 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager | |
40 * The entity type manager service. | |
41 * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager | |
42 * The workspace manager service. | |
43 */ | |
44 public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) { | |
45 $this->entityTypeManager = $entity_type_manager; | |
46 $this->workspaceManager = $workspace_manager; | |
47 } | |
48 | |
49 /** | |
50 * {@inheritdoc} | |
51 */ | |
52 public static function create(ContainerInterface $container) { | |
53 return new static( | |
54 $container->get('entity_type.manager'), | |
55 $container->get('workspaces.manager') | |
56 ); | |
57 } | |
58 | |
59 /** | |
60 * Acts on entities when loaded. | |
61 * | |
62 * @see hook_entity_load() | |
63 */ | |
64 public function entityLoad(array &$entities, $entity_type_id) { | |
65 // Only run if the entity type can belong to a workspace and we are in a | |
66 // non-default workspace. | |
67 if (!$this->workspaceManager->shouldAlterOperations($this->entityTypeManager->getDefinition($entity_type_id))) { | |
68 return; | |
69 } | |
70 | |
71 // Get a list of revision IDs for entities that have a revision set for the | |
72 // current active workspace. If an entity has multiple revisions set for a | |
73 // workspace, only the one with the highest ID is returned. | |
74 $entity_ids = array_keys($entities); | |
75 $max_revision_id = 'max_target_entity_revision_id'; | |
76 $results = $this->entityTypeManager | |
77 ->getStorage('workspace_association') | |
78 ->getAggregateQuery() | |
79 ->accessCheck(FALSE) | |
80 ->allRevisions() | |
81 ->aggregate('target_entity_revision_id', 'MAX', NULL, $max_revision_id) | |
82 ->groupBy('target_entity_id') | |
83 ->condition('target_entity_type_id', $entity_type_id) | |
84 ->condition('target_entity_id', $entity_ids, 'IN') | |
85 ->condition('workspace', $this->workspaceManager->getActiveWorkspace()->id()) | |
86 ->execute(); | |
87 | |
88 // Since hook_entity_load() is called on both regular entity load as well as | |
89 // entity revision load, we need to prevent infinite recursion by checking | |
90 // whether the default revisions were already swapped with the workspace | |
91 // revision. | |
92 // @todo This recursion protection should be removed when | |
93 // https://www.drupal.org/project/drupal/issues/2928888 is resolved. | |
94 if ($results) { | |
95 $results = array_filter($results, function ($result) use ($entities, $max_revision_id) { | |
96 return $entities[$result['target_entity_id']]->getRevisionId() != $result[$max_revision_id]; | |
97 }); | |
98 } | |
99 | |
100 if ($results) { | |
101 /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ | |
102 $storage = $this->entityTypeManager->getStorage($entity_type_id); | |
103 | |
104 // Swap out every entity which has a revision set for the current active | |
105 // workspace. | |
106 $swap_revision_ids = array_column($results, $max_revision_id); | |
107 foreach ($storage->loadMultipleRevisions($swap_revision_ids) as $revision) { | |
108 $entities[$revision->id()] = $revision; | |
109 } | |
110 } | |
111 } | |
112 | |
113 /** | |
114 * Acts on an entity before it is created or updated. | |
115 * | |
116 * @param \Drupal\Core\Entity\EntityInterface $entity | |
117 * The entity being saved. | |
118 * | |
119 * @see hook_entity_presave() | |
120 */ | |
121 public function entityPresave(EntityInterface $entity) { | |
122 $entity_type = $entity->getEntityType(); | |
123 | |
124 // Only run if this is not an entity type provided by the Workspaces module | |
125 // and we are in a non-default workspace | |
126 if ($entity_type->getProvider() === 'workspaces' || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { | |
127 return; | |
128 } | |
129 | |
130 // Disallow any change to an unsupported entity when we are not in the | |
131 // default workspace. | |
132 if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) { | |
133 throw new \RuntimeException('This entity can only be saved in the default workspace.'); | |
134 } | |
135 | |
136 /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ | |
137 if (!$entity->isNew() && !isset($entity->_isReplicating)) { | |
138 // Force a new revision if the entity is not replicating. | |
139 $entity->setNewRevision(TRUE); | |
140 | |
141 // All entities in the non-default workspace are pending revisions, | |
142 // regardless of their publishing status. This means that when creating | |
143 // a published pending revision in a non-default workspace it will also be | |
144 // a published pending revision in the default workspace, however, it will | |
145 // become the default revision only when it is replicated to the default | |
146 // workspace. | |
147 $entity->isDefaultRevision(FALSE); | |
148 } | |
149 | |
150 // When a new published entity is inserted in a non-default workspace, we | |
151 // actually want two revisions to be saved: | |
152 // - An unpublished default revision in the default ('live') workspace. | |
153 // - A published pending revision in the current workspace. | |
154 if ($entity->isNew() && $entity->isPublished()) { | |
155 // Keep track of the publishing status in a dynamic property for | |
156 // ::entityInsert(), then unpublish the default revision. | |
157 // @todo Remove this dynamic property once we have an API for associating | |
158 // temporary data with an entity: https://www.drupal.org/node/2896474. | |
159 $entity->_initialPublished = TRUE; | |
160 $entity->setUnpublished(); | |
161 } | |
162 } | |
163 | |
164 /** | |
165 * Responds to the creation of a new entity. | |
166 * | |
167 * @param \Drupal\Core\Entity\EntityInterface $entity | |
168 * The entity that was just saved. | |
169 * | |
170 * @see hook_entity_insert() | |
171 */ | |
172 public function entityInsert(EntityInterface $entity) { | |
173 /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ | |
174 // Only run if the entity type can belong to a workspace and we are in a | |
175 // non-default workspace. | |
176 if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) { | |
177 return; | |
178 } | |
179 | |
180 $this->trackEntity($entity); | |
181 | |
182 // When an entity is newly created in a workspace, it should be published in | |
183 // that workspace, but not yet published on the live workspace. It is first | |
184 // saved as unpublished for the default revision, then immediately a second | |
185 // revision is created which is published and attached to the workspace. | |
186 // This ensures that the published version of the entity does not 'leak' | |
187 // into the live site. This differs from edits to existing entities where | |
188 // there is already a valid default revision for the live workspace. | |
189 if (isset($entity->_initialPublished)) { | |
190 // Operate on a clone to avoid changing the entity prior to subsequent | |
191 // hook_entity_insert() implementations. | |
192 $pending_revision = clone $entity; | |
193 $pending_revision->setPublished(); | |
194 $pending_revision->isDefaultRevision(FALSE); | |
195 $pending_revision->save(); | |
196 } | |
197 } | |
198 | |
199 /** | |
200 * Responds to updates to an entity. | |
201 * | |
202 * @param \Drupal\Core\Entity\EntityInterface $entity | |
203 * The entity that was just saved. | |
204 * | |
205 * @see hook_entity_update() | |
206 */ | |
207 public function entityUpdate(EntityInterface $entity) { | |
208 // Only run if the entity type can belong to a workspace and we are in a | |
209 // non-default workspace. | |
210 if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) { | |
211 return; | |
212 } | |
213 | |
214 // Only track new revisions. | |
215 /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ | |
216 if ($entity->getLoadedRevisionId() != $entity->getRevisionId()) { | |
217 $this->trackEntity($entity); | |
218 } | |
219 } | |
220 | |
221 /** | |
222 * Acts on an entity before it is deleted. | |
223 * | |
224 * @param \Drupal\Core\Entity\EntityInterface $entity | |
225 * The entity being deleted. | |
226 * | |
227 * @see hook_entity_predelete() | |
228 */ | |
229 public function entityPredelete(EntityInterface $entity) { | |
230 $entity_type = $entity->getEntityType(); | |
231 | |
232 // Only run if this is not an entity type provided by the Workspaces module | |
233 // and we are in a non-default workspace | |
234 if ($entity_type->getProvider() === 'workspaces' || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { | |
235 return; | |
236 } | |
237 | |
238 // Disallow any change to an unsupported entity when we are not in the | |
239 // default workspace. | |
240 if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) { | |
241 throw new \RuntimeException('This entity can only be deleted in the default workspace.'); | |
242 } | |
243 } | |
244 | |
245 /** | |
246 * Updates or creates a WorkspaceAssociation entity for a given entity. | |
247 * | |
248 * If the passed-in entity can belong to a workspace and already has a | |
249 * WorkspaceAssociation entity, then a new revision of this will be created with | |
250 * the new information. Otherwise, a new WorkspaceAssociation entity is created to | |
251 * store the passed-in entity's information. | |
252 * | |
253 * @param \Drupal\Core\Entity\EntityInterface $entity | |
254 * The entity to update or create from. | |
255 */ | |
256 protected function trackEntity(EntityInterface $entity) { | |
257 /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ | |
258 // If the entity is not new, check if there's an existing | |
259 // WorkspaceAssociation entity for it. | |
260 $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); | |
261 if (!$entity->isNew()) { | |
262 $workspace_associations = $workspace_association_storage->loadByProperties([ | |
263 'target_entity_type_id' => $entity->getEntityTypeId(), | |
264 'target_entity_id' => $entity->id(), | |
265 ]); | |
266 | |
267 /** @var \Drupal\Core\Entity\ContentEntityInterface $workspace_association */ | |
268 $workspace_association = reset($workspace_associations); | |
269 } | |
270 | |
271 // If there was a WorkspaceAssociation entry create a new revision, | |
272 // otherwise create a new entity with the type and ID. | |
273 if (!empty($workspace_association)) { | |
274 $workspace_association->setNewRevision(TRUE); | |
275 } | |
276 else { | |
277 $workspace_association = $workspace_association_storage->create([ | |
278 'target_entity_type_id' => $entity->getEntityTypeId(), | |
279 'target_entity_id' => $entity->id(), | |
280 ]); | |
281 } | |
282 | |
283 // Add the revision ID and the workspace ID. | |
284 $workspace_association->set('target_entity_revision_id', $entity->getRevisionId()); | |
285 $workspace_association->set('workspace', $this->workspaceManager->getActiveWorkspace()->id()); | |
286 | |
287 // Save without updating the tracked content entity. | |
288 $workspace_association->save(); | |
289 } | |
290 | |
291 /** | |
292 * Alters entity forms to disallow concurrent editing in multiple workspaces. | |
293 * | |
294 * @param array $form | |
295 * An associative array containing the structure of the form. | |
296 * @param \Drupal\Core\Form\FormStateInterface $form_state | |
297 * The current state of the form. | |
298 * @param string $form_id | |
299 * The form ID. | |
300 * | |
301 * @see hook_form_alter() | |
302 */ | |
303 public function entityFormAlter(array &$form, FormStateInterface $form_state, $form_id) { | |
304 /** @var \Drupal\Core\Entity\EntityInterface $entity */ | |
305 $entity = $form_state->getFormObject()->getEntity(); | |
306 if (!$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) { | |
307 return; | |
308 } | |
309 | |
310 // For supported entity types, signal the fact that this form is safe to use | |
311 // in a non-default workspace. | |
312 // @see \Drupal\workspaces\FormOperations::validateForm() | |
313 $form_state->set('workspace_safe', TRUE); | |
314 | |
315 // Add an entity builder to the form which marks the edited entity object as | |
316 // a pending revision. This is needed so validation constraints like | |
317 // \Drupal\path\Plugin\Validation\Constraint\PathAliasConstraintValidator | |
318 // know in advance (before hook_entity_presave()) that the new revision will | |
319 // be a pending one. | |
320 $active_workspace = $this->workspaceManager->getActiveWorkspace(); | |
321 if (!$active_workspace->isDefaultWorkspace()) { | |
322 $form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild']; | |
323 } | |
324 | |
325 /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ | |
326 $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); | |
327 if ($workspace_ids = $workspace_association_storage->getEntityTrackingWorkspaceIds($entity)) { | |
328 // An entity can only be edited in one workspace. | |
329 $workspace_id = reset($workspace_ids); | |
330 | |
331 if ($workspace_id !== $active_workspace->id()) { | |
332 $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id); | |
333 | |
334 $form['#markup'] = $this->t('The content is being edited in the %label workspace.', ['%label' => $workspace->label()]); | |
335 $form['#access'] = FALSE; | |
336 } | |
337 } | |
338 } | |
339 | |
340 /** | |
341 * Entity builder that marks all supported entities as pending revisions. | |
342 */ | |
343 public static function entityFormEntityBuild($entity_type_id, RevisionableInterface $entity, &$form, FormStateInterface &$form_state) { | |
344 // Set the non-default revision flag so that validation constraints are also | |
345 // aware that a pending revision is about to be created. | |
346 $entity->isDefaultRevision(FALSE); | |
347 } | |
348 | |
349 } |