Loading core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraint.php +1 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,6 @@ class EntityWorkspaceConflictConstraint extends SymfonyConstraint { * * @var string */ public $message = 'The content is being edited in the %label workspace. As a result, your changes cannot be saved.'; public $message = 'The content is being edited in the @label workspace. As a result, your changes cannot be saved.'; } core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php +15 −62 Original line number Diff line number Diff line Loading @@ -6,62 +6,22 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\workspaces\WorkspaceManagerInterface; use Drupal\workspaces\WorkspaceRepositoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; /** * Validates the EntityWorkspaceConflict constraint. */ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * The workspace manager service. * * @var \Drupal\workspaces\WorkspaceManagerInterface */ protected $workspaceManager; /** * The workspace association service. * * @var \Drupal\workspaces\WorkspaceAssociationInterface * @internal */ protected $workspaceAssociation; class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** * The workspace repository service. * * @var \Drupal\workspaces\WorkspaceRepositoryInterface */ protected $workspaceRepository; /** * Constructs an EntityUntranslatableFieldsConstraintValidator object. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * 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. * @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository * The Workspace repository service. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) { $this->entityTypeManager = $entity_type_manager; $this->workspaceManager = $workspace_manager; $this->workspaceAssociation = $workspace_association; $this->workspaceRepository = $workspace_repository; } public function __construct( protected readonly EntityTypeManagerInterface $entityTypeManager, protected readonly WorkspaceManagerInterface $workspaceManager, protected readonly WorkspaceAssociationInterface $workspaceAssociation, ) {} /** * {@inheritdoc} Loading @@ -71,7 +31,6 @@ public static function create(ContainerInterface $container) { $container->get('entity_type.manager'), $container->get('workspaces.manager'), $container->get('workspaces.association'), $container->get('workspaces.repository') ); } Loading @@ -83,22 +42,16 @@ public function validate($entity, Constraint $constraint): void { if (isset($entity) && !$entity->isNew()) { $active_workspace = $this->workspaceManager->getActiveWorkspace(); // Get the latest revision of the entity in order to check if it's being // edited in a different workspace. $latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) { /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); return $storage->loadRevision($storage->getLatestRevisionId($entity->id())); }); // If the latest revision of the entity is tracked in a workspace, it can // only be edited in that workspace or one of its descendants. if ($latest_revision_workspace = $latest_revision->workspace->entity) { $descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($latest_revision_workspace->id()); // If the entity is tracked in a workspace, it can only be edited in // that workspace or one of its descendants. if ($tracking_workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity, TRUE)) { if (!$active_workspace || !in_array($active_workspace->id(), $tracking_workspace_ids, TRUE)) { $first_tracking_workspace_id = reset($tracking_workspace_ids); $workspace = $this->entityTypeManager->getStorage('workspace') ->load($first_tracking_workspace_id); if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) { $this->context->buildViolation($constraint->message) ->setParameter('%label', $latest_revision_workspace->label()) ->setParameter('@label', $workspace->label()) ->addViolation(); } } Loading core/modules/workspaces/src/WorkspaceAssociation.php +26 −6 Original line number Diff line number Diff line Loading @@ -304,13 +304,33 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti /** * {@inheritdoc} */ public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) { $query = $this->database->select(static::TABLE) ->fields(static::TABLE, ['workspace']) ->condition('target_entity_type_id', $entity->getEntityTypeId()) ->condition('target_entity_id', $entity->id()); public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) { $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()); // 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()); $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]'); } $result = $query->execute()->fetchCol(); // Return early if the entity is not tracked in any workspace. if (empty($result)) { return []; } return $query->execute()->fetchCol(); // Return workspace IDs sorted in tree order. $tree = $this->workspaceRepository->loadTree(); return array_keys(array_intersect_key($tree, array_flip($result))); } /** Loading core/modules/workspaces/src/WorkspaceAssociationInterface.php +4 −1 Original line number Diff line number Diff line Loading @@ -96,12 +96,15 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti * * @param \Drupal\Core\Entity\RevisionableInterface $entity * An entity object. * @param bool $latest_revision * (optional) Whether to return only the workspaces in which the latest * revision of the entity is tracked. Defaults to FALSE. * * @return string[] * An array of workspace IDs where the given entity is tracked, or an empty * array if it is not tracked anywhere. */ public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity); public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE); /** * Triggers clean-up operations after publishing a workspace. Loading core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php 0 → 100644 +148 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\workspaces\Kernel; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\entity_test\Entity\EntityTestMulRevPub; use Drupal\KernelTests\KernelTestBase; use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\workspaces\Entity\Workspace; /** * @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraintValidator * @group workspaces */ class EntityWorkspaceConflictConstraintValidatorTest extends KernelTestBase { use UserCreationTrait; use WorkspaceTestTrait; /** * {@inheritdoc} */ protected static $modules = [ 'entity_test', 'path_alias', 'system', 'user', 'workspaces', ]; /** * The entity type manager. */ protected EntityTypeManagerInterface $entityTypeManager; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->entityTypeManager = \Drupal::entityTypeManager(); $this->installSchema('workspaces', ['workspace_association']); $this->installEntitySchema('entity_test_mulrevpub'); $this->installEntitySchema('workspace'); $this->installEntitySchema('user'); $this->createUser(); } /** * @covers ::validate */ public function testNewEntitiesAllowedInDefaultWorkspace(): void { // Create two top-level workspaces and a second-level one. $stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']); $stage->save(); $dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']); $dev->save(); $other = Workspace::create(['id' => 'other', 'label' => 'Other']); $other->save(); // Create an entity in Live, and check that the validation is skipped. $entity = EntityTestMulRevPub::create(); $this->assertCount(0, $entity->validate()); $entity->save(); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); // Edit the entity in Stage. $this->switchToWorkspace('stage'); $entity->save(); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); $expected_message = 'The content is being edited in the Stage workspace. As a result, your changes cannot be saved.'; // Check that the entity can no longer be edited in Live. $this->switchToLive(); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in another top-level // workspace. $this->switchToWorkspace('other'); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can still be edited in a sub-workspace of Stage. $this->switchToWorkspace('dev'); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); // Edit the entity in Dev. $this->switchToWorkspace('dev'); $entity->save(); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); $expected_message = 'The content is being edited in the Dev workspace. As a result, your changes cannot be saved.'; // Check that the entity can no longer be edited in Live. $this->switchToLive(); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in the parent workspace. $this->switchToWorkspace('stage'); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in another top-level // workspace. $this->switchToWorkspace('other'); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); } /** * Reloads the given entity from the storage and returns it. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be reloaded. * * @return \Drupal\Core\Entity\EntityInterface * The reloaded entity. */ protected function reloadEntity(EntityInterface $entity): EntityInterface { $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); $storage->resetCache([$entity->id()]); return $storage->load($entity->id()); } } Loading
core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraint.php +1 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,6 @@ class EntityWorkspaceConflictConstraint extends SymfonyConstraint { * * @var string */ public $message = 'The content is being edited in the %label workspace. As a result, your changes cannot be saved.'; public $message = 'The content is being edited in the @label workspace. As a result, your changes cannot be saved.'; }
core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php +15 −62 Original line number Diff line number Diff line Loading @@ -6,62 +6,22 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\workspaces\WorkspaceManagerInterface; use Drupal\workspaces\WorkspaceRepositoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; /** * Validates the EntityWorkspaceConflict constraint. */ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * The workspace manager service. * * @var \Drupal\workspaces\WorkspaceManagerInterface */ protected $workspaceManager; /** * The workspace association service. * * @var \Drupal\workspaces\WorkspaceAssociationInterface * @internal */ protected $workspaceAssociation; class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** * The workspace repository service. * * @var \Drupal\workspaces\WorkspaceRepositoryInterface */ protected $workspaceRepository; /** * Constructs an EntityUntranslatableFieldsConstraintValidator object. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * 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. * @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository * The Workspace repository service. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) { $this->entityTypeManager = $entity_type_manager; $this->workspaceManager = $workspace_manager; $this->workspaceAssociation = $workspace_association; $this->workspaceRepository = $workspace_repository; } public function __construct( protected readonly EntityTypeManagerInterface $entityTypeManager, protected readonly WorkspaceManagerInterface $workspaceManager, protected readonly WorkspaceAssociationInterface $workspaceAssociation, ) {} /** * {@inheritdoc} Loading @@ -71,7 +31,6 @@ public static function create(ContainerInterface $container) { $container->get('entity_type.manager'), $container->get('workspaces.manager'), $container->get('workspaces.association'), $container->get('workspaces.repository') ); } Loading @@ -83,22 +42,16 @@ public function validate($entity, Constraint $constraint): void { if (isset($entity) && !$entity->isNew()) { $active_workspace = $this->workspaceManager->getActiveWorkspace(); // Get the latest revision of the entity in order to check if it's being // edited in a different workspace. $latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) { /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); return $storage->loadRevision($storage->getLatestRevisionId($entity->id())); }); // If the latest revision of the entity is tracked in a workspace, it can // only be edited in that workspace or one of its descendants. if ($latest_revision_workspace = $latest_revision->workspace->entity) { $descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($latest_revision_workspace->id()); // If the entity is tracked in a workspace, it can only be edited in // that workspace or one of its descendants. if ($tracking_workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity, TRUE)) { if (!$active_workspace || !in_array($active_workspace->id(), $tracking_workspace_ids, TRUE)) { $first_tracking_workspace_id = reset($tracking_workspace_ids); $workspace = $this->entityTypeManager->getStorage('workspace') ->load($first_tracking_workspace_id); if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) { $this->context->buildViolation($constraint->message) ->setParameter('%label', $latest_revision_workspace->label()) ->setParameter('@label', $workspace->label()) ->addViolation(); } } Loading
core/modules/workspaces/src/WorkspaceAssociation.php +26 −6 Original line number Diff line number Diff line Loading @@ -304,13 +304,33 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti /** * {@inheritdoc} */ public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) { $query = $this->database->select(static::TABLE) ->fields(static::TABLE, ['workspace']) ->condition('target_entity_type_id', $entity->getEntityTypeId()) ->condition('target_entity_id', $entity->id()); public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) { $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()); // 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()); $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]'); } $result = $query->execute()->fetchCol(); // Return early if the entity is not tracked in any workspace. if (empty($result)) { return []; } return $query->execute()->fetchCol(); // Return workspace IDs sorted in tree order. $tree = $this->workspaceRepository->loadTree(); return array_keys(array_intersect_key($tree, array_flip($result))); } /** Loading
core/modules/workspaces/src/WorkspaceAssociationInterface.php +4 −1 Original line number Diff line number Diff line Loading @@ -96,12 +96,15 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti * * @param \Drupal\Core\Entity\RevisionableInterface $entity * An entity object. * @param bool $latest_revision * (optional) Whether to return only the workspaces in which the latest * revision of the entity is tracked. Defaults to FALSE. * * @return string[] * An array of workspace IDs where the given entity is tracked, or an empty * array if it is not tracked anywhere. */ public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity); public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE); /** * Triggers clean-up operations after publishing a workspace. Loading
core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php 0 → 100644 +148 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\workspaces\Kernel; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\entity_test\Entity\EntityTestMulRevPub; use Drupal\KernelTests\KernelTestBase; use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\workspaces\Entity\Workspace; /** * @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraintValidator * @group workspaces */ class EntityWorkspaceConflictConstraintValidatorTest extends KernelTestBase { use UserCreationTrait; use WorkspaceTestTrait; /** * {@inheritdoc} */ protected static $modules = [ 'entity_test', 'path_alias', 'system', 'user', 'workspaces', ]; /** * The entity type manager. */ protected EntityTypeManagerInterface $entityTypeManager; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); $this->entityTypeManager = \Drupal::entityTypeManager(); $this->installSchema('workspaces', ['workspace_association']); $this->installEntitySchema('entity_test_mulrevpub'); $this->installEntitySchema('workspace'); $this->installEntitySchema('user'); $this->createUser(); } /** * @covers ::validate */ public function testNewEntitiesAllowedInDefaultWorkspace(): void { // Create two top-level workspaces and a second-level one. $stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']); $stage->save(); $dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']); $dev->save(); $other = Workspace::create(['id' => 'other', 'label' => 'Other']); $other->save(); // Create an entity in Live, and check that the validation is skipped. $entity = EntityTestMulRevPub::create(); $this->assertCount(0, $entity->validate()); $entity->save(); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); // Edit the entity in Stage. $this->switchToWorkspace('stage'); $entity->save(); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); $expected_message = 'The content is being edited in the Stage workspace. As a result, your changes cannot be saved.'; // Check that the entity can no longer be edited in Live. $this->switchToLive(); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in another top-level // workspace. $this->switchToWorkspace('other'); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can still be edited in a sub-workspace of Stage. $this->switchToWorkspace('dev'); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); // Edit the entity in Dev. $this->switchToWorkspace('dev'); $entity->save(); $entity = $this->reloadEntity($entity); $this->assertCount(0, $entity->validate()); $expected_message = 'The content is being edited in the Dev workspace. As a result, your changes cannot be saved.'; // Check that the entity can no longer be edited in Live. $this->switchToLive(); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in the parent workspace. $this->switchToWorkspace('stage'); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); // Check that the entity can no longer be edited in another top-level // workspace. $this->switchToWorkspace('other'); $entity = $this->reloadEntity($entity); $violations = $entity->validate(); $this->assertCount(1, $violations); $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); } /** * Reloads the given entity from the storage and returns it. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be reloaded. * * @return \Drupal\Core\Entity\EntityInterface * The reloaded entity. */ protected function reloadEntity(EntityInterface $entity): EntityInterface { $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); $storage->resetCache([$entity->id()]); return $storage->load($entity->id()); } }