Issue #3101671 by amateescu, webdrips, smithmilner, s_leu, smustgrave: Add mechanism to have workspaces skip processing entity types

(cherry picked from commit 940a069060)
merge-requests/6880/merge
catch 2024-03-04 10:47:09 +00:00
parent bd8960f835
commit 1fd6c15aef
26 changed files with 736 additions and 214 deletions

View File

@ -0,0 +1,24 @@
<?php
namespace Drupal\workspaces\Entity\Handler;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a custom workspace handler for block_content entities.
*
* @internal
*/
class BlockContentWorkspaceHandler extends DefaultWorkspaceHandler {
/**
* {@inheritdoc}
*/
public function isEntitySupported(EntityInterface $entity): bool {
// Only reusable blocks can be tracked individually. Non-reusable or inline
// blocks are tracked as part of the entity they are a composite of.
/** @var \Drupal\block_content\BlockContentInterface $entity */
return $entity->isReusable();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\workspaces\Entity\Handler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Common customizations for most entity types.
*
* @internal
*/
class DefaultWorkspaceHandler implements WorkspaceHandlerInterface, EntityHandlerInterface {
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static();
}
/**
* {@inheritdoc}
*/
public function isEntitySupported(EntityInterface $entity): bool {
return TRUE;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\workspaces\Entity\Handler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a handler for entity types that are ignored by workspaces.
*
* @internal
*/
class IgnoredWorkspaceHandler implements WorkspaceHandlerInterface, EntityHandlerInterface {
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static();
}
/**
* {@inheritdoc}
*/
public function isEntitySupported(EntityInterface $entity): bool {
return FALSE;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\workspaces\Entity\Handler;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines workspace operations that need to vary by entity type.
*
* @internal
*/
interface WorkspaceHandlerInterface {
/**
* Determines if an entity should be tracked in a workspace.
*
* At the general level, workspace support is determined for the entire entity
* type. If an entity type is supported, there may be further decisions each
* entity type can make to evaluate if a given entity is appropriate to be
* tracked in a workspace.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity we may be tracking.
*
* @return bool
* TRUE if this entity should be tracked in a workspace, FALSE otherwise.
*/
public function isEntitySupported(EntityInterface $entity): bool;
}

View File

@ -39,6 +39,7 @@ use Drupal\workspaces\WorkspaceInterface;
* "delete" = "\Drupal\workspaces\Form\WorkspaceDeleteForm",
* "activate" = "\Drupal\workspaces\Form\WorkspaceActivateForm",
* },
* "workspace" = "\Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler",
* },
* admin_permission = "administer workspaces",
* base_table = "workspace",

View File

@ -33,6 +33,13 @@ class EntityAccess implements ContainerInjectionInterface {
*/
protected $workspaceManager;
/**
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected $workspaceInfo;
/**
* Constructs a new EntityAccess instance.
*
@ -40,10 +47,13 @@ class EntityAccess implements ContainerInjectionInterface {
* The entity type manager service.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager service.
* @param \Drupal\workspaces\WorkspaceInformationInterface $workspace_information
* The workspace information service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceInformationInterface $workspace_information) {
$this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager;
$this->workspaceInfo = $workspace_information;
}
/**
@ -52,7 +62,8 @@ class EntityAccess implements ContainerInjectionInterface {
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('workspaces.manager')
$container->get('workspaces.manager'),
$container->get('workspaces.information')
);
}
@ -75,7 +86,7 @@ class EntityAccess implements ContainerInjectionInterface {
// Workspaces themselves are handled by their own access handler and we
// should not try to do any access checks for entity types that can not
// belong to a workspace.
if ($entity->getEntityTypeId() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity->getEntityType()) || !$this->workspaceManager->hasActiveWorkspace()) {
if (!$this->workspaceInfo->isEntitySupported($entity) || !$this->workspaceManager->hasActiveWorkspace()) {
return AccessResult::neutral();
}
@ -102,7 +113,7 @@ class EntityAccess implements ContainerInjectionInterface {
// should not try to do any access checks for entity types that can not
// belong to a workspace.
$entity_type = $this->entityTypeManager->getDefinition($context['entity_type_id']);
if ($entity_type->id() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity_type) || !$this->workspaceManager->hasActiveWorkspace()) {
if (!$this->workspaceInfo->isEntityTypeSupported($entity_type) || !$this->workspaceManager->hasActiveWorkspace()) {
return AccessResult::neutral();
}

View File

@ -4,7 +4,6 @@ namespace Drupal\workspaces;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\FormStateInterface;
@ -42,6 +41,13 @@ class EntityOperations implements ContainerInjectionInterface {
*/
protected $workspaceAssociation;
/**
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected $workspaceInfo;
/**
* Constructs a new EntityOperations instance.
*
@ -51,11 +57,14 @@ class EntityOperations implements ContainerInjectionInterface {
* The workspace manager service.
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service.
* @param \Drupal\workspaces\WorkspaceInformationInterface $workspace_information
* The workspace information service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceInformationInterface $workspace_information) {
$this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager;
$this->workspaceAssociation = $workspace_association;
$this->workspaceInfo = $workspace_information;
}
/**
@ -65,7 +74,8 @@ class EntityOperations implements ContainerInjectionInterface {
return new static(
$container->get('entity_type.manager'),
$container->get('workspaces.manager'),
$container->get('workspaces.association')
$container->get('workspaces.association'),
$container->get('workspaces.information')
);
}
@ -77,9 +87,8 @@ class EntityOperations implements ContainerInjectionInterface {
public function entityPreload(array $ids, $entity_type_id) {
$entities = [];
// Only run if the entity type can belong to a workspace and we are in a
// non-default workspace.
if (!$this->workspaceManager->shouldAlterOperations($this->entityTypeManager->getDefinition($entity_type_id))) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$this->workspaceInfo->isEntityTypeSupported($entity_type) || !$this->workspaceManager->hasActiveWorkspace()) {
return $entities;
}
@ -87,6 +96,11 @@ class EntityOperations implements ContainerInjectionInterface {
// current active workspace. If an entity has multiple revisions set for a
// workspace, only the one with the highest ID is returned.
if ($tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->workspaceManager->getActiveWorkspace()->id(), $entity_type_id, $ids)) {
// Bail out early if there are no tracked entities of this type.
if (!isset($tracked_entities[$entity_type_id])) {
return $entities;
}
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
@ -109,18 +123,13 @@ class EntityOperations implements ContainerInjectionInterface {
* @see hook_entity_presave()
*/
public function entityPresave(EntityInterface $entity) {
$entity_type = $entity->getEntityType();
// Only run if we are not dealing with an entity type provided by the
// Workspaces module, an internal entity type or if we are in a non-default
// workspace.
if ($this->shouldSkipPreOperations($entity_type)) {
if ($this->shouldSkipOperations($entity)) {
return;
}
// Disallow any change to an unsupported entity when we are not in the
// default workspace.
if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) {
if (!$this->workspaceInfo->isEntitySupported($entity)) {
throw new \RuntimeException('This entity can only be saved in the default workspace.');
}
@ -140,7 +149,7 @@ class EntityOperations implements ContainerInjectionInterface {
// Track the workspaces in which the new revision was saved.
if (!$entity->isSyncing()) {
$field_name = $entity_type->getRevisionMetadataKey('workspace');
$field_name = $entity->getEntityType()->getRevisionMetadataKey('workspace');
$entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id();
}
@ -167,10 +176,7 @@ class EntityOperations implements ContainerInjectionInterface {
* @see hook_entity_insert()
*/
public function entityInsert(EntityInterface $entity) {
/** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
// Only run if the entity type can belong to a workspace and we are in a
// non-default workspace.
if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) {
if ($this->shouldSkipOperations($entity) || !$this->workspaceInfo->isEntitySupported($entity)) {
return;
}
@ -202,9 +208,7 @@ class EntityOperations implements ContainerInjectionInterface {
* @see hook_entity_update()
*/
public function entityUpdate(EntityInterface $entity) {
// Only run if the entity type can belong to a workspace and we are in a
// non-default workspace.
if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) {
if ($this->shouldSkipOperations($entity) || !$this->workspaceInfo->isEntitySupported($entity)) {
return;
}
@ -224,18 +228,13 @@ class EntityOperations implements ContainerInjectionInterface {
* @see hook_entity_predelete()
*/
public function entityPredelete(EntityInterface $entity) {
$entity_type = $entity->getEntityType();
// Only run if we are not dealing with an entity type provided by the
// Workspaces module, an internal entity type or if we are in a non-default
// workspace.
if ($this->shouldSkipPreOperations($entity_type)) {
if ($this->shouldSkipOperations($entity)) {
return;
}
// Disallow any change to an unsupported entity when we are not in the
// default workspace.
if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) {
if (!$this->workspaceInfo->isEntitySupported($entity)) {
throw new \RuntimeException('This entity can only be deleted in the default workspace.');
}
}
@ -253,17 +252,21 @@ class EntityOperations implements ContainerInjectionInterface {
* @see hook_form_alter()
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\Core\Entity\RevisionableInterface $entity */
$entity = $form_state->getFormObject()->getEntity();
if (!$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) {
if (!$this->workspaceInfo->isEntitySupported($entity) && !$this->workspaceInfo->isEntityIgnored($entity)) {
return;
}
// For supported entity types, signal the fact that this form is safe to use
// in a non-default workspace.
// For supported and ignored entity types, signal the fact that this form is
// safe to use in a workspace.
// @see \Drupal\workspaces\FormOperations::validateForm()
$form_state->set('workspace_safe', TRUE);
// There is nothing more to do for ignored entity types.
if ($this->workspaceInfo->isEntityIgnored($entity)) {
return;
}
// Add an entity builder to the form which marks the edited entity object as
// a pending revision. This is needed so validation constraints like
// \Drupal\path\Plugin\Validation\Constraint\PathAliasConstraintValidator
@ -295,23 +298,18 @@ class EntityOperations implements ContainerInjectionInterface {
}
/**
* Determines whether we need to react on pre-save or pre-delete operations.
* Determines whether we need to react on entity operations.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to check.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* Returns TRUE if the pre-save or pre-delete entity operations should not
* be altered in the current request, FALSE otherwise.
* Returns TRUE if entity operations should not be altered, FALSE otherwise.
*/
protected function shouldSkipPreOperations(EntityTypeInterface $entity_type) {
// We should not react on pre-save and pre-delete entity operations if one
// of the following conditions are met:
// - the entity type is provided by the Workspaces module;
// - the entity type is internal, which means that it should not affect
// anything in the default (Live) workspace;
// - we are in the default workspace.
return $entity_type->getProvider() === 'workspaces' || $entity_type->isInternal() || !$this->workspaceManager->hasActiveWorkspace();
protected function shouldSkipOperations(EntityInterface $entity) {
// We should not react on entity operations when the entity is ignored or
// when we're not in a workspace context.
return $this->workspaceInfo->isEntityIgnored($entity) || !$this->workspaceManager->hasActiveWorkspace();
}
}

View File

@ -2,52 +2,11 @@
namespace Drupal\workspaces\EntityQuery;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory as BaseQueryFactory;
use Drupal\workspaces\WorkspaceManagerInterface;
/**
* Workspaces PostgreSQL-specific entity query implementation.
*
* @internal
*/
class PgsqlQueryFactory extends BaseQueryFactory {
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a PgsqlQueryFactory object.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection used by the entity query.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
*/
public function __construct(Connection $connection, WorkspaceManagerInterface $workspace_manager) {
$this->connection = $connection;
$this->workspaceManager = $workspace_manager;
$this->namespaces = QueryBase::getNamespaces($this);
}
/**
* {@inheritdoc}
*/
public function get(EntityTypeInterface $entity_type, $conjunction) {
$class = QueryBase::getClass($this->namespaces, 'Query');
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager);
}
/**
* {@inheritdoc}
*/
public function getAggregate(EntityTypeInterface $entity_type, $conjunction) {
$class = QueryBase::getClass($this->namespaces, 'QueryAggregate');
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager);
}
class PgsqlQueryFactory extends QueryFactory {
}

View File

@ -6,34 +6,22 @@ use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\Sql\QueryFactory as BaseQueryFactory;
use Drupal\workspaces\WorkspaceInformationInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
/**
* Workspaces-specific entity query implementation.
*
* @internal
*/
class QueryFactory extends BaseQueryFactory {
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a QueryFactory object.
*
* Initializes the list of namespaces used to locate query
* classes for different entity types.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection used by the entity query.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
*/
public function __construct(Connection $connection, WorkspaceManagerInterface $workspace_manager) {
public function __construct(
Connection $connection,
protected readonly WorkspaceManagerInterface $workspaceManager,
protected readonly WorkspaceInformationInterface $workspaceInfo
) {
$this->connection = $connection;
$this->workspaceManager = $workspace_manager;
$this->namespaces = QueryBase::getNamespaces($this);
}
@ -42,7 +30,7 @@ class QueryFactory extends BaseQueryFactory {
*/
public function get(EntityTypeInterface $entity_type, $conjunction) {
$class = QueryBase::getClass($this->namespaces, 'Query');
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager);
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager, $this->workspaceInfo);
}
/**
@ -50,7 +38,7 @@ class QueryFactory extends BaseQueryFactory {
*/
public function getAggregate(EntityTypeInterface $entity_type, $conjunction) {
$class = QueryBase::getClass($this->namespaces, 'QueryAggregate');
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager);
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager, $this->workspaceInfo);
}
}

View File

@ -4,10 +4,13 @@ namespace Drupal\workspaces\EntityQuery;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\workspaces\WorkspaceInformationInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
/**
* Provides workspaces-specific helpers for altering entity queries.
*
* @internal
*/
trait QueryTrait {
@ -18,6 +21,13 @@ trait QueryTrait {
*/
protected $workspaceManager;
/**
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected $workspaceInfo;
/**
* Constructs a Query object.
*
@ -32,11 +42,14 @@ trait QueryTrait {
* List of potential namespaces of the classes belonging to this query.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
* @param \Drupal\workspaces\WorkspaceInformationInterface $workspace_information
* The workspace information service.
*/
public function __construct(EntityTypeInterface $entity_type, $conjunction, Connection $connection, array $namespaces, WorkspaceManagerInterface $workspace_manager) {
public function __construct(EntityTypeInterface $entity_type, $conjunction, Connection $connection, array $namespaces, WorkspaceManagerInterface $workspace_manager, WorkspaceInformationInterface $workspace_information) {
parent::__construct($entity_type, $conjunction, $connection, $namespaces);
$this->workspaceManager = $workspace_manager;
$this->workspaceInfo = $workspace_information;
}
/**
@ -54,7 +67,7 @@ trait QueryTrait {
// Only alter the query if the active workspace is not the default one and
// the entity type is supported.
if ($this->workspaceManager->isEntityTypeSupported($this->entityType) && $this->workspaceManager->hasActiveWorkspace()) {
if ($this->workspaceInfo->isEntityTypeSupported($this->entityType) && $this->workspaceManager->hasActiveWorkspace()) {
$active_workspace = $this->workspaceManager->getActiveWorkspace();
$this->sqlQuery->addMetaData('active_workspace_id', $active_workspace->id());
$this->sqlQuery->addMetaData('simple_query', FALSE);

View File

@ -13,11 +13,11 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface;
class Tables extends BaseTables {
/**
* The workspace manager.
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected $workspaceManager;
protected $workspaceInfo;
/**
* Workspace association table array, key is base table name, value is alias.
@ -42,7 +42,7 @@ class Tables extends BaseTables {
public function __construct(SelectInterface $sql_query) {
parent::__construct($sql_query);
$this->workspaceManager = \Drupal::service('workspaces.manager');
$this->workspaceInfo = \Drupal::service('workspaces.information');
// The join between the first 'workspace_association' table and base table
// of the query is done in
@ -117,7 +117,7 @@ class Tables extends BaseTables {
$next_base_table_alias = parent::addNextBaseTable($entity_type, $table, $sql_column, $field_storage);
$active_workspace_id = $this->sqlQuery->getMetaData('active_workspace_id');
if ($active_workspace_id && $this->workspaceManager->isEntityTypeSupported($entity_type)) {
if ($active_workspace_id && $this->workspaceInfo->isEntityTypeSupported($entity_type)) {
$this->addWorkspaceAssociationJoin($entity_type->id(), $next_base_table_alias, $active_workspace_id);
}

View File

@ -3,10 +3,13 @@
namespace Drupal\workspaces;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workspaces\Entity\Handler\BlockContentWorkspaceHandler;
use Drupal\workspaces\Entity\Handler\DefaultWorkspaceHandler;
use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -19,31 +22,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class EntityTypeInfo implements ContainerInjectionInterface {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The workspace manager service.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a new EntityTypeInfo instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager;
public function __construct(
protected readonly WorkspaceInformationInterface $workspaceInfo
) {
}
/**
@ -51,13 +32,12 @@ class EntityTypeInfo implements ContainerInjectionInterface {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('workspaces.manager')
$container->get('workspaces.information')
);
}
/**
* Adds the "EntityWorkspaceConflict" constraint to eligible entity types.
* Adds workspace support info to eligible entity types.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
* An associative array of all entity type definitions, keyed by the entity
@ -67,15 +47,31 @@ class EntityTypeInfo implements ContainerInjectionInterface {
*/
public function entityTypeBuild(array &$entity_types) {
foreach ($entity_types as $entity_type) {
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
$entity_type->addConstraint('EntityWorkspaceConflict');
$entity_type->setRevisionMetadataKey('workspace', 'workspace');
if ($entity_type->hasHandlerClass('workspace')) {
continue;
}
// Revisionable and publishable entity types are always supported.
if ($entity_type->entityClassImplements(EntityPublishedInterface::class) && $entity_type->isRevisionable()) {
$entity_type->setHandlerClass('workspace', DefaultWorkspaceHandler::class);
// Support for custom blocks has to be determined on a per-entity
// basis.
if ($entity_type->id() === 'block_content') {
$entity_type->setHandlerClass('workspace', BlockContentWorkspaceHandler::class);
}
}
// Internal entity types are allowed to perform CRUD operations inside a
// workspace.
if ($entity_type->isInternal()) {
$entity_type->setHandlerClass('workspace', IgnoredWorkspaceHandler::class);
}
}
}
/**
* Removes the 'latest-version' link template provided by Content Moderation.
* Adds Workspace configuration to appropriate entity types.
*
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
* An array of entity types.
@ -84,6 +80,16 @@ class EntityTypeInfo implements ContainerInjectionInterface {
*/
public function entityTypeAlter(array &$entity_types) {
foreach ($entity_types as $entity_type) {
if (!$this->workspaceInfo->isEntityTypeSupported($entity_type)) {
continue;
}
// Workspace-support status has been declared in the "build" phase, now we
// can use that information and add additional configuration in the
// "alter" phase.
$entity_type->addConstraint('EntityWorkspaceConflict');
$entity_type->setRevisionMetadataKey('workspace', 'workspace');
// Non-default workspaces display the active revision on the canonical
// route of an entity, so the latest version route is no longer needed.
$link_templates = $entity_type->get('links');
@ -123,7 +129,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* @see hook_entity_base_field_info()
*/
public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
if ($this->workspaceInfo->isEntityTypeSupported($entity_type)) {
$field_name = $entity_type->getRevisionMetadataKey('workspace');
$fields[$field_name] = BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('Workspace'))

View File

@ -9,7 +9,7 @@ use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeListenerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workspaces\WorkspaceManagerInterface;
use Drupal\workspaces\WorkspaceInformationInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@ -35,11 +35,11 @@ class EntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscr
protected $entityLastInstalledSchemaRepository;
/**
* The workspace manager.
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected $workspaceManager;
protected $workspaceInfo;
/**
* Constructs a new EntitySchemaSubscriber.
@ -48,13 +48,13 @@ class EntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscr
* Definition update manager.
* @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository
* Last definitions.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
* @param \Drupal\workspaces\WorkspaceInformationInterface $workspace_information
* The workspace information service.
*/
public function __construct(EntityDefinitionUpdateManagerInterface $entityDefinitionUpdateManager, EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository, WorkspaceManagerInterface $workspace_manager) {
public function __construct(EntityDefinitionUpdateManagerInterface $entityDefinitionUpdateManager, EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository, WorkspaceInformationInterface $workspace_information) {
$this->entityDefinitionUpdateManager = $entityDefinitionUpdateManager;
$this->entityLastInstalledSchemaRepository = $entityLastInstalledSchemaRepository;
$this->workspaceManager = $workspace_manager;
$this->workspaceInfo = $workspace_information;
}
/**
@ -70,7 +70,7 @@ class EntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscr
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
// If the entity type is supported by Workspaces, add the revision metadata
// field.
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
if ($this->workspaceInfo->isEntityTypeSupported($entity_type)) {
$this->addRevisionMetadataField($entity_type);
}
}
@ -88,13 +88,13 @@ class EntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscr
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)) {
if ($this->workspaceInfo->isEntityTypeSupported($entity_type) && !$this->workspaceInfo->isEntityTypeSupported($original)) {
$this->addRevisionMetadataField($entity_type);
}
// 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)) {
if ($this->workspaceInfo->isEntityTypeSupported($original) && !$this->workspaceInfo->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);

View File

@ -4,6 +4,7 @@ namespace Drupal\workspaces\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceInformationInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
@ -28,12 +29,20 @@ class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintV
*/
protected $entityTypeManager;
/**
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected $workspaceInfo;
/**
* Creates a new EntityReferenceSupportedNewEntitiesConstraintValidator instance.
*/
public function __construct(WorkspaceManagerInterface $workspaceManager, EntityTypeManagerInterface $entityTypeManager) {
public function __construct(WorkspaceManagerInterface $workspaceManager, EntityTypeManagerInterface $entityTypeManager, WorkspaceInformationInterface $workspace_information) {
$this->workspaceManager = $workspaceManager;
$this->entityTypeManager = $entityTypeManager;
$this->workspaceInfo = $workspace_information;
}
/**
@ -42,7 +51,8 @@ class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintV
public static function create(ContainerInterface $container) {
return new static(
$container->get('workspaces.manager'),
$container->get('entity_type.manager')
$container->get('entity_type.manager'),
$container->get('workspaces.information')
);
}
@ -58,7 +68,7 @@ class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintV
$target_entity_type_id = $value->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
$target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
if ($value->hasNewEntity() && !$this->workspaceManager->isEntityTypeSupported($target_entity_type)) {
if ($value->hasNewEntity() && !$this->workspaceInfo->isEntityTypeSupported($target_entity_type)) {
$this->context->addViolation($constraint->message, ['%collection_label' => $target_entity_type->getCollectionLabel()]);
}
}

View File

@ -63,6 +63,13 @@ class ViewsQueryAlter implements ContainerInjectionInterface {
*/
protected $languageManager;
/**
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected WorkspaceInformationInterface $workspaceInfo;
/**
* An array of tables adjusted for workspace_association join.
*
@ -85,14 +92,17 @@ class ViewsQueryAlter implements ContainerInjectionInterface {
* The views join plugin manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\workspaces\WorkspaceInformationInterface $workspace_information
* The workspace information service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, WorkspaceManagerInterface $workspace_manager, ViewsData $views_data, ViewsHandlerManager $views_join_plugin_manager, LanguageManagerInterface $language_manager) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, WorkspaceManagerInterface $workspace_manager, ViewsData $views_data, ViewsHandlerManager $views_join_plugin_manager, LanguageManagerInterface $language_manager, WorkspaceInformationInterface $workspace_information) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->workspaceManager = $workspace_manager;
$this->viewsData = $views_data;
$this->viewsJoinPluginManager = $views_join_plugin_manager;
$this->languageManager = $language_manager;
$this->workspaceInfo = $workspace_information;
$this->adjustedTables = new \WeakMap();
}
@ -106,7 +116,8 @@ class ViewsQueryAlter implements ContainerInjectionInterface {
$container->get('workspaces.manager'),
$container->get('views.views_data'),
$container->get('plugin.manager.views.join'),
$container->get('language_manager')
$container->get('language_manager'),
$container->get('workspaces.information')
);
}
@ -140,7 +151,7 @@ class ViewsQueryAlter implements ContainerInjectionInterface {
$entity_type_definitions = $this->entityTypeManager->getDefinitions();
foreach ($entity_type_ids as $entity_type_id) {
if ($this->workspaceManager->isEntityTypeSupported($entity_type_definitions[$entity_type_id])) {
if ($this->workspaceInfo->isEntityTypeSupported($entity_type_definitions[$entity_type_id])) {
$this->alterQueryForEntityType($query, $entity_type_definitions[$entity_type_id]);
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace Drupal\workspaces;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler;
/**
* General service for workspace support information.
*/
class WorkspaceInformation implements WorkspaceInformationInterface {
/**
* An array of workspace-support statuses, keyed by entity type ID.
*
* @var bool[]
*/
protected array $supported = [];
/**
* An array of workspace-ignored statuses, keyed by entity type ID.
*
* @var bool[]
*/
protected array $ignored = [];
public function __construct(
protected readonly EntityTypeManagerInterface $entityTypeManager
) {}
/**
* {@inheritdoc}
*/
public function isEntitySupported(EntityInterface $entity): bool {
$entity_type = $entity->getEntityType();
if (!$this->isEntityTypeSupported($entity_type)) {
return FALSE;
}
$handler = $this->entityTypeManager->getHandler($entity_type->id(), 'workspace');
return $handler->isEntitySupported($entity);
}
/**
* {@inheritdoc}
*/
public function isEntityTypeSupported(EntityTypeInterface $entity_type): bool {
if (!isset($this->supported[$entity_type->id()])) {
if ($entity_type->hasHandlerClass('workspace')) {
$supported = !is_a($entity_type->getHandlerClass('workspace'), IgnoredWorkspaceHandler::class, TRUE);
}
else {
// Fallback for cases when entity type info hasn't been altered yet, for
// example when the Workspaces module is being installed.
$supported = $entity_type->entityClassImplements(EntityPublishedInterface::class) && $entity_type->isRevisionable();
}
$this->supported[$entity_type->id()] = $supported;
}
return $this->supported[$entity_type->id()];
}
/**
* {@inheritdoc}
*/
public function getSupportedEntityTypes(): array {
$entity_types = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($this->isEntityTypeSupported($entity_type)) {
$entity_types[$entity_type_id] = $entity_type;
}
}
return $entity_types;
}
/**
* {@inheritdoc}
*/
public function isEntityIgnored(EntityInterface $entity): bool {
$entity_type = $entity->getEntityType();
if ($this->isEntityTypeIgnored($entity_type)) {
return TRUE;
}
if ($entity_type->hasHandlerClass('workspace')) {
$handler = $this->entityTypeManager->getHandler($entity_type->id(), 'workspace');
return !$handler->isEntitySupported($entity);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function isEntityTypeIgnored(EntityTypeInterface $entity_type): bool {
if (!isset($this->ignored[$entity_type->id()])) {
$this->ignored[$entity_type->id()] = $entity_type->hasHandlerClass('workspace')
&& is_a($entity_type->getHandlerClass('workspace'), IgnoredWorkspaceHandler::class, TRUE);
}
return $this->ignored[$entity_type->id()];
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\workspaces;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an interface for workspace-support information.
*/
interface WorkspaceInformationInterface {
/**
* Determines whether an entity can belong to a workspace.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* TRUE if the entity can belong to a workspace, FALSE otherwise.
*/
public function isEntitySupported(EntityInterface $entity): bool;
/**
* Determines whether an entity type can belong to a workspace.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to check.
*
* @return bool
* TRUE if the entity type can belong to a workspace, FALSE otherwise.
*/
public function isEntityTypeSupported(EntityTypeInterface $entity_type): bool;
/**
* Returns an array of entity types that can belong to workspaces.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity type definition objects.
*/
public function getSupportedEntityTypes(): array;
/**
* Determines whether CRUD operations for an entity are allowed.
*
* CRUD operations for an ignored entity are allowed in a workspace, but their
* revisions are not tracked.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* TRUE if CRUD operations of an entity type can safely be done inside a
* workspace, without impacting the Live site, FALSE otherwise.
*/
public function isEntityIgnored(EntityInterface $entity): bool;
/**
* Determines whether CRUD operations for an entity type are allowed.
*
* CRUD operations for an ignored entity type are allowed in a workspace, but
* their revisions are not tracked.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to check.
*
* @return bool
* TRUE if CRUD operations of an entity type can safely be done inside a
* workspace, without impacting the Live site, FALSE otherwise.
*/
public function isEntityTypeIgnored(EntityTypeInterface $entity_type): bool;
}

View File

@ -4,7 +4,6 @@ namespace Drupal\workspaces;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
@ -21,15 +20,6 @@ class WorkspaceManager implements WorkspaceManagerInterface {
use StringTranslationTrait;
/**
* An array of which entity types are supported.
*
* @var string[]
*/
protected $supported = [
'workspace' => FALSE,
];
/**
* The request stack.
*
@ -86,6 +76,13 @@ class WorkspaceManager implements WorkspaceManagerInterface {
*/
protected $workspaceAssociation;
/**
* The workspace information service.
*
* @var \Drupal\workspaces\WorkspaceInformationInterface
*/
protected WorkspaceInformationInterface $workspaceInfo;
/**
* The workspace negotiator service IDs.
*
@ -119,10 +116,12 @@ class WorkspaceManager implements WorkspaceManagerInterface {
* The class resolver.
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service.
* @param array $negotiator_ids
* @param \Drupal\workspaces\WorkspaceInformationInterface|null $workspace_information
* The workspace information service.
* @param array|null $negotiator_ids
* The workspace negotiator service IDs.
*/
public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, WorkspaceAssociationInterface $workspace_association, array $negotiator_ids) {
public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, WorkspaceAssociationInterface $workspace_association, protected ?WorkspaceInformationInterface $workspace_information = NULL, array $negotiator_ids = NULL) {
$this->requestStack = $request_stack;
$this->entityTypeManager = $entity_type_manager;
$this->entityMemoryCache = $entity_memory_cache;
@ -131,33 +130,34 @@ class WorkspaceManager implements WorkspaceManagerInterface {
$this->logger = $logger;
$this->classResolver = $class_resolver;
$this->workspaceAssociation = $workspace_association;
$this->negotiatorIds = $negotiator_ids;
if (!$workspace_information instanceof WorkspaceInformationInterface) {
@trigger_error('Calling ' . __METHOD__ . '() without the $workspace_information argument is deprecated in drupal:10.3.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3324297', E_USER_DEPRECATED);
$this->workspaceInfo = \Drupal::service('workspaces.information');
// The negotiator IDs are always the last constructor argument.
$this->negotiatorIds = $workspace_information;
}
else {
$this->workspaceInfo = $workspace_information;
$this->negotiatorIds = $negotiator_ids;
}
}
/**
* {@inheritdoc}
*/
public function isEntityTypeSupported(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
if (!isset($this->supported[$entity_type_id])) {
// Only entity types which are revisionable and publishable can belong
// to a workspace.
$this->supported[$entity_type_id] = $entity_type->entityClassImplements(EntityPublishedInterface::class) && $entity_type->isRevisionable();
}
return $this->supported[$entity_type_id];
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\workspaces\WorkspaceInformation::isEntityTypeSupported instead. See https://www.drupal.org/node/3324297', E_USER_DEPRECATED);
return $this->workspaceInfo->isEntityTypeSupported($entity_type);
}
/**
* {@inheritdoc}
*/
public function getSupportedEntityTypes() {
$entity_types = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($this->isEntityTypeSupported($entity_type)) {
$entity_types[$entity_type_id] = $entity_type;
}
}
return $entity_types;
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\workspaces\WorkspaceInformation::getSupportedEntityTypes instead. See https://www.drupal.org/node/3324297', E_USER_DEPRECATED);
return $this->workspaceInfo->getSupportedEntityTypes();
}
/**
@ -254,7 +254,7 @@ class WorkspaceManager implements WorkspaceManagerInterface {
// Clear the static entity cache for the supported entity types.
$cache_tags_to_invalidate = array_map(function ($entity_type_id) {
return 'entity.memory_cache:' . $entity_type_id;
}, array_keys($this->getSupportedEntityTypes()));
}, array_keys($this->workspaceInfo->getSupportedEntityTypes()));
$this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
// Clear the static cache for path aliases. We can't inject the path alias
@ -297,7 +297,8 @@ class WorkspaceManager implements WorkspaceManagerInterface {
* {@inheritdoc}
*/
public function shouldAlterOperations(EntityTypeInterface $entity_type) {
return $this->isEntityTypeSupported($entity_type) && $this->hasActiveWorkspace();
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3324297', E_USER_DEPRECATED);
return $this->workspaceInfo->isEntityTypeSupported($entity_type) && $this->hasActiveWorkspace();
}
/**
@ -318,7 +319,7 @@ class WorkspaceManager implements WorkspaceManagerInterface {
$workspace_id = reset($deleted_workspace_ids);
$all_associated_revisions = [];
foreach (array_keys($this->getSupportedEntityTypes()) as $entity_type_id) {
foreach (array_keys($this->workspaceInfo->getSupportedEntityTypes()) as $entity_type_id) {
$all_associated_revisions[$entity_type_id] = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id);
}
$all_associated_revisions = array_filter($all_associated_revisions);
@ -359,7 +360,7 @@ class WorkspaceManager implements WorkspaceManagerInterface {
// request a fresh list of tracked entities. If it is empty, we can go ahead
// and remove the deleted workspace ID entry from state.
$has_associated_revisions = FALSE;
foreach (array_keys($this->getSupportedEntityTypes()) as $entity_type_id) {
foreach (array_keys($this->workspaceInfo->getSupportedEntityTypes()) as $entity_type_id) {
if (!empty($this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id))) {
$has_associated_revisions = TRUE;
break;

View File

@ -17,6 +17,11 @@ interface WorkspaceManagerInterface {
*
* @return bool
* TRUE if the entity type can belong to a workspace, FALSE otherwise.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\workspaces\WorkspaceInformation::isEntityTypeSupported instead.
*
* @see https://www.drupal.org/node/3324297
*/
public function isEntityTypeSupported(EntityTypeInterface $entity_type);
@ -25,6 +30,11 @@ interface WorkspaceManagerInterface {
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* The entity types what can belong to workspaces.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\workspaces\WorkspaceInformation::getSupportedEntityTypes instead.
*
* @see https://www.drupal.org/node/3324297
*/
public function getSupportedEntityTypes();
@ -97,6 +107,11 @@ interface WorkspaceManagerInterface {
* @return bool
* TRUE if the entity operations or queries should be altered in the current
* request, FALSE otherwise.
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no
* replacement.
*
* @see https://www.drupal.org/node/3324297
*/
public function shouldAlterOperations(EntityTypeInterface $entity_type);

View File

@ -0,0 +1,20 @@
<?php
namespace Drupal\workspaces_test;
use Drupal\Core\Entity\EntityInterface;
use Drupal\workspaces\Entity\Handler\DefaultWorkspaceHandler;
/**
* Provides a custom workspace handler for testing purposes.
*/
class EntityTestRevPubWorkspaceHandler extends DefaultWorkspaceHandler {
/**
* {@inheritdoc}
*/
public function isEntitySupported(EntityInterface $entity): bool {
return $entity->bundle() !== 'ignored_bundle';
}
}

View File

@ -0,0 +1,7 @@
name: 'Workspace Test'
type: module
description: 'Provides supporting code for testing workspaces.'
package: Testing
version: VERSION
dependencies:
- drupal:workspaces

View File

@ -0,0 +1,178 @@
<?php
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces_test\EntityTestRevPubWorkspaceHandler;
/**
* Tests the workspace information service.
*
* @coversDefaultClass \Drupal\workspaces\WorkspaceInformation
*
* @group workspaces
*/
class WorkspaceInformationTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The workspace information service.
*
* @var \Drupal\wse\Core\WorkspaceInformationInterface
*/
protected $workspaceInformation;
/**
* The state store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'path_alias',
'user',
'workspaces',
'workspaces_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->workspaceInformation = \Drupal::service('workspaces.information');
$this->state = \Drupal::state();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_rev');
$this->installEntitySchema('entity_test_revpub');
$this->installEntitySchema('workspace');
$this->installSchema('workspaces', ['workspace_association']);
// Create a new workspace and activate it.
Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
$this->switchToWorkspace('stage');
}
/**
* Tests fully supported entity types.
*/
public function testSupportedEntityTypes() {
// Check a supported entity type.
$entity = $this->entityTypeManager->getStorage('entity_test_revpub')->create();
$this->assertTrue($this->workspaceInformation->isEntitySupported($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
$this->assertFalse($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
// Check that supported entity types are tracked in a workspace. This entity
// is published by default, so the second revision will be tracked.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => [2]], 'entity_test_revpub');
}
/**
* Tests an entity type with a custom workspace handler.
*/
public function testCustomSupportEntityTypes() {
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_revpub');
$entity_type->setHandlerClass('workspace', EntityTestRevPubWorkspaceHandler::class);
$this->state->set('entity_test_revpub.entity_type', $entity_type);
$this->entityTypeManager->clearCachedDefinitions();
$entity = $this->entityTypeManager->getStorage('entity_test_revpub')->create([
'type' => 'supported_bundle',
]);
$this->assertTrue($this->workspaceInformation->isEntitySupported($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
$this->assertFalse($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
// Check that supported entity types are tracked in a workspace. This entity
// is published by default, so the second revision will be tracked.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => [2]], 'entity_test_revpub');
$entity = $this->entityTypeManager->getStorage('entity_test_revpub')->create([
'type' => 'ignored_bundle',
]);
$this->assertFalse($this->workspaceInformation->isEntitySupported($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
$this->assertTrue($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
// Check that an ignored entity can be saved, but won't be tracked.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => [2]], 'entity_test_revpub');
}
/**
* Tests ignored entity types.
*/
public function testIgnoredEntityTypes() {
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_rev');
$entity_type->setHandlerClass('workspace', IgnoredWorkspaceHandler::class);
$this->state->set('entity_test_rev.entity_type', $entity_type);
$this->entityTypeManager->clearCachedDefinitions();
// Check an ignored entity type. CRUD operations for an ignored entity type
// are allowed in a workspace, but their revisions are not tracked.
$entity = $this->entityTypeManager->getStorage('entity_test_rev')->create();
$this->assertTrue($this->workspaceInformation->isEntityIgnored($entity));
$this->assertTrue($this->workspaceInformation->isEntityTypeIgnored($entity->getEntityType()));
$this->assertFalse($this->workspaceInformation->isEntitySupported($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeSupported($entity->getEntityType()));
// Check that ignored entity types are not tracked in a workspace.
$entity->save();
$this->assertWorkspaceAssociation(['stage' => []], 'entity_test_rev');
}
/**
* Tests unsupported entity types.
*/
public function testUnsupportedEntityTypes() {
// Check an unsupported entity type.
$entity_test = $this->entityTypeManager->getDefinition('entity_test');
$this->assertFalse($entity_test->hasHandlerClass('workspace'));
$entity = $this->entityTypeManager->getStorage('entity_test')->create();
$this->assertFalse($this->workspaceInformation->isEntitySupported($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeSupported($entity_test));
$this->assertFalse($this->workspaceInformation->isEntityIgnored($entity));
$this->assertFalse($this->workspaceInformation->isEntityTypeIgnored($entity_test));
// Check that unsupported entity types can not be saved in a workspace.
$this->expectException(EntityStorageException::class);
$this->expectExceptionMessage('This entity can only be saved in the default workspace.');
$entity->save();
}
}

View File

@ -39,7 +39,7 @@ trait WorkspaceTestTrait {
// Install the entity schema for supported entity types to ensure that the
// 'workspace' revision metadata field gets created.
foreach (array_keys($this->workspaceManager->getSupportedEntityTypes()) as $entity_type_id) {
foreach (array_keys(\Drupal::service('workspaces.information')->getSupportedEntityTypes()) as $entity_type_id) {
$this->installEntitySchema($entity_type_id);
}

View File

@ -35,11 +35,11 @@ function workspaces_module_preinstall($module) {
return;
}
/** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */
$workspace_manager = \Drupal::service('workspaces.manager');
/** @var \Drupal\workspaces\WorkspaceInformationInterface $workspace_info */
$workspace_info = \Drupal::service('workspaces.information');
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) {
if ($workspace_manager->isEntityTypeSupported($entity_type)) {
if ($workspace_info->isEntityTypeSupported($entity_type)) {
$entity_type->setRevisionMetadataKey('workspace', 'workspace');
$entity_definition_update_manager->updateEntityType($entity_type);
}

View File

@ -148,7 +148,7 @@ function workspaces_entity_predelete(EntityInterface $entity) {
* Implements hook_entity_delete().
*/
function workspaces_entity_delete(EntityInterface $entity) {
if (\Drupal::service('workspaces.manager')->isEntityTypeSupported($entity->getEntityType())) {
if (\Drupal::service('workspaces.information')->isEntityTypeSupported($entity->getEntityType())) {
\Drupal::service('workspaces.association')
->deleteAssociations(NULL, $entity->getEntityTypeId(), [$entity->id()]);
}
@ -158,7 +158,7 @@ function workspaces_entity_delete(EntityInterface $entity) {
* Implements hook_entity_revision_delete().
*/
function workspaces_entity_revision_delete(EntityInterface $entity) {
if (\Drupal::service('workspaces.manager')->isEntityTypeSupported($entity->getEntityType())) {
if (\Drupal::service('workspaces.information')->isEntityTypeSupported($entity->getEntityType())) {
\Drupal::service('workspaces.association')
->deleteAssociations(NULL, $entity->getEntityTypeId(), [$entity->id()], [$entity->getRevisionId()]);
}

View File

@ -3,10 +3,14 @@ services:
autoconfigure: true
workspaces.manager:
class: Drupal\workspaces\WorkspaceManager
arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver', '@workspaces.association']
arguments: ['@request_stack', '@entity_type.manager', '@entity.memory_cache', '@current_user', '@state', '@logger.channel.workspaces', '@class_resolver', '@workspaces.association', '@workspaces.information']
tags:
- { name: service_id_collector, tag: workspace_negotiator }
Drupal\workspaces\WorkspaceManagerInterface: '@workspaces.manager'
workspaces.information:
class: Drupal\workspaces\WorkspaceInformation
arguments: [ '@entity_type.manager' ]
Drupal\workspaces\WorkspaceInformationInterface: '@workspaces.information'
workspaces.operation_factory:
class: Drupal\workspaces\WorkspaceOperationFactory
arguments: ['@entity_type.manager', '@database', '@workspaces.manager', '@workspaces.association', '@cache_tags.invalidator', '@event_dispatcher', '@logger.channel.workspaces']
@ -41,7 +45,7 @@ services:
workspaces.entity_schema_listener:
class: Drupal\workspaces\EventSubscriber\EntitySchemaSubscriber
arguments: ['@entity.definition_update_manager', '@entity.last_installed_schema.repository', '@workspaces.manager']
arguments: ['@entity.definition_update_manager', '@entity.last_installed_schema.repository', '@workspaces.information']
workspaces.workspace_subscriber:
class: Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber
arguments: ['@path_alias.manager', '@path.current', '@router.route_provider', '@workspaces.manager']
@ -58,7 +62,7 @@ services:
workspaces.entity.query.sql:
decorates: entity.query.sql
class: Drupal\workspaces\EntityQuery\QueryFactory
arguments: ['@database', '@workspaces.manager']
arguments: ['@database', '@workspaces.manager', '@workspaces.information']
public: false
decoration_priority: 50
tags:
@ -66,7 +70,7 @@ services:
pgsql.workspaces.entity.query.sql:
decorates: pgsql.entity.query.sql
class: Drupal\workspaces\EntityQuery\PgsqlQueryFactory
arguments: ['@database', '@workspaces.manager']
arguments: ['@database', '@workspaces.manager', '@workspaces.information']
public: false
decoration_priority: 50