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