Loading core/modules/workspaces/src/EntityOperations.php +3 −1 Original line number Diff line number Diff line Loading @@ -136,8 +136,10 @@ public function entityPresave(EntityInterface $entity) { // become the default revision only when it is replicated to the default // workspace. $entity->isDefaultRevision(FALSE); } // Track the workspaces in which the new revision was saved. if (!$entity->isSyncing()) { $field_name = $entity_type->getRevisionMetadataKey('workspace'); $entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id(); } Loading core/modules/workspaces/src/Plugin/Validation/Constraint/DeletedWorkspaceConstraintValidator.php +11 −10 Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ namespace Drupal\workspaces\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\Core\State\StateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; Loading @@ -14,20 +14,20 @@ class DeletedWorkspaceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** * The workspace association service. * The state service. * * @var \Drupal\workspaces\WorkspaceAssociationInterface * @var \Drupal\Core\State\StateInterface */ protected $workspaceAssociation; protected $state; /** * Creates a new DeletedWorkspaceConstraintValidator instance. * * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association * The workspace association service. * @param \Drupal\Core\State\StateInterface $state * The state service. */ public function __construct(WorkspaceAssociationInterface $workspace_association) { $this->workspaceAssociation = $workspace_association; public function __construct(StateInterface $state) { $this->state = $state; } /** Loading @@ -35,7 +35,7 @@ public function __construct(WorkspaceAssociationInterface $workspace_association */ public static function create(ContainerInterface $container) { return new static( $container->get('workspaces.association') $container->get('state') ); } Loading @@ -49,7 +49,8 @@ public function validate($value, Constraint $constraint) { return; } if ($this->workspaceAssociation->getTrackedEntities($value->getEntity()->id())) { $deleted_workspace_ids = $this->state->get('workspace.deleted', []); if (isset($deleted_workspace_ids[$value->getEntity()->id()])) { $this->context->addViolation($constraint->message); } } Loading core/modules/workspaces/src/WorkspaceAssociation.php +46 −2 Original line number Diff line number Diff line Loading @@ -186,13 +186,21 @@ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_i $id_field = $table_mapping->getColumnNames($entity_type->getKey('id'))['value']; $revision_id_field = $table_mapping->getColumnNames($entity_type->getKey('revision'))['value']; $workspace_tree = $this->workspaceRepository->loadTree(); if (isset($workspace_tree[$workspace_id])) { $workspace_candidates = array_merge([$workspace_id], $workspace_tree[$workspace_id]['ancestors']); } else { $workspace_candidates = [$workspace_id]; } $query = $this->database->select($entity_type->getRevisionTable(), 'revision'); $query->leftJoin($entity_type->getBaseTable(), 'base', "[revision].[$id_field] = [base].[$id_field]"); $query ->fields('revision', [$revision_id_field, $id_field]) ->condition("revision.$workspace_field", $workspace_id) ->where("[revision].[$revision_id_field] > [base].[$revision_id_field]") ->condition("revision.$workspace_field", $workspace_candidates, 'IN') ->where("[revision].[$revision_id_field] >= [base].[$revision_id_field]") ->orderBy("revision.$revision_id_field", 'ASC'); // Restrict the result to a set of entity ID's if provided. Loading @@ -203,6 +211,42 @@ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_i return $query->execute()->fetchAllKeyed(); } /** * {@inheritdoc} */ public function getAssociatedInitialRevisions(string $workspace_id, string $entity_type_id, array $entity_ids = []) { /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ $storage = $this->entityTypeManager->getStorage($entity_type_id); // If the entity type is not using core's default entity storage, we can't // assume the table mapping layout so we have to return only the latest // tracked revisions. if (!$storage instanceof SqlContentEntityStorage) { return $this->getTrackedEntities($workspace_id, $entity_type_id, $entity_ids)[$entity_type_id]; } $entity_type = $storage->getEntityType(); $table_mapping = $storage->getTableMapping(); $workspace_field = $table_mapping->getColumnNames($entity_type->get('revision_metadata_keys')['workspace'])['target_id']; $id_field = $table_mapping->getColumnNames($entity_type->getKey('id'))['value']; $revision_id_field = $table_mapping->getColumnNames($entity_type->getKey('revision'))['value']; $query = $this->database->select($entity_type->getBaseTable(), 'base'); $query->leftJoin($entity_type->getRevisionTable(), 'revision', "[base].[$revision_id_field] = [revision].[$revision_id_field]"); $query ->fields('base', [$revision_id_field, $id_field]) ->condition("revision.$workspace_field", $workspace_id, '=') ->orderBy("base.$revision_id_field", 'ASC'); // Restrict the result to a set of entity ID's if provided. if ($entity_ids) { $query->condition("base.$id_field", $entity_ids, 'IN'); } return $query->execute()->fetchAllKeyed(); } /** * {@inheritdoc} */ Loading core/modules/workspaces/src/WorkspaceAssociationInterface.php +17 −0 Original line number Diff line number Diff line Loading @@ -74,6 +74,23 @@ public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entit */ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_ids = NULL); /** * Retrieves all content revisions that were created in a given workspace. * * @param string $workspace_id * The ID of the workspace. * @param string $entity_type_id * An entity type ID to find revisions for. * @param int[]|string[] $entity_ids * (optional) An array of entity IDs to filter the results by. Defaults to * an empty array. * * @return array * Returns an array where the values are an array of entity IDs keyed by * revision IDs. */ public function getAssociatedInitialRevisions(string $workspace_id, string $entity_type_id, array $entity_ids = []); /** * Gets a list of workspace IDs in which an entity is tracked. * Loading core/modules/workspaces/src/WorkspaceManager.php +37 −8 Original line number Diff line number Diff line Loading @@ -316,31 +316,60 @@ public function purgeDeletedWorkspacesBatch() { // Get the first deleted workspace from the list and delete the revisions // associated with it, along with the workspace association records. $workspace_id = reset($deleted_workspace_ids); $tracked_entities = $this->workspaceAssociation->getTrackedEntities($workspace_id); $all_associated_revisions = []; foreach (array_keys($this->getSupportedEntityTypes()) as $entity_type_id) { $all_associated_revisions[$entity_type_id] = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id); } $all_associated_revisions = array_filter($all_associated_revisions); $count = 1; foreach ($tracked_entities as $entity_type_id => $entities) { foreach ($all_associated_revisions as $entity_type_id => $associated_revisions) { $associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id); $associated_revisions = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id); // Sort the associated revisions in reverse ID order, so we can delete the // most recent revisions first. krsort($associated_revisions); // Get a list of default revisions tracked by the given workspace, because // they need to be handled differently than pending revisions. $initial_revision_ids = $this->workspaceAssociation->getAssociatedInitialRevisions($workspace_id, $entity_type_id); foreach (array_keys($associated_revisions) as $revision_id) { if ($count > $batch_size) { continue 2; } // If the workspace is tracking the entity's default revision (i.e. the // entity was created inside that workspace), we need to delete the // whole entity after all of its pending revisions are gone. if (isset($initial_revision_ids[$revision_id])) { $associated_entity_storage->delete([$associated_entity_storage->load($initial_revision_ids[$revision_id])]); } else { // Delete the associated entity revision. $associated_entity_storage->deleteRevision($revision_id); } $count++; } // Delete the workspace association entries. $this->workspaceAssociation->deleteAssociations($workspace_id, $entity_type_id, $entities); } // The purging operation above might have taken a long time, so we need to // request a fresh list of tracked entities. If it is empty, we can go ahead // and remove the deleted workspace ID entry from state. if (!$this->workspaceAssociation->getTrackedEntities($workspace_id)) { $has_associated_revisions = FALSE; foreach (array_keys($this->getSupportedEntityTypes()) as $entity_type_id) { if (!empty($this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id))) { $has_associated_revisions = TRUE; break; } } if (!$has_associated_revisions) { unset($deleted_workspace_ids[$workspace_id]); $this->state->set('workspace.deleted', $deleted_workspace_ids); // Delete any possible leftover association entries. $this->workspaceAssociation->deleteAssociations($workspace_id); } } Loading Loading
core/modules/workspaces/src/EntityOperations.php +3 −1 Original line number Diff line number Diff line Loading @@ -136,8 +136,10 @@ public function entityPresave(EntityInterface $entity) { // become the default revision only when it is replicated to the default // workspace. $entity->isDefaultRevision(FALSE); } // Track the workspaces in which the new revision was saved. if (!$entity->isSyncing()) { $field_name = $entity_type->getRevisionMetadataKey('workspace'); $entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id(); } Loading
core/modules/workspaces/src/Plugin/Validation/Constraint/DeletedWorkspaceConstraintValidator.php +11 −10 Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ namespace Drupal\workspaces\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\Core\State\StateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; Loading @@ -14,20 +14,20 @@ class DeletedWorkspaceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** * The workspace association service. * The state service. * * @var \Drupal\workspaces\WorkspaceAssociationInterface * @var \Drupal\Core\State\StateInterface */ protected $workspaceAssociation; protected $state; /** * Creates a new DeletedWorkspaceConstraintValidator instance. * * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association * The workspace association service. * @param \Drupal\Core\State\StateInterface $state * The state service. */ public function __construct(WorkspaceAssociationInterface $workspace_association) { $this->workspaceAssociation = $workspace_association; public function __construct(StateInterface $state) { $this->state = $state; } /** Loading @@ -35,7 +35,7 @@ public function __construct(WorkspaceAssociationInterface $workspace_association */ public static function create(ContainerInterface $container) { return new static( $container->get('workspaces.association') $container->get('state') ); } Loading @@ -49,7 +49,8 @@ public function validate($value, Constraint $constraint) { return; } if ($this->workspaceAssociation->getTrackedEntities($value->getEntity()->id())) { $deleted_workspace_ids = $this->state->get('workspace.deleted', []); if (isset($deleted_workspace_ids[$value->getEntity()->id()])) { $this->context->addViolation($constraint->message); } } Loading
core/modules/workspaces/src/WorkspaceAssociation.php +46 −2 Original line number Diff line number Diff line Loading @@ -186,13 +186,21 @@ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_i $id_field = $table_mapping->getColumnNames($entity_type->getKey('id'))['value']; $revision_id_field = $table_mapping->getColumnNames($entity_type->getKey('revision'))['value']; $workspace_tree = $this->workspaceRepository->loadTree(); if (isset($workspace_tree[$workspace_id])) { $workspace_candidates = array_merge([$workspace_id], $workspace_tree[$workspace_id]['ancestors']); } else { $workspace_candidates = [$workspace_id]; } $query = $this->database->select($entity_type->getRevisionTable(), 'revision'); $query->leftJoin($entity_type->getBaseTable(), 'base', "[revision].[$id_field] = [base].[$id_field]"); $query ->fields('revision', [$revision_id_field, $id_field]) ->condition("revision.$workspace_field", $workspace_id) ->where("[revision].[$revision_id_field] > [base].[$revision_id_field]") ->condition("revision.$workspace_field", $workspace_candidates, 'IN') ->where("[revision].[$revision_id_field] >= [base].[$revision_id_field]") ->orderBy("revision.$revision_id_field", 'ASC'); // Restrict the result to a set of entity ID's if provided. Loading @@ -203,6 +211,42 @@ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_i return $query->execute()->fetchAllKeyed(); } /** * {@inheritdoc} */ public function getAssociatedInitialRevisions(string $workspace_id, string $entity_type_id, array $entity_ids = []) { /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ $storage = $this->entityTypeManager->getStorage($entity_type_id); // If the entity type is not using core's default entity storage, we can't // assume the table mapping layout so we have to return only the latest // tracked revisions. if (!$storage instanceof SqlContentEntityStorage) { return $this->getTrackedEntities($workspace_id, $entity_type_id, $entity_ids)[$entity_type_id]; } $entity_type = $storage->getEntityType(); $table_mapping = $storage->getTableMapping(); $workspace_field = $table_mapping->getColumnNames($entity_type->get('revision_metadata_keys')['workspace'])['target_id']; $id_field = $table_mapping->getColumnNames($entity_type->getKey('id'))['value']; $revision_id_field = $table_mapping->getColumnNames($entity_type->getKey('revision'))['value']; $query = $this->database->select($entity_type->getBaseTable(), 'base'); $query->leftJoin($entity_type->getRevisionTable(), 'revision', "[base].[$revision_id_field] = [revision].[$revision_id_field]"); $query ->fields('base', [$revision_id_field, $id_field]) ->condition("revision.$workspace_field", $workspace_id, '=') ->orderBy("base.$revision_id_field", 'ASC'); // Restrict the result to a set of entity ID's if provided. if ($entity_ids) { $query->condition("base.$id_field", $entity_ids, 'IN'); } return $query->execute()->fetchAllKeyed(); } /** * {@inheritdoc} */ Loading
core/modules/workspaces/src/WorkspaceAssociationInterface.php +17 −0 Original line number Diff line number Diff line Loading @@ -74,6 +74,23 @@ public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entit */ public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_ids = NULL); /** * Retrieves all content revisions that were created in a given workspace. * * @param string $workspace_id * The ID of the workspace. * @param string $entity_type_id * An entity type ID to find revisions for. * @param int[]|string[] $entity_ids * (optional) An array of entity IDs to filter the results by. Defaults to * an empty array. * * @return array * Returns an array where the values are an array of entity IDs keyed by * revision IDs. */ public function getAssociatedInitialRevisions(string $workspace_id, string $entity_type_id, array $entity_ids = []); /** * Gets a list of workspace IDs in which an entity is tracked. * Loading
core/modules/workspaces/src/WorkspaceManager.php +37 −8 Original line number Diff line number Diff line Loading @@ -316,31 +316,60 @@ public function purgeDeletedWorkspacesBatch() { // Get the first deleted workspace from the list and delete the revisions // associated with it, along with the workspace association records. $workspace_id = reset($deleted_workspace_ids); $tracked_entities = $this->workspaceAssociation->getTrackedEntities($workspace_id); $all_associated_revisions = []; foreach (array_keys($this->getSupportedEntityTypes()) as $entity_type_id) { $all_associated_revisions[$entity_type_id] = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id); } $all_associated_revisions = array_filter($all_associated_revisions); $count = 1; foreach ($tracked_entities as $entity_type_id => $entities) { foreach ($all_associated_revisions as $entity_type_id => $associated_revisions) { $associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id); $associated_revisions = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id); // Sort the associated revisions in reverse ID order, so we can delete the // most recent revisions first. krsort($associated_revisions); // Get a list of default revisions tracked by the given workspace, because // they need to be handled differently than pending revisions. $initial_revision_ids = $this->workspaceAssociation->getAssociatedInitialRevisions($workspace_id, $entity_type_id); foreach (array_keys($associated_revisions) as $revision_id) { if ($count > $batch_size) { continue 2; } // If the workspace is tracking the entity's default revision (i.e. the // entity was created inside that workspace), we need to delete the // whole entity after all of its pending revisions are gone. if (isset($initial_revision_ids[$revision_id])) { $associated_entity_storage->delete([$associated_entity_storage->load($initial_revision_ids[$revision_id])]); } else { // Delete the associated entity revision. $associated_entity_storage->deleteRevision($revision_id); } $count++; } // Delete the workspace association entries. $this->workspaceAssociation->deleteAssociations($workspace_id, $entity_type_id, $entities); } // The purging operation above might have taken a long time, so we need to // request a fresh list of tracked entities. If it is empty, we can go ahead // and remove the deleted workspace ID entry from state. if (!$this->workspaceAssociation->getTrackedEntities($workspace_id)) { $has_associated_revisions = FALSE; foreach (array_keys($this->getSupportedEntityTypes()) as $entity_type_id) { if (!empty($this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id))) { $has_associated_revisions = TRUE; break; } } if (!$has_associated_revisions) { unset($deleted_workspace_ids[$workspace_id]); $this->state->set('workspace.deleted', $deleted_workspace_ids); // Delete any possible leftover association entries. $this->workspaceAssociation->deleteAssociations($workspace_id); } } Loading