diff --git a/core/lib/Drupal/Core/Entity/EntityTypeListener.php b/core/lib/Drupal/Core/Entity/EntityTypeListener.php index 0adcecfb62..4bf05404d9 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeListener.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeListener.php @@ -71,12 +71,12 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { $storage->onEntityTypeCreate($entity_type); } - $this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type)); - $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) { $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id)); } + + $this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type)); } /** @@ -94,9 +94,9 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI $storage->onEntityTypeUpdate($entity_type, $original); } - $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); - $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); + + $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); } /** @@ -116,9 +116,9 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { $storage->onEntityTypeDelete($entity_type); } - $this->eventDispatcher->dispatch(EntityTypeEvents::DELETE, new EntityTypeEvent($entity_type)); - $this->entityLastInstalledSchemaRepository->deleteLastInstalledDefinition($entity_type_id); + + $this->eventDispatcher->dispatch(EntityTypeEvents::DELETE, new EntityTypeEvent($entity_type)); } /** @@ -135,12 +135,12 @@ public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, En } if ($sandbox === NULL || (isset($sandbox['#finished']) && $sandbox['#finished'] == 1)) { - $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); - $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) { $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($entity_type_id, $field_storage_definitions); } + + $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); } } diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php index fda621b929..9194880fa8 100644 --- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php @@ -85,10 +85,10 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $ $storage->onFieldStorageDefinitionCreate($storage_definition); } - $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::CREATE, new FieldStorageDefinitionEvent($storage_definition)); - $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinition($storage_definition); $this->entityFieldManager->clearCachedFieldDefinitions(); + + $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::CREATE, new FieldStorageDefinitionEvent($storage_definition)); } /** @@ -104,10 +104,10 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $ $storage->onFieldStorageDefinitionUpdate($storage_definition, $original); } - $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::UPDATE, new FieldStorageDefinitionEvent($storage_definition, $original)); - $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinition($storage_definition); $this->entityFieldManager->clearCachedFieldDefinitions(); + + $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::UPDATE, new FieldStorageDefinitionEvent($storage_definition, $original)); } /** @@ -133,10 +133,10 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $ $storage->onFieldStorageDefinitionDelete($storage_definition); } - $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::DELETE, new FieldStorageDefinitionEvent($storage_definition)); - $this->entityLastInstalledSchemaRepository->deleteLastInstalledFieldStorageDefinition($storage_definition); $this->entityFieldManager->clearCachedFieldDefinitions(); + + $this->eventDispatcher->dispatch(FieldStorageDefinitionEvents::DELETE, new FieldStorageDefinitionEvent($storage_definition)); } } diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 1371ea0721..b6f47663eb 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1441,3 +1441,22 @@ 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) { + 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/system/tests/modules/entity_test/entity_test.services.yml b/core/modules/system/tests/modules/entity_test/entity_test.services.yml index 75e1bf33ce..4e202ae8fd 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.services.yml +++ b/core/modules/system/tests/modules/entity_test/entity_test.services.yml @@ -1,7 +1,7 @@ services: entity_test.definition.subscriber: class: Drupal\entity_test\EntityTestDefinitionSubscriber - arguments: ['@state'] + arguments: ['@state', '@entity.last_installed_schema.repository'] tags: - { name: event_subscriber } cache_context.entity_test_view_grants: diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php b/core/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php index 6cdceb6d9c..03e768fb3c 100644 --- a/core/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php +++ b/core/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\entity_test; +use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; use Drupal\Core\Entity\EntityTypeEvents; use Drupal\Core\Entity\EntityTypeEventSubscriberTrait; use Drupal\Core\Entity\EntityTypeInterface; @@ -28,6 +29,13 @@ class EntityTestDefinitionSubscriber implements EventSubscriberInterface, Entity */ protected $state; + /** + * The last installed schema repository. + * + * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface + */ + protected $entityLastInstalledSchemaRepository; + /** * Flag determining whether events should be tracked. * @@ -38,8 +46,9 @@ class EntityTestDefinitionSubscriber implements EventSubscriberInterface, Entity /** * {@inheritdoc} */ - public function __construct(StateInterface $state) { + public function __construct(StateInterface $state, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository) { $this->state = $state; + $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository; } /** @@ -53,6 +62,9 @@ public static function getSubscribedEvents() { * {@inheritdoc} */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { + if ($this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type->id())) { + $this->storeDefinitionUpdate(EntityTypeEvents::CREATE); + } $this->storeEvent(EntityTypeEvents::CREATE); } @@ -60,6 +72,11 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { * {@inheritdoc} */ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + $last_installed_definition = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type->id()); + if ((string) $last_installed_definition->getLabel() === 'Updated entity test rev') { + $this->storeDefinitionUpdate(EntityTypeEvents::UPDATE); + } + $this->storeEvent(EntityTypeEvents::UPDATE); } @@ -74,6 +91,9 @@ public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, En * {@inheritdoc} */ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + if (!$this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type->id())) { + $this->storeDefinitionUpdate(EntityTypeEvents::DELETE); + } $this->storeEvent(EntityTypeEvents::DELETE); } @@ -81,6 +101,9 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { * {@inheritdoc} */ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + if (isset($this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($storage_definition->getTargetEntityTypeId())[$storage_definition->getName()])) { + $this->storeDefinitionUpdate(FieldStorageDefinitionEvents::CREATE); + } $this->storeEvent(FieldStorageDefinitionEvents::CREATE); } @@ -88,6 +111,10 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $ * {@inheritdoc} */ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $last_installed_definition = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($storage_definition->getTargetEntityTypeId())[$storage_definition->getName()]; + if ((string) $last_installed_definition->getLabel() === 'Updated field storage test') { + $this->storeDefinitionUpdate(FieldStorageDefinitionEvents::UPDATE); + } $this->storeEvent(FieldStorageDefinitionEvents::UPDATE); } @@ -95,6 +122,9 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $ * {@inheritdoc} */ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + if (!isset($this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($storage_definition->getTargetEntityTypeId())[$storage_definition->getName()])) { + $this->storeDefinitionUpdate(FieldStorageDefinitionEvents::DELETE); + } $this->storeEvent(FieldStorageDefinitionEvents::DELETE); } @@ -130,4 +160,30 @@ protected function storeEvent($event_name) { } } + /** + * Checks whether the installed definitions were updated before the event. + * + * @param string $event_name + * The event name. + * + * @return bool + * TRUE if the last installed entity type of field storage definitions have + * been updated before the was fired, FALSE otherwise. + */ + public function hasDefinitionBeenUpdated($event_name) { + return (bool) $this->state->get($event_name . '_updated_definition'); + } + + /** + * Stores the installed definition state for the specified event. + * + * @param string $event_name + * The event name. + */ + protected function storeDefinitionUpdate($event_name) { + if ($this->trackEvents) { + $this->state->set($event_name . '_updated_definition', TRUE); + } + } + } diff --git a/core/modules/system/tests/modules/entity_test_update/entity_test_update.services.yml b/core/modules/system/tests/modules/entity_test_update/entity_test_update.services.yml new file mode 100644 index 0000000000..45d1606daa --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_update/entity_test_update.services.yml @@ -0,0 +1,6 @@ +services: + entity_test_update.entity_schema_listener: + class: Drupal\entity_test_update\EventSubscriber\EntitySchemaSubscriber + arguments: ['@entity.definition_update_manager', '@state'] + tags: + - { name: 'event_subscriber' } diff --git a/core/modules/system/tests/modules/entity_test_update/src/EventSubscriber/EntitySchemaSubscriber.php b/core/modules/system/tests/modules/entity_test_update/src/EventSubscriber/EntitySchemaSubscriber.php new file mode 100644 index 0000000000..37dac651e2 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_update/src/EventSubscriber/EntitySchemaSubscriber.php @@ -0,0 +1,74 @@ +entityDefinitionUpdateManager = $entityDefinitionUpdateManager; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return static::getEntityTypeEvents(); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + // Only add the new base field when a test needs it. + if (!$this->state->get('entity_test_update.install_new_base_field_during_update', FALSE)) { + return; + } + + // Add a new base field when the entity type is updated. + $definitions = $this->state->get('entity_test_update.additional_base_field_definitions', []); + $definitions['new_base_field'] = BaseFieldDefinition::create('string') + ->setName('new_base_field') + ->setLabel(new TranslatableMarkup('A new base field')); + $this->state->set('entity_test_update.additional_base_field_definitions', $definitions); + + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test_update', $definitions['new_base_field']); + } + +} diff --git a/core/modules/workspaces/src/Entity/WorkspaceAssociation.php b/core/modules/workspaces/src/Entity/WorkspaceAssociation.php deleted file mode 100644 index 6c65c81ed0..0000000000 --- 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 f599c1c031..3859d95565 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 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Wor 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 @@ public function entityPreload(array $ids, $entity_type_id) { // 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 @@ 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. + $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 @@ public function entityInsert(EntityInterface $entity) { 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 @@ public function entityUpdate(EntityInterface $entity) { // 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 @@ public function entityPredelete(EntityInterface $entity) { } } - /** - * 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 @@ protected function trackEntity(RevisionableInterface $entity) { * @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; @@ -319,9 +271,7 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, $f $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 5495c7fa4e..7a72eb246d 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 @@ public function entityTypeBuild(array &$entity_types) { 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 @@ public function fieldInfoAlter(&$definitions) { } } + /** + * 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 0000000000..7c3b4cbd82 --- /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 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 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 8086873f9a..195cac732f 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 070e89050b..1543b5fc2b 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 @@ 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 @@ public function __construct(WorkspaceAssociationStorageInterface $workspace_asso */ 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 @@ public function validate($value, Constraint $constraint) { 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 89691d751c..3d2cc4e1df 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 @@ 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 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Wor 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 @@ public static function create(ContainerInterface $container) { 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 && !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 0000000000..c6a59ed3a1 --- /dev/null +++ b/core/modules/workspaces/src/WorkspaceAssociation.php @@ -0,0 +1,155 @@ +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, 'wa'); + $query + ->fields('wa', ['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); + $entity_type = $storage->getEntityType(); + $workspace_field = $entity_type->get('revision_metadata_keys')['workspace']; + $id_field = $entity_type->getKey('id'); + + $query = $storage->getQuery()->allRevisions(); + + // If the workspace ID is NULL, query for the 'live' workspace. + if ($workspace_id) { + $query->condition($workspace_field, $workspace_id); + } + else { + $query->notExists($workspace_field); + } + + // Restrict the result to a set of entity ID's if provided. + if ($entity_ids) { + $query->condition($id_field, $entity_ids, 'IN'); + } + + return $query->execute(); + } + + /** + * {@inheritdoc} + */ + public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) { + $query = $this->database->select(static::TABLE, 'wa'); + $query + ->fields('wa', ['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 0000000000..d2db503268 --- /dev/null +++ b/core/modules/workspaces/src/WorkspaceAssociationInterface.php @@ -0,0 +1,92 @@ +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 24663206e3..0000000000 --- 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 @@ public function __construct(RequestStack $request_stack, EntityTypeManagerInterf $this->state = $state; $this->logger = $logger; $this->classResolver = $class_resolver; + $this->workspaceAssociation = $workspace_association; $this->negotiatorIds = $negotiator_ids; } @@ -267,67 +277,42 @@ public function purgeDeletedWorkspacesBatch() { $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. $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 = $associated_entity_storage->loadRevision($revision_id)) { if ($entity->isDefaultRevision()) { $entity->delete(); } else { - $associated_entity_storage->deleteRevision($workspace_association->target_entity_revision_id->value); + $associated_entity_storage->deleteRevision($revision_id); } } - - // Delete the workspace_association revision. - if ($workspace_association->isDefaultRevision()) { - $workspace_association->delete(); - } - else { - $workspace_association_storage->deleteRevision($workspace_association->getRevisionId()); - } + $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 d523365667..7b761a4a39 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 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Con * 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 5047eb8f2b..b8cfc33640 100644 --- a/core/modules/workspaces/src/WorkspacePublisher.php +++ b/core/modules/workspaces/src/WorkspacePublisher.php @@ -41,18 +41,18 @@ class WorkspacePublisher implements WorkspacePublisherInterface { protected $database; /** - * The workspace association storage. + * The workspace manager. * - * @var \Drupal\workspaces\WorkspaceAssociationStorageInterface + * @var \Drupal\workspaces\WorkspaceManagerInterface */ - protected $workspaceAssociationStorage; + protected $workspaceManager; /** - * The workspace manager. + * The workspace association service. * - * @var \Drupal\workspaces\WorkspaceManagerInterface + * @var \Drupal\workspaces\WorkspaceAssociationInterface */ - protected $workspaceManager; + protected $workspaceAssociation; /** * Constructs a new WorkspacePublisher. @@ -63,12 +63,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; $this->targetWorkspace = $this->entityTypeManager->getStorage('workspace')->load(WorkspaceInterface::DEFAULT_WORKSPACE); } @@ -100,6 +102,11 @@ public function publish() { // 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(); } @@ -112,9 +119,8 @@ public function publish() { 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); } /** @@ -146,7 +152,7 @@ public function checkConflictsOnTarget() { 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); @@ -176,7 +182,7 @@ public function getDifferringRevisionIdsOnTarget() { */ 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 0000000000..7e6276abfe Binary files /dev/null and b/core/modules/workspaces/tests/fixtures/update/drupal-8.6.0-workspaces_installed.php differ diff --git a/core/modules/workspaces/tests/src/Functional/Update/WorkspacesUpdateTest.php b/core/modules/workspaces/tests/src/Functional/Update/WorkspacesUpdateTest.php new file mode 100644 index 0000000000..1459d21d3f --- /dev/null +++ b/core/modules/workspaces/tests/src/Functional/Update/WorkspacesUpdateTest.php @@ -0,0 +1,105 @@ +databaseDumpFiles = [ + __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', 'wa')->countQuery()->execute()->fetchField(); + $this->assertEquals(2, $wa_records); + $war_records = $database->select('workspace_association_revision', 'war')->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', 'wa')->fields('wa')->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 e652e3fc82..e4932eca32 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspacesUninstallTest.php @@ -36,6 +36,12 @@ public function testUninstallingWorkspace() { $this->drupalPostForm(NULL, [], 'Uninstall'); $session->pageTextContains('The selected modules have been uninstalled.'); $session->pageTextNotContains('Workspaces'); + + $this->assertFalse($this->getDatabaseConnection()->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 b8065a15c8..f8a4964eda 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceAccessTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceAccessTest.php @@ -33,7 +33,6 @@ protected function setUp() { $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 d7d5710c57..b02b065c4c 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\NodeCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\workspaces\Entity\Workspace; -use Drupal\workspaces\Entity\WorkspaceAssociation; /** * Tests CRUD operations for workspaces. @@ -66,7 +65,7 @@ protected function setUp() { $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 +90,8 @@ public function testDeletingWorkspaces() { ]); $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 +103,12 @@ public function testDeletingWorkspaces() { $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 = $workspace_association->getAssociatedRevisions(NULL, '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 +117,17 @@ public function testDeletingWorkspaces() { $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 = $workspace_association->getAssociatedRevisions(NULL, '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 +136,17 @@ public function testDeletingWorkspaces() { // 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 = $workspace_association->getAssociatedRevisions(NULL, '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 +163,27 @@ public function testDeletingWorkspaces() { $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 = $workspace_association->getAssociatedRevisions(NULL, '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 = $workspace_association->getAssociatedRevisions(NULL, '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 +202,21 @@ public function testDeletingWorkspaces() { // 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_deleted = \Drupal::state()->get('workspace.deleted'); - $this->assertCount(0, $workspace_deleted); - } + // '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); - /** - * Tests workspace association validation. - * - * @covers \Drupal\workspaces\Entity\WorkspaceAssociation::validate - */ - public function testWorkspaceAssociationValidation() { - $workspace = Workspace::create([ - 'id' => 'gibbon', - 'label' => 'Gibbon', - ]); - $workspace->save(); - $node = $this->createNode(); - - $workspace_association = WorkspaceAssociation::create([ - 'workspace' => $workspace, - 'target_entity_type_id' => $node->getEntityTypeId(), - 'target_entity_id' => $node->id(), - 'target_entity_revision_id' => $node->getRevisionId(), - ]); + // The 3 revisions in 'live' remain. + $live_revisions = $workspace_association->getAssociatedRevisions(NULL, 'node'); + $this->assertCount(3, $live_revisions); - $violations = $workspace_association->validate(); - $this->assertCount(0, $violations); + $workspace_deleted = \Drupal::state()->get('workspace.deleted'); + $this->assertCount(0, $workspace_deleted); } } diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php index 26e1c622b5..587138e56d 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php @@ -275,7 +275,7 @@ public function testWorkspaces() { ], ]); $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 @@ public function testEntityQueryWithoutConditions() { $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'); @@ -805,10 +806,10 @@ protected function assertEntityQuery(array $expected_values, $entity_type_id) { * 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 06201ce744..0000000000 --- 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 13cff6a3df..2d77bbc65f 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php @@ -35,7 +35,7 @@ protected function initializeWorkspacesModule() { $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']); diff --git a/core/modules/workspaces/workspaces.install b/core/modules/workspaces/workspaces.install index 7bcc18b641..4b9a4cd3b2 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(). */ @@ -67,3 +90,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 c080ec4318..d4b8ca3643 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; @@ -65,6 +66,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 2ca0ced399..87d17f2bf1 100644 --- a/core/modules/workspaces/workspaces.post_update.php +++ b/core/modules/workspaces/workspaces.post_update.php @@ -5,8 +5,133 @@ * 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. */ function workspaces_post_update_access_clear_caches() { } + +/** + * 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(), 'wa') + ->fields('wa', ['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 61b83fd06e..67d78cfac0 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.default: class: Drupal\workspaces\Negotiator\DefaultWorkspaceNegotiator @@ -24,6 +29,12 @@ services: tags: - { name: workspace_negotiator, priority: 100 } + 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'] diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 76b63efef1..afe5af6681 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -14,6 +14,8 @@ use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionEvents; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\entity_test\FieldStorageDefinition; use Drupal\entity_test_update\Entity\EntityTestUpdate; use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait; @@ -835,28 +837,47 @@ public function testDefinitionEvents() { $event_subscriber->enableEventTracking(); // Test field storage definition events. - $storage_definition = current(\Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_rev')); - $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.'); - \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition); - $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.'); + $storage_definition = FieldStorageDefinition::create('string') + ->setName('field_storage_test') + ->setLabel(new TranslatableMarkup('Field storage test')) + ->setTargetEntityTypeId('entity_test_rev'); + $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.'); \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition); $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.'); + + $updated_storage_definition = clone $storage_definition; + $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test')); $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.'); - \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($storage_definition, $storage_definition); + \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition); $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.'); + + $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.'); + \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition); + $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.'); // Test entity type events. $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev'); + $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.'); \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type); $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.'); + + $updated_entity_type = clone $entity_type; + $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev')); $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $entity_type); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type); $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.'); + $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.'); \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type); $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.'); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php index 937a2ab5a4..a525f650eb 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php @@ -151,13 +151,17 @@ public function testFieldableEntityTypeUpdates($initial_rev, $initial_mul, $new_ $this->assertEntityData($initial_rev, $initial_mul); } + // Enable the creation of a new base field during a fieldable entity type + // update. + $this->state->set('entity_test_update.install_new_base_field_during_update', TRUE); + // Simulate a batch run since we are converting the entities one by one. $sandbox = []; do { $this->entityDefinitionUpdateManager->updateFieldableEntityType($updated_entity_type, $updated_field_storage_definitions, $sandbox); } while ($sandbox['#finished'] != 1); - $this->assertEntityTypeSchema($new_rev, $new_mul); + $this->assertEntityTypeSchema($new_rev, $new_mul, TRUE); $this->assertEntityData($initial_rev, $initial_mul); $change_list = $this->entityDefinitionUpdateManager->getChangeList(); @@ -427,8 +431,20 @@ protected function assertEntityData($revisionable, $translatable) { * Whether the entity type is revisionable or not. * @param bool $translatable * Whether the entity type is translatable or not. + * @param bool $new_base_field + * (optional) Whether a new base field was added as part of the update. + * Defaults to FALSE. */ - protected function assertEntityTypeSchema($revisionable, $translatable) { + protected function assertEntityTypeSchema($revisionable, $translatable, $new_base_field = FALSE) { + // Check whether the 'new_base_field' field has been installed correctly. + $field_storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('new_base_field', $this->entityTypeId); + if ($new_base_field) { + $this->assertNotNull($field_storage_definition); + } + else { + $this->assertNull($field_storage_definition); + } + if ($revisionable && $translatable) { $this->assertRevisionableAndTranslatable(); }