From 6caa28c09492405818153ae709e40b2cfbe7b3ed Mon Sep 17 00:00:00 2001 From: catch Date: Fri, 20 Sep 2019 11:17:19 +0100 Subject: [PATCH] Issue #3062434 by amateescu, pmelab, blazey, catch, Sam152, Leksat: Track the workspace of a revision in a base field and convert the workspace_association entity type to a custom index --- core/modules/system/system.module | 22 +++ .../src/Entity/WorkspaceAssociation.php | 77 -------- .../workspaces/src/EntityOperations.php | 96 +++------- .../modules/workspaces/src/EntityTypeInfo.php | 33 ++++ .../EntitySchemaSubscriber.php | 147 +++++++++++++++ .../src/Form/WorkspaceDeleteForm.php | 47 ++++- .../DeletedWorkspaceConstraintValidator.php | 27 ++- ...tyWorkspaceConflictConstraintValidator.php | 20 ++- .../workspaces/src/WorkspaceAssociation.php | 163 +++++++++++++++++ .../src/WorkspaceAssociationInterface.php | 103 +++++++++++ .../src/WorkspaceAssociationStorage.php | 59 ------- .../WorkspaceAssociationStorageInterface.php | 48 ----- .../workspaces/src/WorkspaceManager.php | 80 +++------ .../src/WorkspaceOperationFactory.php | 16 +- .../workspaces/src/WorkspacePublisher.php | 34 ++-- .../drupal-8.6.0-workspaces_installed.php | Bin 0 -> 42910 bytes .../Update/WorkspacesUpdateTest.php | 105 +++++++++++ .../Functional/WorkspacesUninstallTest.php | 6 + .../tests/src/Kernel/WorkspaceAccessTest.php | 1 - .../tests/src/Kernel/WorkspaceCRUDTest.php | 167 +++++++++++++----- .../src/Kernel/WorkspaceIntegrationTest.php | 15 +- .../Kernel/WorkspaceInternalResourceTest.php | 43 ----- .../tests/src/Kernel/WorkspaceTestTrait.php | 31 +++- core/modules/workspaces/workspaces.install | 103 +++++++++++ core/modules/workspaces/workspaces.module | 10 ++ .../workspaces/workspaces.post_update.php | 124 +++++++++++++ .../workspaces/workspaces.services.yml | 15 +- 27 files changed, 1146 insertions(+), 446 deletions(-) delete mode 100644 core/modules/workspaces/src/Entity/WorkspaceAssociation.php create mode 100644 core/modules/workspaces/src/EventSubscriber/EntitySchemaSubscriber.php create mode 100644 core/modules/workspaces/src/WorkspaceAssociation.php create mode 100644 core/modules/workspaces/src/WorkspaceAssociationInterface.php delete mode 100644 core/modules/workspaces/src/WorkspaceAssociationStorage.php delete mode 100644 core/modules/workspaces/src/WorkspaceAssociationStorageInterface.php create mode 100644 core/modules/workspaces/tests/fixtures/update/drupal-8.6.0-workspaces_installed.php create mode 100644 core/modules/workspaces/tests/src/Functional/Update/WorkspacesUpdateTest.php delete mode 100644 core/modules/workspaces/tests/src/Kernel/WorkspaceInternalResourceTest.php diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c7b2937684da..4d2a7f6cbf04 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1454,3 +1454,25 @@ function system_element_info_alter(&$type) { $type['page']['#theme_wrappers']['off_canvas_page_wrapper'] = ['#weight' => -1000]; } } + +/** + * Implements hook_modules_uninstalled(). + */ +function system_modules_uninstalled($modules) { + // @todo Remove this when modules are able to maintain their revision metadata + // keys. + // @see https://www.drupal.org/project/drupal/issues/3074333 + if (!in_array('workspaces', $modules, TRUE)) { + return; + } + + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) { + $revision_metadata_keys = $entity_type->get('revision_metadata_keys'); + if ($revision_metadata_keys && array_key_exists('workspace', $revision_metadata_keys)) { + unset($revision_metadata_keys['workspace']); + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + $entity_definition_update_manager->updateEntityType($entity_type); + } + } +} diff --git a/core/modules/workspaces/src/Entity/WorkspaceAssociation.php b/core/modules/workspaces/src/Entity/WorkspaceAssociation.php deleted file mode 100644 index 6c65c81ed024..000000000000 --- a/core/modules/workspaces/src/Entity/WorkspaceAssociation.php +++ /dev/null @@ -1,77 +0,0 @@ -setLabel(new TranslatableMarkup('workspace')) - ->setDescription(new TranslatableMarkup('The workspace of the referenced content.')) - ->setSetting('target_type', 'workspace') - ->setRequired(TRUE) - ->setRevisionable(TRUE); - - $fields['target_entity_type_id'] = BaseFieldDefinition::create('string') - ->setLabel(new TranslatableMarkup('Content entity type ID')) - ->setDescription(new TranslatableMarkup('The ID of the content entity type associated with this workspace.')) - ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH) - ->setRequired(TRUE) - ->setRevisionable(TRUE); - - $fields['target_entity_id'] = BaseFieldDefinition::create('integer') - ->setLabel(new TranslatableMarkup('Content entity ID')) - ->setDescription(new TranslatableMarkup('The ID of the content entity associated with this workspace.')) - ->setRequired(TRUE) - ->setRevisionable(TRUE); - - $fields['target_entity_revision_id'] = BaseFieldDefinition::create('integer') - ->setLabel(new TranslatableMarkup('Content entity revision ID')) - ->setDescription(new TranslatableMarkup('The revision ID of the content entity associated with this workspace.')) - ->setRequired(TRUE) - ->setRevisionable(TRUE); - - return $fields; - } - -} diff --git a/core/modules/workspaces/src/EntityOperations.php b/core/modules/workspaces/src/EntityOperations.php index 6182da200f2c..4409162a282b 100644 --- a/core/modules/workspaces/src/EntityOperations.php +++ b/core/modules/workspaces/src/EntityOperations.php @@ -34,6 +34,13 @@ class EntityOperations implements ContainerInjectionInterface { */ protected $workspaceManager; + /** + * The workspace association service. + * + * @var \Drupal\workspaces\WorkspaceAssociationInterface + */ + protected $workspaceAssociation; + /** * Constructs a new EntityOperations instance. * @@ -41,10 +48,13 @@ class EntityOperations implements ContainerInjectionInterface { * The entity type manager service. * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager * The workspace manager service. + * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association + * The workspace association service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) { $this->entityTypeManager = $entity_type_manager; $this->workspaceManager = $workspace_manager; + $this->workspaceAssociation = $workspace_association; } /** @@ -53,7 +63,8 @@ class EntityOperations implements ContainerInjectionInterface { public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), - $container->get('workspaces.manager') + $container->get('workspaces.manager'), + $container->get('workspaces.association') ); } @@ -74,31 +85,13 @@ class EntityOperations implements ContainerInjectionInterface { // 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. - $max_revision_id = 'max_target_entity_revision_id'; - $query = $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('workspace', $this->workspaceManager->getActiveWorkspace()->id()); - - if ($ids) { - $query->condition('target_entity_id', $ids, 'IN'); - } - - $results = $query->execute(); - - if ($results) { + if ($tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->workspaceManager->getActiveWorkspace()->id(), $entity_type_id, $ids)) { /** @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) { + foreach ($storage->loadMultipleRevisions(array_keys($tracked_entities[$entity_type_id])) as $revision) { $entities[$revision->id()] = $revision; } } @@ -142,6 +135,10 @@ class EntityOperations implements ContainerInjectionInterface { // 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. + $field_name = $entity_type->getRevisionMetadataKey('workspace'); + $entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id(); } // When a new published entity is inserted in a non-default workspace, we @@ -174,7 +171,7 @@ class EntityOperations implements ContainerInjectionInterface { return; } - $this->trackEntity($entity); + $this->workspaceAssociation->trackEntity($entity, $this->workspaceManager->getActiveWorkspace()); // When an entity is newly created in a workspace, it should be published in // that workspace, but not yet published on the live workspace. It is first @@ -211,7 +208,7 @@ class EntityOperations implements ContainerInjectionInterface { // Only track new revisions. /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ if ($entity->getLoadedRevisionId() != $entity->getRevisionId()) { - $this->trackEntity($entity); + $this->workspaceAssociation->trackEntity($entity, $this->workspaceManager->getActiveWorkspace()); } } @@ -240,51 +237,6 @@ class EntityOperations implements ContainerInjectionInterface { } } - /** - * 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\RevisionableInterface $entity - * The entity to update or create from. - */ - protected function trackEntity(RevisionableInterface $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. * @@ -298,7 +250,7 @@ class EntityOperations implements ContainerInjectionInterface { * @see hook_form_alter() */ public function entityFormAlter(array &$form, FormStateInterface $form_state, $form_id) { - /** @var \Drupal\Core\Entity\EntityInterface $entity */ + /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ $entity = $form_state->getFormObject()->getEntity(); if (!$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) { return; @@ -318,9 +270,7 @@ class EntityOperations implements ContainerInjectionInterface { $form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild']; } - /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ - $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); - if ($workspace_ids = $workspace_association_storage->getEntityTrackingWorkspaceIds($entity)) { + if ($workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity)) { // An entity can only be edited in one workspace. $workspace_id = reset($workspace_ids); diff --git a/core/modules/workspaces/src/EntityTypeInfo.php b/core/modules/workspaces/src/EntityTypeInfo.php index 5495c7fa4e7c..7a72eb246d26 100644 --- a/core/modules/workspaces/src/EntityTypeInfo.php +++ b/core/modules/workspaces/src/EntityTypeInfo.php @@ -3,7 +3,10 @@ namespace Drupal\workspaces; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -66,6 +69,10 @@ class EntityTypeInfo implements ContainerInjectionInterface { foreach ($entity_types as $entity_type) { if ($this->workspaceManager->isEntityTypeSupported($entity_type)) { $entity_type->addConstraint('EntityWorkspaceConflict'); + + $revision_metadata_keys = $entity_type->get('revision_metadata_keys'); + $revision_metadata_keys['workspace'] = 'workspace'; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); } } } @@ -84,4 +91,30 @@ class EntityTypeInfo implements ContainerInjectionInterface { } } + /** + * Provides custom base field definitions for a content entity type. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface[] + * An array of field definitions, keyed by field name. + * + * @see hook_entity_base_field_info() + */ + public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { + if ($this->workspaceManager->isEntityTypeSupported($entity_type)) { + $field_name = $entity_type->getRevisionMetadataKey('workspace'); + $fields[$field_name] = BaseFieldDefinition::create('entity_reference') + ->setLabel(new TranslatableMarkup('Workspace')) + ->setDescription(new TranslatableMarkup('Indicates the workspace that this revision belongs to.')) + ->setSetting('target_type', 'workspace') + ->setInternal(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(TRUE); + + return $fields; + } + } + } diff --git a/core/modules/workspaces/src/EventSubscriber/EntitySchemaSubscriber.php b/core/modules/workspaces/src/EventSubscriber/EntitySchemaSubscriber.php new file mode 100644 index 000000000000..30fd77aa6445 --- /dev/null +++ b/core/modules/workspaces/src/EventSubscriber/EntitySchemaSubscriber.php @@ -0,0 +1,147 @@ +entityDefinitionUpdateManager = $entityDefinitionUpdateManager; + $this->entityLastInstalledSchemaRepository = $entityLastInstalledSchemaRepository; + $this->workspaceManager = $workspace_manager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return static::getEntityTypeEvents(); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeCreate(EntityTypeInterface $entity_type) { + // Nothing to do here. + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + // If the entity type is now supported by Workspaces, add the revision + // metadata field. + if ($this->workspaceManager->isEntityTypeSupported($entity_type) && !$this->workspaceManager->isEntityTypeSupported($original)) { + $revision_metadata_keys = $entity_type->get('revision_metadata_keys'); + + if (!isset($revision_metadata_keys['workspace'])) { + // Bail out if there's an existing field called 'workspace'. + if ($this->entityDefinitionUpdateManager->getFieldStorageDefinition('workspace', $entity_type->id())) { + throw new \RuntimeException("An existing 'workspace' field was found for the '{$entity_type->id()}' entity type. Set the 'workspace' revision metadata key to use a different field name and run this update function again."); + } + + $revision_metadata_keys['workspace'] = 'workspace'; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + + // We are only adding a revision metadata key so we don't need to go + // through the entity update process. + $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); + } + + $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_metadata_keys['workspace'], $entity_type->id(), 'workspaces', $this->getWorkspaceFieldDefinition()); + } + + // If the entity type is no longer supported by Workspaces, remove the + // revision metadata field. + if ($this->workspaceManager->isEntityTypeSupported($original) && !$this->workspaceManager->isEntityTypeSupported($entity_type)) { + $revision_metadata_keys = $original->get('revision_metadata_keys'); + $field_storage_definition = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id())[$revision_metadata_keys['workspace']]; + $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($field_storage_definition); + + $revision_metadata_keys = $entity_type->get('revision_metadata_keys'); + unset($revision_metadata_keys['workspace']); + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + + // We are only removing a revision metadata key so we don't need to go + // through the entity update process. + $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); + } + } + + /** + * {@inheritdoc} + */ + public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) { + $this->onEntityTypeUpdate($entity_type, $original); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + // Nothing to do here. + } + + /** + * Gets the base field definition for the 'workspace' revision metadata field. + * + * @return \Drupal\Core\Field\BaseFieldDefinition + * The base field definition. + */ + protected function getWorkspaceFieldDefinition() { + return BaseFieldDefinition::create('entity_reference') + ->setLabel($this->t('Workspace')) + ->setDescription($this->t('Indicates the workspace that this revision belongs to.')) + ->setSetting('target_type', 'workspace') + ->setInternal(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(TRUE); + } + +} diff --git a/core/modules/workspaces/src/Form/WorkspaceDeleteForm.php b/core/modules/workspaces/src/Form/WorkspaceDeleteForm.php index 8086873f9ade..195cac732fd2 100644 --- a/core/modules/workspaces/src/Form/WorkspaceDeleteForm.php +++ b/core/modules/workspaces/src/Form/WorkspaceDeleteForm.php @@ -2,8 +2,13 @@ namespace Drupal\workspaces\Form; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\ContentEntityDeleteForm; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\workspaces\WorkspaceAssociationInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a form for deleting a workspace. @@ -19,14 +24,52 @@ class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFo */ protected $entity; + /** + * The workspace association service. + * + * @var \Drupal\workspaces\WorkspaceAssociationInterface + */ + protected $workspaceAssociation; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.repository'), + $container->get('workspaces.association'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time') + ); + } + + /** + * Constructs a WorkspaceDeleteForm object. + * + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository service. + * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association + * The workspace association service to check how many revisions will be + * deleted. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle service. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. + */ + public function __construct(EntityRepositoryInterface $entity_repository, WorkspaceAssociationInterface $workspace_association, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) { + parent::__construct($entity_repository, $entity_type_bundle_info, $time); + $this->workspaceAssociation = $workspace_association; + } + /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $form = parent::buildForm($form, $form_state); - $source_rev_diff = $this->entityTypeManager->getStorage('workspace_association')->getTrackedEntities($this->entity->id()); + $tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->entity->id()); $items = []; - foreach ($source_rev_diff as $entity_type_id => $revision_ids) { + foreach (array_keys($tracked_entities) as $entity_type_id => $entity_ids) { + $revision_ids = $this->workspaceAssociation->getAssociatedRevisions($this->entity->id(), $entity_type_id, $entity_ids); $label = $this->entityTypeManager->getDefinition($entity_type_id)->getLabel(); $items[] = $this->formatPlural(count($revision_ids), '1 @label revision.', '@count @label revisions.', ['@label' => $label]); } diff --git a/core/modules/workspaces/src/Plugin/Validation/Constraint/DeletedWorkspaceConstraintValidator.php b/core/modules/workspaces/src/Plugin/Validation/Constraint/DeletedWorkspaceConstraintValidator.php index 070e89050bb8..1543b5fc2b30 100644 --- a/core/modules/workspaces/src/Plugin/Validation/Constraint/DeletedWorkspaceConstraintValidator.php +++ b/core/modules/workspaces/src/Plugin/Validation/Constraint/DeletedWorkspaceConstraintValidator.php @@ -3,7 +3,7 @@ namespace Drupal\workspaces\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\workspaces\WorkspaceAssociationStorageInterface; +use Drupal\workspaces\WorkspaceAssociationInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -14,20 +14,20 @@ use Symfony\Component\Validator\ConstraintValidator; class DeletedWorkspaceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** - * The workspace association storage. + * The workspace association service. * - * @var \Drupal\workspaces\WorkspaceAssociationStorageInterface + * @var \Drupal\workspaces\WorkspaceAssociationInterface */ - protected $workspaceAssociationStorage; + protected $workspaceAssociation; /** * Creates a new DeletedWorkspaceConstraintValidator instance. * - * @param \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage - * The workspace association storage. + * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association + * The workspace association service. */ - public function __construct(WorkspaceAssociationStorageInterface $workspace_association_storage) { - $this->workspaceAssociationStorage = $workspace_association_storage; + public function __construct(WorkspaceAssociationInterface $workspace_association) { + $this->workspaceAssociation = $workspace_association; } /** @@ -35,7 +35,7 @@ class DeletedWorkspaceConstraintValidator extends ConstraintValidator implements */ public static function create(ContainerInterface $container) { return new static( - $container->get('entity_type.manager')->getStorage('workspace_association') + $container->get('workspaces.association') ); } @@ -49,14 +49,7 @@ class DeletedWorkspaceConstraintValidator extends ConstraintValidator implements return; } - $count = $this->workspaceAssociationStorage - ->getQuery() - ->allRevisions() - ->accessCheck(FALSE) - ->condition('workspace', $value->getEntity()->id()) - ->count() - ->execute(); - if ($count) { + if ($this->workspaceAssociation->getTrackedEntities($value->getEntity()->id())) { $this->context->addViolation($constraint->message); } } diff --git a/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php b/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php index 17adc7c0d2d8..66bb887e7b12 100644 --- a/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php +++ b/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php @@ -4,6 +4,7 @@ namespace Drupal\workspaces\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\workspaces\WorkspaceManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; @@ -28,6 +29,13 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp */ protected $workspaceManager; + /** + * The workspace association service. + * + * @var \Drupal\workspaces\WorkspaceAssociationInterface + */ + protected $workspaceAssociation; + /** * Constructs an EntityUntranslatableFieldsConstraintValidator object. * @@ -35,10 +43,13 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp * The entity type manager service. * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager * The workspace manager service. + * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association + * The workspace association service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) { $this->entityTypeManager = $entity_type_manager; $this->workspaceManager = $workspace_manager; + $this->workspaceAssociation = $workspace_association; } /** @@ -47,7 +58,8 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), - $container->get('workspaces.manager') + $container->get('workspaces.manager'), + $container->get('workspaces.association') ); } @@ -57,9 +69,7 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp public function validate($entity, Constraint $constraint) { /** @var \Drupal\Core\Entity\EntityInterface $entity */ if (isset($entity) && !$entity->isNew()) { - /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ - $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); - $workspace_ids = $workspace_association_storage->getEntityTrackingWorkspaceIds($entity); + $workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity); $active_workspace = $this->workspaceManager->getActiveWorkspace(); if ($workspace_ids && (!$active_workspace || !in_array($active_workspace->id(), $workspace_ids, TRUE))) { diff --git a/core/modules/workspaces/src/WorkspaceAssociation.php b/core/modules/workspaces/src/WorkspaceAssociation.php new file mode 100644 index 000000000000..a67f95469372 --- /dev/null +++ b/core/modules/workspaces/src/WorkspaceAssociation.php @@ -0,0 +1,163 @@ +database = $connection; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $workspace) { + $this->database->merge(static::TABLE) + ->fields([ + 'target_entity_revision_id' => $entity->getRevisionId(), + ]) + ->keys([ + 'workspace' => $workspace->id(), + 'target_entity_type_id' => $entity->getEntityTypeId(), + 'target_entity_id' => $entity->id(), + ]) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entity_ids = NULL) { + $query = $this->database->select(static::TABLE); + $query + ->fields(static::TABLE, ['target_entity_type_id', 'target_entity_id', 'target_entity_revision_id']) + ->orderBy('target_entity_revision_id', 'ASC') + ->condition('workspace', $workspace_id); + + if ($entity_type_id) { + $query->condition('target_entity_type_id', $entity_type_id, '='); + + if ($entity_ids) { + $query->condition('target_entity_id', $entity_ids, 'IN'); + } + } + + $tracked_revisions = []; + foreach ($query->execute() as $record) { + $tracked_revisions[$record->target_entity_type_id][$record->target_entity_revision_id] = $record->target_entity_id; + } + + return $tracked_revisions; + } + + /** + * {@inheritdoc} + */ + public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_ids = NULL) { + /** @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->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") + ->orderBy("revision.$revision_id_field", 'ASC'); + + // Restrict the result to a set of entity ID's if provided. + if ($entity_ids) { + $query->condition("revision.$id_field", $entity_ids, 'IN'); + } + + return $query->execute()->fetchAllKeyed(); + } + + /** + * {@inheritdoc} + */ + public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) { + $query = $this->database->select(static::TABLE) + ->fields(static::TABLE, ['workspace']) + ->condition('target_entity_type_id', $entity->getEntityTypeId()) + ->condition('target_entity_id', $entity->id()); + + return $query->execute()->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function postPublish(WorkspaceInterface $workspace) { + $this->deleteAssociations($workspace->id()); + } + + /** + * {@inheritdoc} + */ + public function deleteAssociations($workspace_id, $entity_type_id = NULL, $entity_ids = NULL) { + $query = $this->database->delete(static::TABLE) + ->condition('workspace', $workspace_id); + + if ($entity_type_id) { + $query->condition('target_entity_type_id', $entity_type_id, '='); + + if ($entity_ids) { + $query->condition('target_entity_id', $entity_ids, 'IN'); + } + } + + $query->execute(); + } + +} diff --git a/core/modules/workspaces/src/WorkspaceAssociationInterface.php b/core/modules/workspaces/src/WorkspaceAssociationInterface.php new file mode 100644 index 000000000000..1c93ca4aaa19 --- /dev/null +++ b/core/modules/workspaces/src/WorkspaceAssociationInterface.php @@ -0,0 +1,103 @@ +database - ->delete($this->entityType->getBaseTable()) - ->condition('workspace', $workspace->id()) - ->execute(); - $this->database - ->delete($this->entityType->getRevisionTable()) - ->condition('workspace', $workspace->id()) - ->execute(); - } - - /** - * {@inheritdoc} - */ - public function getTrackedEntities($workspace_id, $all_revisions = FALSE) { - $table = $all_revisions ? $this->getRevisionTable() : $this->getBaseTable(); - $query = $this->database->select($table, 'base_table'); - $query - ->fields('base_table', ['target_entity_type_id', 'target_entity_id', 'target_entity_revision_id']) - ->orderBy('target_entity_revision_id', 'ASC') - ->condition('workspace', $workspace_id); - - $tracked_revisions = []; - foreach ($query->execute() as $record) { - $tracked_revisions[$record->target_entity_type_id][$record->target_entity_revision_id] = $record->target_entity_id; - } - - return $tracked_revisions; - } - - /** - * {@inheritdoc} - */ - public function getEntityTrackingWorkspaceIds(EntityInterface $entity) { - $query = $this->database->select($this->getBaseTable(), 'base_table'); - $query - ->fields('base_table', ['workspace']) - ->condition('target_entity_type_id', $entity->getEntityTypeId()) - ->condition('target_entity_id', $entity->id()); - - return $query->execute()->fetchCol(); - } - -} diff --git a/core/modules/workspaces/src/WorkspaceAssociationStorageInterface.php b/core/modules/workspaces/src/WorkspaceAssociationStorageInterface.php deleted file mode 100644 index 24663206e30c..000000000000 --- a/core/modules/workspaces/src/WorkspaceAssociationStorageInterface.php +++ /dev/null @@ -1,48 +0,0 @@ -requestStack = $request_stack; $this->entityTypeManager = $entity_type_manager; $this->entityMemoryCache = $entity_memory_cache; @@ -125,6 +134,7 @@ class WorkspaceManager implements WorkspaceManagerInterface { $this->state = $state; $this->logger = $logger; $this->classResolver = $class_resolver; + $this->workspaceAssociation = $workspace_association; $this->negotiatorIds = $negotiator_ids; } @@ -305,67 +315,35 @@ class WorkspaceManager implements WorkspaceManagerInterface { $batch_size = Settings::get('entity_update_batch_size', 50); - /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ - $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); - // Get the first deleted workspace from the list and delete the revisions - // associated with it, along with the workspace_association entries. + // associated with it, along with the workspace association records. $workspace_id = reset($deleted_workspace_ids); - $workspace_association_ids = $this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size); + $tracked_entities = $this->workspaceAssociation->getTrackedEntities($workspace_id); + + $count = 1; + foreach ($tracked_entities as $entity_type_id => $entities) { + $associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + $associated_revisions = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id); + foreach (array_keys($associated_revisions) as $revision_id) { + if ($count > $batch_size) { + continue 2; + } - if ($workspace_association_ids) { - $workspace_associations = $workspace_association_storage->loadMultipleRevisions(array_keys($workspace_association_ids)); - foreach ($workspace_associations as $workspace_association) { - $associated_entity_storage = $this->entityTypeManager->getStorage($workspace_association->target_entity_type_id->value); // Delete the associated entity revision. - if ($entity = $associated_entity_storage->loadRevision($workspace_association->target_entity_revision_id->value)) { - if ($entity->isDefaultRevision()) { - $entity->delete(); - } - else { - $associated_entity_storage->deleteRevision($workspace_association->target_entity_revision_id->value); - } - } - - // Delete the workspace_association revision. - if ($workspace_association->isDefaultRevision()) { - $workspace_association->delete(); - } - else { - $workspace_association_storage->deleteRevision($workspace_association->getRevisionId()); - } + $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 workspace association IDs. If it is empty, we can - // go ahead and remove the deleted workspace ID entry from state. - if (!$this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size)) { + // 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)) { unset($deleted_workspace_ids[$workspace_id]); $this->state->set('workspace.deleted', $deleted_workspace_ids); } } - /** - * Gets a list of workspace association IDs to purge. - * - * @param string $workspace_id - * The ID of the workspace. - * @param int $batch_size - * The maximum number of records that will be purged. - * - * @return array - * An array of workspace association IDs, keyed by their revision IDs. - */ - protected function getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size) { - return $this->entityTypeManager->getStorage('workspace_association') - ->getQuery() - ->allRevisions() - ->accessCheck(FALSE) - ->condition('workspace', $workspace_id) - ->sort('revision_id', 'ASC') - ->range(0, $batch_size) - ->execute(); - } - } diff --git a/core/modules/workspaces/src/WorkspaceOperationFactory.php b/core/modules/workspaces/src/WorkspaceOperationFactory.php index d523365667ac..7b761a4a3994 100644 --- a/core/modules/workspaces/src/WorkspaceOperationFactory.php +++ b/core/modules/workspaces/src/WorkspaceOperationFactory.php @@ -36,6 +36,13 @@ class WorkspaceOperationFactory { */ protected $workspaceManager; + /** + * The workspace association service. + * + * @var \Drupal\workspaces\WorkspaceAssociationInterface + */ + protected $workspaceAssociation; + /** * Constructs a new WorkspacePublisher. * @@ -43,11 +50,16 @@ class WorkspaceOperationFactory { * The entity type manager. * @param \Drupal\Core\Database\Connection $database * Database connection. + * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager + * The workspace manager service. + * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association + * The workspace association service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) { $this->entityTypeManager = $entity_type_manager; $this->database = $database; $this->workspaceManager = $workspace_manager; + $this->workspaceAssociation = $workspace_association; } /** @@ -60,7 +72,7 @@ class WorkspaceOperationFactory { * A workspace publisher object. */ public function getPublisher(WorkspaceInterface $source) { - return new WorkspacePublisher($this->entityTypeManager, $this->database, $this->workspaceManager, $source); + return new WorkspacePublisher($this->entityTypeManager, $this->database, $this->workspaceManager, $this->workspaceAssociation, $source); } } diff --git a/core/modules/workspaces/src/WorkspacePublisher.php b/core/modules/workspaces/src/WorkspacePublisher.php index c7da6b8be782..d38e97383c39 100644 --- a/core/modules/workspaces/src/WorkspacePublisher.php +++ b/core/modules/workspaces/src/WorkspacePublisher.php @@ -36,13 +36,6 @@ class WorkspacePublisher implements WorkspacePublisherInterface { */ protected $database; - /** - * The workspace association storage. - * - * @var \Drupal\workspaces\WorkspaceAssociationStorageInterface - */ - protected $workspaceAssociationStorage; - /** * The workspace manager. * @@ -50,6 +43,13 @@ class WorkspacePublisher implements WorkspacePublisherInterface { */ protected $workspaceManager; + /** + * The workspace association service. + * + * @var \Drupal\workspaces\WorkspaceAssociationInterface + */ + protected $workspaceAssociation; + /** * Constructs a new WorkspacePublisher. * @@ -59,12 +59,14 @@ class WorkspacePublisher implements WorkspacePublisherInterface { * Database connection. * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager * The workspace manager. + * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association + * The workspace association service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceInterface $source) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceInterface $source) { $this->entityTypeManager = $entity_type_manager; $this->database = $database; - $this->workspaceAssociationStorage = $entity_type_manager->getStorage('workspace_association'); $this->workspaceManager = $workspace_manager; + $this->workspaceAssociation = $workspace_association; $this->sourceWorkspace = $source; } @@ -95,6 +97,11 @@ class WorkspacePublisher implements WorkspacePublisherInterface { // revisions. $entity->setSyncing(TRUE); $entity->isDefaultRevision(TRUE); + + // The default revision is not workspace-specific anymore. + $field_name = $entity->getEntityType()->getRevisionMetadataKey('workspace'); + $entity->{$field_name}->target_id = NULL; + $entity->original = $default_revisions[$entity->id()]; $entity->save(); } @@ -107,9 +114,8 @@ class WorkspacePublisher implements WorkspacePublisherInterface { throw $e; } - // Notify the workspace association storage that a workspace has been - // pushed. - $this->workspaceAssociationStorage->postPush($this->sourceWorkspace); + // Notify the workspace association that a workspace has been published. + $this->workspaceAssociation->postPublish($this->sourceWorkspace); } /** @@ -141,7 +147,7 @@ class WorkspacePublisher implements WorkspacePublisherInterface { public function getDifferringRevisionIdsOnTarget() { $target_revision_difference = []; - $tracked_entities = $this->workspaceAssociationStorage->getTrackedEntities($this->sourceWorkspace->id()); + $tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->sourceWorkspace->id()); foreach ($tracked_entities as $entity_type_id => $tracked_revisions) { $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); @@ -171,7 +177,7 @@ class WorkspacePublisher implements WorkspacePublisherInterface { */ public function getDifferringRevisionIdsOnSource() { // Get the Workspace association revisions which haven't been pushed yet. - return $this->workspaceAssociationStorage->getTrackedEntities($this->sourceWorkspace->id()); + return $this->workspaceAssociation->getTrackedEntities($this->sourceWorkspace->id()); } /** diff --git a/core/modules/workspaces/tests/fixtures/update/drupal-8.6.0-workspaces_installed.php b/core/modules/workspaces/tests/fixtures/update/drupal-8.6.0-workspaces_installed.php new file mode 100644 index 0000000000000000000000000000000000000000..7e6276abfead76a94a69250aa7db85f9d06582ef GIT binary patch literal 42910 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 = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz', + __DIR__ . '/../../../fixtures/update/drupal-8.6.0-workspaces_installed.php', + ]; + } + + /** + * Tests the move of workspace association data to a custom table. + * + * @see workspaces_update_8801() + * @see workspaces_post_update_move_association_data() + */ + public function testWorkspaceAssociationRemoval() { + $database = \Drupal::database(); + + // Check that we have two records in the 'workspace_association' base table + // and three records in its revision table. + $wa_records = $database->select('workspace_association')->countQuery()->execute()->fetchField(); + $this->assertEquals(2, $wa_records); + $war_records = $database->select('workspace_association_revision')->countQuery()->execute()->fetchField(); + $this->assertEquals(3, $war_records); + + // Check that the node entity type does not have a 'workspace' field. + $this->assertNull(\Drupal::entityDefinitionUpdateManager()->getFieldStorageDefinition('workspace', 'node')); + + $this->runUpdates(); + + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Check that the 'workspace' field has been installed for an entity type + // that was workspace-supported before Drupal 8.7.0. + $this->assertTrue($entity_definition_update_manager->getFieldStorageDefinition('workspace', 'node')); + + // Check that the 'workspace' field has been installed for an entity type + // which became workspace-supported as part of an entity schema update. + $this->assertTrue($entity_definition_update_manager->getFieldStorageDefinition('workspace', 'taxonomy_term')); + + // Check that the 'workspace' revision metadata field has been created only + // in the revision table. + $schema = $database->schema(); + $this->assertTrue($schema->fieldExists('node_revision', 'workspace')); + $this->assertFalse($schema->fieldExists('node', 'workspace')); + $this->assertFalse($schema->fieldExists('node_field_data', 'workspace')); + $this->assertFalse($schema->fieldExists('node_field_revision', 'workspace')); + + // Check that the 'workspace_association' records have been migrated + // properly. + $wa_records = $database->select('workspace_association')->fields('workspace_association')->execute()->fetchAll(\PDO::FETCH_ASSOC); + $expected = [ + [ + 'workspace' => 'stage', + 'target_entity_type_id' => 'node', + 'target_entity_id' => '1', + 'target_entity_revision_id' => '2', + ], + [ + 'workspace' => 'dev', + 'target_entity_type_id' => 'node', + 'target_entity_id' => '8', + 'target_entity_revision_id' => '10', + ], + ]; + $this->assertEquals($expected, $wa_records); + + // Check that the 'workspace_association' revisions has been migrated + // properly to the new 'workspace' revision metadata field. + $revisions = \Drupal::entityTypeManager()->getStorage('node')->loadMultipleRevisions([2, 9, 10]); + $this->assertEquals('stage', $revisions[2]->workspace->target_id); + $this->assertEquals('dev', $revisions[9]->workspace->target_id); + $this->assertEquals('dev', $revisions[10]->workspace->target_id); + + // Check that the 'workspace_association' entity type has been uninstalled. + $this->assertNull($entity_definition_update_manager->getEntityType('workspace_association')); + $this->assertNull($entity_definition_update_manager->getFieldStorageDefinition('id', 'workspace_association')); + $this->assertNull(\Drupal::keyValue('entity.storage_schema.sql')->get('workspace_association.entity_schema_data')); + + // Check that the 'workspace_association_revision' table has been removed. + $this->assertFalse($schema->tableExists('workspace_association_revision')); + } + +} diff --git a/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php b/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php index e652e3fc826b..7e09248c0b5d 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php @@ -36,6 +36,12 @@ class WorkspacesUninstallTest extends BrowserTestBase { $this->drupalPostForm(NULL, [], 'Uninstall'); $session->pageTextContains('The selected modules have been uninstalled.'); $session->pageTextNotContains('Workspaces'); + + $this->assertFalse(\Drupal::database()->schema()->fieldExists('node_revision', 'workspace')); + + // Verify that the revision metadata key has been removed. + $revision_metadata_keys = \Drupal::entityDefinitionUpdateManager()->getEntityType('node')->get('revision_metadata_keys'); + $this->assertArrayNotHasKey('workspace', $revision_metadata_keys); } } diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceAccessTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceAccessTest.php index b8065a15c8dd..f8a4964eda30 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceAccessTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceAccessTest.php @@ -33,7 +33,6 @@ class WorkspaceAccessTest extends KernelTestBase { $this->installSchema('system', ['sequences']); $this->installEntitySchema('workspace'); - $this->installEntitySchema('workspace_association'); $this->installEntitySchema('user'); // User 1. diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php index d7d5710c5701..c8b1705a7e07 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceCRUDTest.php @@ -7,7 +7,6 @@ use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\workspaces\Entity\Workspace; -use Drupal\workspaces\Entity\WorkspaceAssociation; /** * Tests CRUD operations for workspaces. @@ -19,6 +18,7 @@ class WorkspaceCRUDTest extends KernelTestBase { use UserCreationTrait; use NodeCreationTrait; use ContentTypeCreationTrait; + use WorkspaceTestTrait; /** * The entity type manager. @@ -66,7 +66,7 @@ class WorkspaceCRUDTest extends KernelTestBase { $this->installSchema('node', ['node_access']); $this->installEntitySchema('workspace'); - $this->installEntitySchema('workspace_association'); + $this->installSchema('workspaces', ['workspace_association']); $this->installEntitySchema('node'); $this->installConfig(['filter', 'node', 'system']); @@ -91,10 +91,8 @@ class WorkspaceCRUDTest extends KernelTestBase { ]); $this->setCurrentUser($admin); - /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ - $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); - /** @var \Drupal\node\NodeStorageInterface $node_storage */ - $node_storage = $this->entityTypeManager->getStorage('node'); + /** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */ + $workspace_association = \Drupal::service('workspaces.association'); // Create a workspace with a very small number of associated node revisions. $workspace_1 = Workspace::create([ @@ -106,6 +104,12 @@ class WorkspaceCRUDTest extends KernelTestBase { $workspace_1_node_1 = $this->createNode(['status' => FALSE]); $workspace_1_node_2 = $this->createNode(['status' => FALSE]); + + // The 'live' workspace should have 2 revisions now. The initial revision + // for each node. + $live_revisions = $this->getUnassociatedRevisions('node'); + $this->assertCount(2, $live_revisions); + for ($i = 0; $i < 4; $i++) { $workspace_1_node_1->setNewRevision(TRUE); $workspace_1_node_1->save(); @@ -114,9 +118,17 @@ class WorkspaceCRUDTest extends KernelTestBase { $workspace_1_node_2->save(); } - // The workspace should have 10 associated node revisions, 5 for each node. - $associated_revisions = $workspace_association_storage->getTrackedEntities($workspace_1->id(), TRUE); - $this->assertCount(10, $associated_revisions['node']); + // The workspace should now track 2 nodes. + $tracked_entities = $workspace_association->getTrackedEntities($workspace_1->id()); + $this->assertCount(2, $tracked_entities['node']); + + // There should still be 2 revisions associated with 'live'. + $live_revisions = $this->getUnassociatedRevisions('node'); + $this->assertCount(2, $live_revisions); + + // The other 8 revisions should be associated with 'workspace_1'. + $associated_revisions = $workspace_association->getAssociatedRevisions($workspace_1->id(), 'node'); + $this->assertCount(8, $associated_revisions); // Check that we are allowed to delete the workspace. $this->assertTrue($workspace_1->access('delete', $admin)); @@ -125,14 +137,17 @@ class WorkspaceCRUDTest extends KernelTestBase { // entities and all the node revisions have been deleted as well. $workspace_1->delete(); - $associated_revisions = $workspace_association_storage->getTrackedEntities($workspace_1->id(), TRUE); + // There are no more tracked entities in 'workspace_1'. + $tracked_entities = $workspace_association->getTrackedEntities($workspace_1->id()); + $this->assertEmpty($tracked_entities); + + // There are no more revisions associated with 'workspace_1'. + $associated_revisions = $workspace_association->getAssociatedRevisions($workspace_1->id(), 'node'); $this->assertCount(0, $associated_revisions); - $node_revision_count = $node_storage - ->getQuery() - ->allRevisions() - ->count() - ->execute(); - $this->assertEquals(0, $node_revision_count); + + // There should still be 2 revisions associated with 'live'. + $live_revisions = $this->getUnassociatedRevisions('node'); + $this->assertCount(2, $live_revisions); // Create another workspace, this time with a larger number of associated // node revisions so we can test the batch purge process. @@ -149,16 +164,27 @@ class WorkspaceCRUDTest extends KernelTestBase { $workspace_2_node_1->save(); } - // The workspace should have 60 associated node revisions. - $associated_revisions = $workspace_association_storage->getTrackedEntities($workspace_2->id(), TRUE); - $this->assertCount(60, $associated_revisions['node']); + // Now there is one entity tracked in 'workspace_2'. + $tracked_entities = $workspace_association->getTrackedEntities($workspace_2->id()); + $this->assertCount(1, $tracked_entities['node']); - // Delete the workspace and check that we still have 10 revision left to + // One revision of this entity is in 'live'. + $live_revisions = $this->getUnassociatedRevisions('node', [$workspace_2_node_1->id()]); + $this->assertCount(1, $live_revisions); + + // The other 59 are associated with 'workspace_2'. + $associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]); + $this->assertCount(59, $associated_revisions); + + // Delete the workspace and check that we still have 9 revision left to // delete. $workspace_2->delete(); + $associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]); + $this->assertCount(9, $associated_revisions); - $associated_revisions = $workspace_association_storage->getTrackedEntities($workspace_2->id(), TRUE); - $this->assertCount(10, $associated_revisions['node']); + // The live revision is also still there. + $live_revisions = $this->getUnassociatedRevisions('node', [$workspace_2_node_1->id()]); + $this->assertCount(1, $live_revisions); $workspace_deleted = \Drupal::state()->get('workspace.deleted'); $this->assertCount(1, $workspace_deleted); @@ -177,41 +203,94 @@ class WorkspaceCRUDTest extends KernelTestBase { // from the "workspace.delete" state entry. \Drupal::service('cron')->run(); - $associated_revisions = $workspace_association_storage->getTrackedEntities($workspace_2->id(), TRUE); + $associated_revisions = $workspace_association->getTrackedEntities($workspace_2->id()); $this->assertCount(0, $associated_revisions); - $node_revision_count = $node_storage - ->getQuery() - ->allRevisions() - ->count() - ->execute(); - $this->assertEquals(0, $node_revision_count); + + // 'workspace_2 'is empty now. + $associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]); + $this->assertCount(0, $associated_revisions); + $tracked_entities = $workspace_association->getTrackedEntities($workspace_2->id()); + $this->assertEmpty($tracked_entities); + + // The 3 revisions in 'live' remain. + $live_revisions = $this->getUnassociatedRevisions('node'); + $this->assertCount(3, $live_revisions); $workspace_deleted = \Drupal::state()->get('workspace.deleted'); $this->assertCount(0, $workspace_deleted); } /** - * Tests workspace association validation. - * - * @covers \Drupal\workspaces\Entity\WorkspaceAssociation::validate + * Tests that deleting a workspace keeps its already published content. */ - public function testWorkspaceAssociationValidation() { + public function testDeletingPublishedWorkspace() { + $admin = $this->createUser([ + 'administer nodes', + 'create workspace', + 'view any workspace', + 'delete any workspace', + ]); + $this->setCurrentUser($admin); + + $live_workspace = Workspace::create([ + 'id' => 'live', + 'label' => 'Live', + ]); + $live_workspace->save(); $workspace = Workspace::create([ - 'id' => 'gibbon', - 'label' => 'Gibbon', + 'id' => 'stage', + 'label' => 'Stage', ]); $workspace->save(); - $node = $this->createNode(); + $this->workspaceManager->setActiveWorkspace($workspace); - $workspace_association = WorkspaceAssociation::create([ - 'workspace' => $workspace, - 'target_entity_type_id' => $node->getEntityTypeId(), - 'target_entity_id' => $node->id(), - 'target_entity_revision_id' => $node->getRevisionId(), - ]); + // Create a new node in the 'stage' workspace + $node = $this->createNode(['status' => TRUE]); - $violations = $workspace_association->validate(); - $this->assertCount(0, $violations); + // Create an additional workspace-specific revision for the node. + $node->setNewRevision(TRUE); + $node->save(); + + // The node should have 3 revisions now: a default and 2 pending ones. + $revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3]); + $this->assertCount(3, $revisions); + $this->assertTrue($revisions[1]->isDefaultRevision()); + $this->assertFalse($revisions[2]->isDefaultRevision()); + $this->assertFalse($revisions[3]->isDefaultRevision()); + + // Publish the workspace, which should mark revision 3 as the default one + // and keep revision 2 as a 'source' draft revision. + $workspace->publish(); + $revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3]); + $this->assertFalse($revisions[1]->isDefaultRevision()); + $this->assertFalse($revisions[2]->isDefaultRevision()); + $this->assertTrue($revisions[3]->isDefaultRevision()); + + // Create two new workspace-revisions for the node. + $node->setNewRevision(TRUE); + $node->save(); + $node->setNewRevision(TRUE); + $node->save(); + + // The node should now have 5 revisions. + $revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3, 4, 5]); + $this->assertFalse($revisions[1]->isDefaultRevision()); + $this->assertFalse($revisions[2]->isDefaultRevision()); + $this->assertTrue($revisions[3]->isDefaultRevision()); + $this->assertFalse($revisions[4]->isDefaultRevision()); + $this->assertFalse($revisions[5]->isDefaultRevision()); + + // Delete the workspace and check that only the two new pending revisions + // were deleted by the workspace purging process. + $workspace->delete(); + + $revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3, 4, 5]); + $this->assertCount(3, $revisions); + $this->assertFalse($revisions[1]->isDefaultRevision()); + $this->assertFalse($revisions[2]->isDefaultRevision()); + $this->assertTrue($revisions[3]->isDefaultRevision()); + $this->assertFalse(isset($revisions[4])); + $this->assertFalse(isset($revisions[5])); } } diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php index 20a7efd04e30..1530f3865830 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php @@ -275,7 +275,7 @@ class WorkspaceIntegrationTest extends KernelTestBase { ], ]); $test_scenarios['add_published_node_in_stage'] = $revision_state; - $expected_workspace_association['add_published_node_in_stage'] = ['stage' => [3, 4, 5, 6, 7]]; + $expected_workspace_association['add_published_node_in_stage'] = ['stage' => [3, 4, 5, 7]]; // Deploying 'stage' to 'live' should simply make the latest revisions in // 'stage' the default ones in 'live'. @@ -365,8 +365,9 @@ class WorkspaceIntegrationTest extends KernelTestBase { $this->switchToWorkspace('stage'); // Add a workspace-specific revision to a pre-existing node. - $this->nodes[1]->title->value = 'stage - 2 - r3 - published'; - $this->nodes[1]->save(); + $node = $this->entityTypeManager->getStorage('node')->load(2); + $node->title->value = 'stage - 2 - r3 - published'; + $node->save(); $query = $this->entityTypeManager->getStorage('node')->getQuery(); $query->sort('nid'); @@ -809,7 +810,7 @@ class WorkspaceIntegrationTest extends KernelTestBase { } /** - * Checks the workspace_association entries for a test scenario. + * Checks the workspace_association records for a test scenario. * * @param array $expected * An array of expected values, as defined in ::testWorkspaces(). @@ -817,10 +818,10 @@ class WorkspaceIntegrationTest extends KernelTestBase { * The ID of the entity type that is being tested. */ protected function assertWorkspaceAssociation(array $expected, $entity_type_id) { - /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ - $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); + /** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */ + $workspace_association = \Drupal::service('workspaces.association'); foreach ($expected as $workspace_id => $expected_tracked_revision_ids) { - $tracked_entities = $workspace_association_storage->getTrackedEntities($workspace_id, TRUE); + $tracked_entities = $workspace_association->getTrackedEntities($workspace_id, $entity_type_id); $tracked_revision_ids = isset($tracked_entities[$entity_type_id]) ? $tracked_entities[$entity_type_id] : []; $this->assertEquals($expected_tracked_revision_ids, array_keys($tracked_revision_ids)); } diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceInternalResourceTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceInternalResourceTest.php deleted file mode 100644 index 06201ce7446d..000000000000 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceInternalResourceTest.php +++ /dev/null @@ -1,43 +0,0 @@ -expectException(PluginNotFoundException::class); - $this->expectExceptionMessage('The "entity:workspace_association" plugin does not exist.'); - RestResourceConfig::create([ - 'id' => 'entity.workspace_association', - 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY, - 'configuration' => [ - 'methods' => ['GET'], - 'formats' => ['json'], - 'authentication' => ['cookie'], - ], - ]) - ->enable() - ->save(); - } - -} diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php index 13cff6a3dfeb..50cf4704c396 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php @@ -35,7 +35,7 @@ trait WorkspaceTestTrait { $this->workspaceManager = \Drupal::service('workspaces.manager'); $this->installEntitySchema('workspace'); - $this->installEntitySchema('workspace_association'); + $this->installSchema('workspaces', ['workspace_association']); // Create two workspaces by default, 'live' and 'stage'. $this->workspaces['live'] = Workspace::create(['id' => 'live']); @@ -64,4 +64,33 @@ trait WorkspaceTestTrait { \Drupal::service('workspaces.manager')->setActiveWorkspace($workspace); } + /** + * Returns all the revisions which are not associated with any workspace. + * + * @param string $entity_type_id + * An entity type ID to find revisions for. + * @param int[]|string[]|null $entity_ids + * (optional) An array of entity IDs to filter the results by. Defaults to + * NULL. + * + * @return array + * An array of entity IDs, keyed by revision IDs. + */ + protected function getUnassociatedRevisions($entity_type_id, $entity_ids = NULL) { + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + + $query = \Drupal::entityTypeManager() + ->getStorage($entity_type_id) + ->getQuery() + ->allRevisions() + ->accessCheck(FALSE) + ->notExists($entity_type->get('revision_metadata_keys')['workspace']); + + if ($entity_ids) { + $query->condition($entity_type->getKey('id'), $entity_ids, 'IN'); + } + + return $query->execute(); + } + } diff --git a/core/modules/workspaces/workspaces.install b/core/modules/workspaces/workspaces.install index c6ac304b6db3..2bfc003f0e10 100644 --- a/core/modules/workspaces/workspaces.install +++ b/core/modules/workspaces/workspaces.install @@ -5,6 +5,8 @@ * Contains install, update and uninstall functions for the Workspaces module. */ +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\workspaces\Entity\Workspace; /** @@ -32,6 +34,27 @@ function workspaces_requirements($phase) { return $requirements; } +/** + * Implements hook_module_preinstall(). + */ +function workspaces_module_preinstall($module) { + if ($module !== 'workspaces') { + return; + } + + /** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspaces.manager'); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) { + $revision_metadata_keys = $entity_type->get('revision_metadata_keys'); + if ($workspace_manager->isEntityTypeSupported($entity_type)) { + $revision_metadata_keys['workspace'] = 'workspace'; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + $entity_definition_update_manager->updateEntityType($entity_type); + } + } +} + /** * Implements hook_install(). */ @@ -61,3 +84,83 @@ function workspaces_install() { 'uid' => $owner_id, ])->save(); } + +/** + * Implements hook_schema(). + */ +function workspaces_schema() { + $schema['workspace_association'] = [ + 'description' => 'Stores the association between entity revisions and their workspace.', + 'fields' => [ + 'workspace' => [ + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The workspace ID.', + ], + 'target_entity_type_id' => [ + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The ID of the associated entity type.', + ], + 'target_entity_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The ID of the associated entity.', + ], + 'target_entity_revision_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The revision ID of the associated entity.', + ], + ], + 'indexes' => [ + 'target_entity_revision_id' => ['target_entity_revision_id'], + ], + 'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id'], + ]; + + return $schema; +} + +/** + * Add the 'workspace' revision metadata field on all supported entity types. + */ +function workspaces_update_8801() { + /** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspaces.manager'); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type_id => $entity_type) { + if ($workspace_manager->isEntityTypeSupported($entity_type)) { + $revision_metadata_keys = $entity_type->get('revision_metadata_keys'); + + if (!isset($revision_metadata_keys['workspace'])) { + // Bail out if there's an existing field called 'workspace'. + if ($entity_definition_update_manager->getFieldStorageDefinition('workspace', $entity_type_id)) { + throw new \RuntimeException("An existing 'workspace' field was found for the '$entity_type_id' entity type. Set the 'workspace' revision metadata key to use a different field name and run this update function again."); + } + + $revision_metadata_keys['workspace'] = 'workspace'; + $entity_type->set('revision_metadata_keys', $revision_metadata_keys); + $entity_definition_update_manager->updateEntityType($entity_type); + } + + $field_storage = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Workspace')) + ->setDescription(t('Indicates the workspace that this revision belongs to.')) + ->setSetting('target_type', 'workspace') + ->setInternal(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(TRUE); + + $entity_definition_update_manager->installFieldStorageDefinition($revision_metadata_keys['workspace'], $entity_type_id, 'workspaces', $field_storage); + } + } + + return t("The 'workspace' revision metadata field has been installed."); +} diff --git a/core/modules/workspaces/workspaces.module b/core/modules/workspaces/workspaces.module index d47985b223de..c3b0d0776274 100644 --- a/core/modules/workspaces/workspaces.module +++ b/core/modules/workspaces/workspaces.module @@ -8,6 +8,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; @@ -66,6 +67,15 @@ function workspaces_field_info_alter(&$definitions) { ->fieldInfoAlter($definitions); } +/** + * Implements hook_entity_base_field_info(). + */ +function workspaces_entity_base_field_info(EntityTypeInterface $entity_type) { + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityTypeInfo::class) + ->entityBaseFieldInfo($entity_type); +} + /** * Implements hook_entity_preload(). */ diff --git a/core/modules/workspaces/workspaces.post_update.php b/core/modules/workspaces/workspaces.post_update.php index 0df5add2cb8d..ac45f3f93285 100644 --- a/core/modules/workspaces/workspaces.post_update.php +++ b/core/modules/workspaces/workspaces.post_update.php @@ -5,6 +5,11 @@ * Post update functions for the Workspaces module. */ +use Drupal\Core\Entity\ContentEntityNullStorage; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\Sql\SqlContentEntityStorage; +use Drupal\Core\Site\Settings; + /** * Clear caches due to access changes. */ @@ -19,3 +24,122 @@ function workspaces_post_update_remove_default_workspace() { $workspace->delete(); } } + +/** + * Move the workspace association data to an entity field and a custom table. + */ +function workspaces_post_update_move_association_data(&$sandbox) { + $database = \Drupal::database(); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + $entity_type_manager = \Drupal::entityTypeManager(); + $entity_type = $entity_definition_update_manager->getEntityType('workspace_association'); + + // We can't migrate the workspace association data if the entity type is not + // using its default storage. + if ($entity_type->getHandlerClasses()['storage'] !== 'Drupal\workspaces\WorkspaceAssociationStorage') { + return; + } + + // Since the custom storage class doesn't exist anymore, we have to use core's + // default storage. + $entity_type->setStorageClass(SqlContentEntityStorage::class); + + // If 'progress' is not set, this will be the first run of the batch. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['current_id'] = -1; + + // Create a temporary table for the new workspace_association index. + $schema = [ + 'description' => 'Stores the association between entity revisions and their workspace.', + 'fields' => [ + 'workspace' => [ + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The workspace ID.', + ], + 'target_entity_type_id' => [ + 'type' => 'varchar_ascii', + 'length' => EntityTypeInterface::ID_MAX_LENGTH, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The ID of the associated entity type.', + ], + 'target_entity_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The ID of the associated entity.', + ], + 'target_entity_revision_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The revision ID of the associated entity.', + ], + ], + 'indexes' => [ + 'target_entity_revision_id' => ['target_entity_revision_id'], + ], + 'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id'], + ]; + if ($database->schema()->tableExists('tmp_workspace_association')) { + $database->schema()->dropTable('tmp_workspace_association'); + } + $database->schema()->createTable('tmp_workspace_association', $schema); + + // Copy all the data from the base table of the 'workspace_association' + // entity type to the temporary association table. + $select = $database->select($entity_type->getBaseTable()) + ->fields($entity_type->getBaseTable(), ['workspace', 'target_entity_type_id', 'target_entity_id', 'target_entity_revision_id']); + $database->insert('tmp_workspace_association')->from($select)->execute(); + } + + $table_name = $entity_type->getRevisionTable(); + $revision_field_name = 'revision_id'; + + // Get the next entity association revision records to migrate. + $step_size = Settings::get('entity_update_batch_size', 50); + $workspace_association_records = $database->select($table_name, 't') + ->condition("t.$revision_field_name", $sandbox['current_id'], '>') + ->fields('t') + ->orderBy($revision_field_name, 'ASC') + ->range(0, $step_size) + ->execute() + ->fetchAll(); + + foreach ($workspace_association_records as $record) { + // Set the workspace reference on the tracked entity revision. + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + $revision = $entity_type_manager->getStorage($record->target_entity_type_id)->loadRevision($record->target_entity_revision_id); + $revision->set('workspace', $record->workspace); + $revision->setSyncing(TRUE); + $revision->save(); + + $sandbox['progress']++; + $sandbox['current_id'] = $record->{$revision_field_name}; + } + + // Get an updated count of workspace_association revisions that still need to + // be migrated to the new storage. + $missing = $database->select($table_name, 't') + ->condition("t.$revision_field_name", $sandbox['current_id'], '>') + ->orderBy($revision_field_name, 'ASC') + ->countQuery() + ->execute() + ->fetchField(); + $sandbox['#finished'] = $missing ? $sandbox['progress'] / ($sandbox['progress'] + (int) $missing) : 1; + + // Uninstall the 'workspace_association' entity type and rename the temporary + // table. + if ($sandbox['#finished'] == 1) { + $entity_type->setStorageClass(ContentEntityNullStorage::class); + $entity_definition_update_manager->uninstallEntityType($entity_type); + $database->schema()->dropTable('workspace_association'); + $database->schema()->dropTable('workspace_association_revision'); + + $database->schema()->renameTable('tmp_workspace_association', 'workspace_association'); + } +} diff --git a/core/modules/workspaces/workspaces.services.yml b/core/modules/workspaces/workspaces.services.yml index 9823d5ce2ec9..45f1d5074614 100644 --- a/core/modules/workspaces/workspaces.services.yml +++ b/core/modules/workspaces/workspaces.services.yml @@ -1,12 +1,17 @@ services: workspaces.manager: class: Drupal\workspaces\WorkspaceManager - arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver'] + arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver', '@workspaces.association'] tags: - { name: service_id_collector, tag: workspace_negotiator } workspaces.operation_factory: class: Drupal\workspaces\WorkspaceOperationFactory - arguments: ['@entity_type.manager', '@database', '@workspaces.manager'] + arguments: ['@entity_type.manager', '@database', '@workspaces.manager', '@workspaces.association'] + workspaces.association: + class: Drupal\workspaces\WorkspaceAssociation + arguments: ['@database', '@entity_type.manager'] + tags: + - { name: backend_overridable } workspaces.negotiator.session: class: Drupal\workspaces\Negotiator\SessionWorkspaceNegotiator @@ -25,6 +30,12 @@ services: tags: - { name: access_check, applies_to: _has_active_workspace } + workspaces.entity_schema_listener: + class: Drupal\workspaces\EventSubscriber\EntitySchemaSubscriber + arguments: ['@entity.definition_update_manager', '@entity.last_installed_schema.repository', '@workspaces.manager'] + tags: + - { name: 'event_subscriber' } + cache_context.workspace: class: Drupal\workspaces\WorkspaceCacheContext arguments: ['@workspaces.manager']