From 88b2e375af10494be5fa9f74c7dbcb53aa217bd5 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 14 Nov 2024 19:40:55 +0000 Subject: [PATCH] Issue #3473608 by amateescu, plach, ajits: Workspace association does not work with entities with non-numeric IDs (cherry picked from commit 989fe116125eff80d2fa8b6cd2177e93f336268a) --- .../workspaces/src/EntityQuery/QueryTrait.php | 4 +- .../workspaces/src/EntityQuery/Tables.php | 4 +- .../workspaces/src/ViewsQueryAlter.php | 2 +- .../workspaces/src/WorkspaceAssociation.php | 78 ++++++++++++--- .../tests/fixtures/update/workspaces.php | 90 +++++++++++++++++ .../Entity/EntityTestMulRevPubStringId.php | 73 ++++++++++++++ .../workspaces_test/workspaces_test.info.yml | 1 + ...paceAssociationStringIdsUpdatePathTest.php | 49 ++++++++++ .../src/Kernel/WorkspaceAssociationTest.php | 98 ++++++++++++------- .../tests/src/Kernel/WorkspaceTestTrait.php | 21 ++++ core/modules/workspaces/workspaces.install | 37 ++++++- .../Drupal/Tests/UpdatePathTestTrait.php | 27 +++-- 12 files changed, 424 insertions(+), 60 deletions(-) create mode 100644 core/modules/workspaces/tests/fixtures/update/workspaces.php create mode 100644 core/modules/workspaces/tests/modules/workspaces_test/src/Entity/EntityTestMulRevPubStringId.php create mode 100644 core/modules/workspaces/tests/src/Functional/Update/WorkspaceAssociationStringIdsUpdatePathTest.php diff --git a/core/modules/workspaces/src/EntityQuery/QueryTrait.php b/core/modules/workspaces/src/EntityQuery/QueryTrait.php index 415ef4b7a086..eef973cc4563 100644 --- a/core/modules/workspaces/src/EntityQuery/QueryTrait.php +++ b/core/modules/workspaces/src/EntityQuery/QueryTrait.php @@ -4,6 +4,7 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\workspaces\WorkspaceAssociation; use Drupal\workspaces\WorkspaceInformationInterface; use Drupal\workspaces\WorkspaceManagerInterface; @@ -76,7 +77,8 @@ public function prepare() { // can properly include live content along with a possible workspace // revision. $id_field = $this->entityType->getKey('id'); - $this->sqlQuery->leftJoin('workspace_association', 'workspace_association', "[%alias].[target_entity_type_id] = '{$this->entityTypeId}' AND [%alias].[target_entity_id] = [base_table].[$id_field] AND [%alias].[workspace] = '{$active_workspace->id()}'"); + $target_id_field = WorkspaceAssociation::getIdField($this->entityTypeId); + $this->sqlQuery->leftJoin('workspace_association', 'workspace_association', "[%alias].[target_entity_type_id] = '{$this->entityTypeId}' AND [%alias].[$target_id_field] = [base_table].[$id_field] AND [%alias].[workspace] = '{$active_workspace->id()}'"); } return $this; diff --git a/core/modules/workspaces/src/EntityQuery/Tables.php b/core/modules/workspaces/src/EntityQuery/Tables.php index e67e107bfbb0..199d5cc15597 100644 --- a/core/modules/workspaces/src/EntityQuery/Tables.php +++ b/core/modules/workspaces/src/EntityQuery/Tables.php @@ -6,6 +6,7 @@ use Drupal\Core\Entity\EntityType; use Drupal\Core\Entity\Query\Sql\Tables as BaseTables; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\workspaces\WorkspaceAssociation; /** * Alters entity queries to use a workspace revision instead of the default one. @@ -144,10 +145,11 @@ public function addWorkspaceAssociationJoin($entity_type_id, $base_table_alias, if (!isset($this->contentWorkspaceTables[$base_table_alias])) { $entity_type = $this->entityTypeManager->getActiveDefinition($entity_type_id); $id_field = $entity_type->getKey('id'); + $target_id_field = WorkspaceAssociation::getIdField($entity_type_id); // LEFT join the Workspace association entity's table so we can properly // include live content along with a possible workspace-specific revision. - $this->contentWorkspaceTables[$base_table_alias] = $this->sqlQuery->leftJoin('workspace_association', NULL, "[%alias].[target_entity_type_id] = '$entity_type_id' AND [%alias].[target_entity_id] = [$base_table_alias].[$id_field] AND [%alias].[workspace] = '$active_workspace_id'"); + $this->contentWorkspaceTables[$base_table_alias] = $this->sqlQuery->leftJoin('workspace_association', NULL, "[%alias].[target_entity_type_id] = '$entity_type_id' AND [%alias].[$target_id_field] = [$base_table_alias].[$id_field] AND [%alias].[workspace] = '$active_workspace_id'"); $this->baseTablesEntityType[$base_table_alias] = $entity_type->id(); } diff --git a/core/modules/workspaces/src/ViewsQueryAlter.php b/core/modules/workspaces/src/ViewsQueryAlter.php index 4cfb5d78dad3..f409a20b0d9c 100644 --- a/core/modules/workspaces/src/ViewsQueryAlter.php +++ b/core/modules/workspaces/src/ViewsQueryAlter.php @@ -311,7 +311,7 @@ protected function ensureWorkspaceAssociationTable($entity_type_id, Sql $query, // Construct the join. $definition = [ 'table' => 'workspace_association', - 'field' => 'target_entity_id', + 'field' => WorkspaceAssociation::getIdField($entity_type_id), 'left_table' => $relationship, 'left_field' => $table_data['table']['base']['field'], 'extra' => [ diff --git a/core/modules/workspaces/src/WorkspaceAssociation.php b/core/modules/workspaces/src/WorkspaceAssociation.php index 8a93a83e34c5..25b476fab864 100644 --- a/core/modules/workspaces/src/WorkspaceAssociation.php +++ b/core/modules/workspaces/src/WorkspaceAssociation.php @@ -2,6 +2,7 @@ namespace Drupal\workspaces; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Query\PagerSelectExtender; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -60,6 +61,7 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w if (isset($tracked[$entity->getEntityTypeId()])) { $tracked_revision_id = key($tracked[$entity->getEntityTypeId()]); } + $id_field = static::getIdField($entity->getEntityTypeId()); try { $transaction = $this->database->startTransaction(); @@ -72,7 +74,7 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w ]) ->condition('workspace', $affected_workspaces, 'IN') ->condition('target_entity_type_id', $entity->getEntityTypeId()) - ->condition('target_entity_id', $entity->id()) + ->condition($id_field, $entity->id()) // Only update descendant workspaces if they have the same initial // revision, which means they are currently inheriting content. ->condition('target_entity_revision_id', $tracked_revision_id) @@ -86,15 +88,15 @@ public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $w $insert_query = $this->database->insert(static::TABLE) ->fields([ 'workspace', - 'target_entity_revision_id', 'target_entity_type_id', - 'target_entity_id', + $id_field, + 'target_entity_revision_id', ]); foreach ($missing_workspaces as $workspace_id) { $insert_query->values([ 'workspace' => $workspace_id, 'target_entity_type_id' => $entity->getEntityTypeId(), - 'target_entity_id' => $entity->id(), + $id_field => $entity->id(), 'target_entity_revision_id' => $entity->getRevisionId(), ]); } @@ -128,8 +130,13 @@ public function workspaceInsert(WorkspaceInterface $workspace) { */ 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_id_string', + 'target_entity_revision_id', + ]); $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); @@ -137,13 +144,14 @@ public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entit $query->condition('target_entity_type_id', $entity_type_id, '='); if ($entity_ids) { - $query->condition('target_entity_id', $entity_ids, 'IN'); + $query->condition(static::getIdField($entity_type_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; + $target_id = $record->{static::getIdField($record->target_entity_type_id)}; + $tracked_revisions[$record->target_entity_type_id][$record->target_entity_revision_id] = $target_id; } return $tracked_revisions; @@ -160,15 +168,21 @@ public function getTrackedEntitiesForListing($workspace_id, ?int $pager_id = NUL $query->element($pager_id); } + $query->fields(static::TABLE, [ + 'target_entity_type_id', + 'target_entity_id', + 'target_entity_id_string', + 'target_entity_revision_id', + ]); $query - ->fields(static::TABLE, ['target_entity_type_id', 'target_entity_id', 'target_entity_revision_id']) ->orderBy('target_entity_type_id', 'ASC') ->orderBy('target_entity_revision_id', 'DESC') ->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; + $target_id = $record->{static::getIdField($record->target_entity_type_id)}; + $tracked_revisions[$record->target_entity_type_id][$record->target_entity_revision_id] = $target_id; } return $tracked_revisions; @@ -291,17 +305,18 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti * {@inheritdoc} */ public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) { + $id_field = static::getIdField($entity->getEntityTypeId()); $query = $this->database->select(static::TABLE, 'wa') ->fields('wa', ['workspace']) ->condition('[wa].[target_entity_type_id]', $entity->getEntityTypeId()) - ->condition('[wa].[target_entity_id]', $entity->id()); + ->condition("[wa].[$id_field]", $entity->id()); // Use a self-join to get only the workspaces in which the latest revision // of the entity is tracked. if ($latest_revision) { $inner_select = $this->database->select(static::TABLE, 'wai') ->condition('[wai].[target_entity_type_id]', $entity->getEntityTypeId()) - ->condition('[wai].[target_entity_id]', $entity->id()); + ->condition("[wai].[$id_field]", $entity->id()); $inner_select->addExpression('MAX([wai].[target_entity_revision_id])', 'max_revision_id'); $query->join($inner_select, 'waj', '[wa].[target_entity_revision_id] = [waj].[max_revision_id]'); @@ -341,7 +356,18 @@ public function deleteAssociations($workspace_id = NULL, $entity_type_id = NULL, $query->condition('target_entity_type_id', $entity_type_id, '='); if ($entity_ids) { - $query->condition('target_entity_id', $entity_ids, 'IN'); + try { + $query->condition(static::getIdField($entity_type_id), $entity_ids, 'IN'); + } + catch (PluginNotFoundException) { + // When an entity type is being deleted, we no longer have the ability + // to retrieve its identifier field type, so we try both. + $query->condition( + $query->orConditionGroup() + ->condition('target_entity_id', $entity_ids, 'IN') + ->condition('target_entity_id_string', $entity_ids, 'IN') + ); + } } if ($revision_ids) { @@ -366,6 +392,7 @@ public function initializeWorkspace(WorkspaceInterface $workspace) { $indexed_rows->fields(static::TABLE, [ 'target_entity_type_id', 'target_entity_id', + 'target_entity_id_string', 'target_entity_revision_id', ]); $indexed_rows->condition('workspace', $parent_id); @@ -399,4 +426,31 @@ public function onPostPublish(WorkspacePublishEvent $event): void { } } + /** + * Determines the target ID field name for an entity type. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return string + * The name of the workspace association target ID field. + * + * @internal + */ + public static function getIdField(string $entity_type_id): string { + static $id_field_map = []; + + if (!isset($id_field_map[$entity_type_id])) { + $id_field = \Drupal::entityTypeManager()->getDefinition($entity_type_id) + ->getKey('id'); + $field_map = \Drupal::service('entity_field.manager')->getFieldMap()[$entity_type_id]; + + $id_field_map[$entity_type_id] = $field_map[$id_field]['type'] !== 'integer' + ? 'target_entity_id_string' + : 'target_entity_id'; + } + + return $id_field_map[$entity_type_id]; + } + } diff --git a/core/modules/workspaces/tests/fixtures/update/workspaces.php b/core/modules/workspaces/tests/fixtures/update/workspaces.php new file mode 100644 index 000000000000..6928ddb66ced --- /dev/null +++ b/core/modules/workspaces/tests/fixtures/update/workspaces.php @@ -0,0 +1,90 @@ +<?php +// phpcs:ignoreFile + +use Drupal\Core\Database\Database; +use Drupal\Core\Entity\EntityTypeInterface; + +$connection = Database::getConnection(); + +// Set the schema version. +$connection->merge('key_value') + ->fields([ + 'value' => 'i:10000;', + 'name' => 'workspaces', + 'collection' => 'system.schema', + ]) + ->condition('collection', 'system.schema') + ->condition('name', 'workspaces') + ->execute(); + +// Update core.extension. +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['workspaces'] = 0; +$connection->update('config') + ->fields(['data' => serialize($extensions)]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + +// Add all workspaces_removed_post_updates() as existing updates. +require_once __DIR__ . '/../../../../workspaces/workspaces.post_update.php'; +$existing_updates = $connection->select('key_value') + ->fields('key_value', ['value']) + ->condition('collection', 'post_update') + ->condition('name', 'existing_updates') + ->execute() + ->fetchField(); +$existing_updates = unserialize($existing_updates); +$existing_updates = array_merge( + $existing_updates, + array_keys(workspaces_removed_post_updates()) +); +$connection->update('key_value') + ->fields(['value' => serialize($existing_updates)]) + ->condition('collection', 'post_update') + ->condition('name', 'existing_updates') + ->execute(); + +// Create the 'workspace_association' table. +$spec = [ + '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'], +]; +$connection->schema()->createTable('workspace_association', $spec); diff --git a/core/modules/workspaces/tests/modules/workspaces_test/src/Entity/EntityTestMulRevPubStringId.php b/core/modules/workspaces/tests/modules/workspaces_test/src/Entity/EntityTestMulRevPubStringId.php new file mode 100644 index 000000000000..f782cfee8662 --- /dev/null +++ b/core/modules/workspaces/tests/modules/workspaces_test/src/Entity/EntityTestMulRevPubStringId.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\workspaces_test\Entity; + +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\entity_test\Entity\EntityTestMulRevPub; + +/** + * Defines the test entity class. + * + * @ContentEntityType( + * id = "entity_test_mulrevpub_string_id", + * label = @Translation("Test entity - revisions, data table, and published interface"), + * handlers = { + * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder", + * "access" = "Drupal\entity_test\EntityTestAccessControlHandler", + * "form" = { + * "default" = "Drupal\entity_test\EntityTestForm", + * "delete" = "Drupal\entity_test\EntityTestDeleteForm", + * "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm" + * }, + * "views_data" = "Drupal\views\EntityViewsData", + * "route_provider" = { + * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", + * }, + * }, + * base_table = "entity_test_mulrevpub_string_id", + * data_table = "entity_test_mulrevpub_string_id_property_data", + * revision_table = "entity_test_mulrevpub_string_id_revision", + * revision_data_table = "entity_test_mulrevpub_string_id_property_revision", + * admin_permission = "administer entity_test content", + * translatable = TRUE, + * show_revision_ui = TRUE, + * entity_keys = { + * "id" = "id", + * "uuid" = "uuid", + * "bundle" = "type", + * "revision" = "revision_id", + * "label" = "name", + * "langcode" = "langcode", + * "published" = "status", + * }, + * links = { + * "add-form" = "/entity_test_mulrevpub/add", + * "canonical" = "/entity_test_mulrevpub/manage/{entity_test_mulrevpub}", + * "delete-form" = "/entity_test/delete/entity_test_mulrevpub/{entity_test_mulrevpub}", + * "delete-multiple-form" = "/entity_test/delete", + * "edit-form" = "/entity_test_mulrevpub/manage/{entity_test_mulrevpub}/edit", + * "revision" = "/entity_test_mulrevpub/{entity_test_mulrevpub}/revision/{entity_test_mulrevpub_revision}/view", + * } + * ) + */ +class EntityTestMulRevPubStringId extends EntityTestMulRevPub { + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + $fields['id'] = BaseFieldDefinition::create('string') + ->setLabel(t('ID')) + ->setDescription(t('The ID of the test entity.')) + ->setReadOnly(TRUE) + // In order to work around the InnoDB 191 character limit on utf8mb4 + // primary keys, we set the character set for the field to ASCII. + ->setSetting('is_ascii', TRUE); + return $fields; + } + +} diff --git a/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.info.yml b/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.info.yml index 62886a0b12d2..532cbad02f45 100644 --- a/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.info.yml +++ b/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.info.yml @@ -4,4 +4,5 @@ description: 'Provides supporting code for testing workspaces.' package: Testing version: VERSION dependencies: + - drupal:entity_test - drupal:workspaces diff --git a/core/modules/workspaces/tests/src/Functional/Update/WorkspaceAssociationStringIdsUpdatePathTest.php b/core/modules/workspaces/tests/src/Functional/Update/WorkspaceAssociationStringIdsUpdatePathTest.php new file mode 100644 index 000000000000..d36a8b3aa15c --- /dev/null +++ b/core/modules/workspaces/tests/src/Functional/Update/WorkspaceAssociationStringIdsUpdatePathTest.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\workspaces\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * Tests the update path for string IDs in workspace_association. + * + * @group workspaces + */ +class WorkspaceAssociationStringIdsUpdatePathTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected $checkEntityFieldDefinitionUpdates = FALSE; + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles(): void { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz', + __DIR__ . '/../../../fixtures/update/workspaces.php', + ]; + } + + /** + * Tests the update path for string IDs in workspace_association. + */ + public function testRunUpdates(): void { + $schema = \Drupal::database()->schema(); + $find_primary_key_columns = new \ReflectionMethod(get_class($schema), 'findPrimaryKeyColumns'); + + $this->assertFalse($schema->fieldExists('workspace_association', 'target_entity_id_string')); + $primary_key_columns = ['workspace', 'target_entity_type_id', 'target_entity_id']; + $this->assertEquals($primary_key_columns, $find_primary_key_columns->invoke($schema, 'workspace_association')); + + $this->runUpdates(); + + $this->assertTrue($schema->fieldExists('workspace_association', 'target_entity_id_string')); + $primary_key_columns = ['workspace', 'target_entity_type_id', 'target_entity_id', 'target_entity_id_string']; + $this->assertEquals($primary_key_columns, $find_primary_key_columns->invoke($schema, 'workspace_association')); + } + +} diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceAssociationTest.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceAssociationTest.php index 183d5247bfc5..7cb8f5515a4e 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceAssociationTest.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceAssociationTest.php @@ -5,8 +5,6 @@ namespace Drupal\Tests\workspaces\Kernel; use Drupal\KernelTests\KernelTestBase; -use Drupal\Tests\node\Traits\ContentTypeCreationTrait; -use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\workspaces\Entity\Workspace; @@ -19,8 +17,6 @@ */ class WorkspaceAssociationTest extends KernelTestBase { - use ContentTypeCreationTrait; - use NodeCreationTrait; use UserCreationTrait; use WorkspaceTestTrait; @@ -35,13 +31,11 @@ class WorkspaceAssociationTest extends KernelTestBase { * {@inheritdoc} */ protected static $modules = [ - 'field', - 'filter', - 'node', - 'text', + 'entity_test', 'user', 'system', 'workspaces', + 'workspaces_test', ]; /** @@ -52,17 +46,15 @@ protected function setUp(): void { $this->entityTypeManager = \Drupal::entityTypeManager(); - $this->installEntitySchema('node'); + $this->installEntitySchema('entity_test_mulrevpub'); + $this->installEntitySchema('entity_test_mulrevpub_string_id'); $this->installEntitySchema('user'); $this->installEntitySchema('workspace'); - $this->installConfig(['filter', 'node', 'system']); + $this->installConfig(['system']); - $this->installSchema('node', ['node_access']); $this->installSchema('workspaces', ['workspace_association']); - $this->createContentType(['type' => 'article']); - $permissions = array_intersect([ 'administer nodes', 'create workspace', @@ -80,27 +72,33 @@ protected function setUp(): void { /** * Tests the revisions tracked by a workspace. * + * @param string $entity_type_id + * The ID of the entity type to test. + * @param array $entity_values + * An array of values for the entities created in this test. + * * @covers ::getTrackedEntities * @covers ::getAssociatedRevisions + * + * @dataProvider getEntityTypeIds */ - public function testWorkspaceAssociation(): void { - $this->createNode(['title' => 'Test article 1 - live - unpublished', 'type' => 'article', 'status' => 0]); - $this->createNode(['title' => 'Test article 2 - live - published', 'type' => 'article']); + public function testWorkspaceAssociation(string $entity_type_id, array $entity_values): void { + $entity_1 = $this->createEntity($entity_type_id, $entity_values[1]); + $this->createEntity($entity_type_id, $entity_values[2]); // Edit one of the existing nodes in 'stage'. $this->switchToWorkspace('stage'); - $node = $this->entityTypeManager->getStorage('node')->load(1); - $node->setTitle('Test article 1 - stage - published'); - $node->setPublished(); + $entity_1->set('name', 'Test entity 1 - stage - published'); + $entity_1->setPublished(); // This creates rev. 3. - $node->save(); + $entity_1->save(); // Generate content with the following structure: // Stage: - // - Test article 3 - stage - unpublished (rev. 4) - // - Test article 4 - stage - published (rev. 5 and 6) - $this->createNode(['title' => 'Test article 3 - stage - unpublished', 'type' => 'article', 'status' => 0]); - $this->createNode(['title' => 'Test article 4 - stage - published', 'type' => 'article']); + // - Test entity 3 - stage - unpublished (rev. 4) + // - Test entity 4 - stage - published (rev. 5 and 6) + $this->createEntity($entity_type_id, $entity_values[3]); + $this->createEntity($entity_type_id, $entity_values[4]); $expected_latest_revisions = [ 'stage' => [3, 4, 6], @@ -111,17 +109,17 @@ public function testWorkspaceAssociation(): void { $expected_initial_revisions = [ 'stage' => [4, 5], ]; - $this->assertWorkspaceAssociations('node', $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions); + $this->assertWorkspaceAssociations($entity_type_id, $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions); // Dev: - // - Test article 1 - stage - published (rev. 3) - // - Test article 3 - stage - unpublished (rev. 4) - // - Test article 4 - stage - published (rev. 5 and 6) - // - Test article 5 - dev - unpublished (rev. 7) - // - Test article 6 - dev - published (rev. 8 and 9) + // - Test entity 1 - stage - published (rev. 3) + // - Test entity 3 - stage - unpublished (rev. 4) + // - Test entity 4 - stage - published (rev. 5 and 6) + // - Test entity 5 - dev - unpublished (rev. 7) + // - Test entity 6 - dev - published (rev. 8 and 9) $this->switchToWorkspace('dev'); - $this->createNode(['title' => 'Test article 5 - dev - unpublished', 'type' => 'article', 'status' => 0]); - $this->createNode(['title' => 'Test article 6 - dev - published', 'type' => 'article']); + $this->createEntity($entity_type_id, $entity_values[5]); + $this->createEntity($entity_type_id, $entity_values[6]); $expected_latest_revisions += [ 'dev' => [3, 4, 6, 7, 9], @@ -134,7 +132,7 @@ public function testWorkspaceAssociation(): void { $expected_initial_revisions += [ 'dev' => [7, 8], ]; - $this->assertWorkspaceAssociations('node', $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions); + $this->assertWorkspaceAssociations($entity_type_id, $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions); // Merge 'dev' into 'stage' and check the workspace associations. /** @var \Drupal\workspaces\WorkspaceMergerInterface $workspace_merger */ @@ -155,7 +153,7 @@ public function testWorkspaceAssociation(): void { // Which leaves revision 8 as the only remaining initial revision in 'dev'. $expected_initial_revisions['dev'] = [8]; - $this->assertWorkspaceAssociations('node', $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions); + $this->assertWorkspaceAssociations($entity_type_id, $expected_latest_revisions, $expected_all_revisions, $expected_initial_revisions); // Publish 'stage' and check the workspace associations. /** @var \Drupal\workspaces\WorkspacePublisherInterface $workspace_publisher */ @@ -163,7 +161,37 @@ public function testWorkspaceAssociation(): void { $workspace_publisher->publish(); $expected_revisions['stage'] = $expected_revisions['dev'] = []; - $this->assertWorkspaceAssociations('node', $expected_revisions, $expected_revisions, $expected_revisions); + $this->assertWorkspaceAssociations($entity_type_id, $expected_revisions, $expected_revisions, $expected_revisions); + } + + /** + * The data provider for ::testWorkspaceAssociation(). + */ + public static function getEntityTypeIds(): array { + return [ + [ + 'entity_type_id' => 'entity_test_mulrevpub', + 'entity_values' => [ + 1 => ['name' => 'Test entity 1 - live - unpublished', 'status' => FALSE], + 2 => ['name' => 'Test entity 2 - live - published', 'status' => TRUE], + 3 => ['name' => 'Test entity 3 - stage - unpublished', 'status' => FALSE], + 4 => ['name' => 'Test entity 4 - stage - published', 'status' => TRUE], + 5 => ['name' => 'Test entity 5 - dev - unpublished', 'status' => FALSE], + 6 => ['name' => 'Test entity 6 - dev - published', 'status' => TRUE], + ], + ], + [ + 'entity_type_id' => 'entity_test_mulrevpub_string_id', + 'entity_values' => [ + 1 => ['id' => 'test_1', 'name' => 'Test entity 1 - live - unpublished', 'status' => FALSE], + 2 => ['id' => 'test_2', 'name' => 'Test entity 2 - live - published', 'status' => TRUE], + 3 => ['id' => 'test_3', 'name' => 'Test entity 3 - stage - unpublished', 'status' => FALSE], + 4 => ['id' => 'test_4', 'name' => 'Test entity 4 - stage - published', 'status' => TRUE], + 5 => ['id' => 'test_5', 'name' => 'Test entity 5 - dev - unpublished', 'status' => FALSE], + 6 => ['id' => 'test_6', 'name' => 'Test entity 6 - dev - published', 'status' => TRUE], + ], + ], + ]; } /** diff --git a/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php b/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php index 504a1aa30fa5..8391859f46bf 100644 --- a/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php +++ b/core/modules/workspaces/tests/src/Kernel/WorkspaceTestTrait.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\workspaces\Kernel; +use Drupal\Core\Entity\EntityInterface; use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler; use Drupal\workspaces\Entity\Workspace; @@ -163,4 +164,24 @@ protected function ignoreEntityType(string $entity_type_id): void { \Drupal::entityTypeManager()->clearCachedDefinitions(); } + /** + * Creates an entity. + * + * @param string $entity_type_id + * The entity type ID. + * @param array $values + * An array of values for the entity. + * + * @return \Drupal\Core\Entity\EntityInterface + * The created entity. + */ + protected function createEntity(string $entity_type_id, array $values = []): EntityInterface { + $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); + + $entity = $storage->create($values); + $entity->save(); + + return $entity; + } + } diff --git a/core/modules/workspaces/workspaces.install b/core/modules/workspaces/workspaces.install index e4c0a6668b99..1a1eb1f98fa2 100644 --- a/core/modules/workspaces/workspaces.install +++ b/core/modules/workspaces/workspaces.install @@ -83,8 +83,16 @@ function workspaces_schema() { 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, + 'default' => 0, 'description' => 'The ID of the associated entity.', ], + 'target_entity_id_string' => [ + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The string ID of the associated entity.', + ], 'target_entity_revision_id' => [ 'type' => 'int', 'unsigned' => TRUE, @@ -92,10 +100,10 @@ function workspaces_schema() { 'description' => 'The revision ID of the associated entity.', ], ], + 'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id', 'target_entity_id_string'], 'indexes' => [ 'target_entity_revision_id' => ['target_entity_revision_id'], ], - 'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id'], ]; return $schema; @@ -107,3 +115,30 @@ function workspaces_schema() { function workspaces_update_last_removed(): int { return 8803; } + +/** + * Update workspace associations to support entity types with string IDs. + */ +function workspaces_update_11101(): void { + $schema = \Drupal::database()->schema(); + + $target_id_spec = [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The ID of the associated entity.', + ]; + $schema->changeField('workspace_association', 'target_entity_id', 'target_entity_id', $target_id_spec); + + $target_id_string_spec = [ + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The string ID of the associated entity.', + ]; + $schema->addField('workspace_association', 'target_entity_id_string', $target_id_string_spec, [ + 'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id', 'target_entity_id_string'], + ]); +} diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php index ccd930eaa8fb..b90023ec8820 100644 --- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php +++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php @@ -23,6 +23,13 @@ trait UpdatePathTestTrait { */ protected $checkFailedUpdates = TRUE; + /** + * Fail the test if there are entity field definition updates needed. + * + * @var bool + */ + protected $checkEntityFieldDefinitionUpdates = TRUE; + /** * Helper function to run pending database updates. * @@ -145,17 +152,19 @@ protected function runUpdates($update_url = NULL) { } // Ensure that the update hooks updated all entity schema. - $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates(); - if ($needs_updates) { - foreach (\Drupal::entityDefinitionUpdateManager()->getChangeSummary() as $entity_type_id => $summary) { - $entity_type_label = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getLabel(); - foreach ($summary as $message) { - $this->fail("$entity_type_label: $message"); + if ($this->checkEntityFieldDefinitionUpdates) { + $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates(); + if ($needs_updates) { + foreach (\Drupal::entityDefinitionUpdateManager()->getChangeSummary() as $entity_type_id => $summary) { + $entity_type_label = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getLabel(); + foreach ($summary as $message) { + $this->fail("$entity_type_label: $message"); + } } + // The above calls to `fail()` should prevent this from ever being + // called, but it is here in case something goes really wrong. + $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.'); } - // The above calls to `fail()` should prevent this from ever being - // called, but it is here in case something goes really wrong. - $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.'); } } } -- GitLab