diff --git a/src/Entity/ContentWorkspace.php b/src/Entity/WorkspaceAssociation.php similarity index 80% rename from src/Entity/ContentWorkspace.php rename to src/Entity/WorkspaceAssociation.php index 20203cd..db38755 100644 --- a/src/Entity/ContentWorkspace.php +++ b/src/Entity/WorkspaceAssociation.php @@ -8,19 +8,19 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\StringTranslation\TranslatableMarkup; /** - * Defines the Content workspace entity. + * Defines the Workspace association entity. * * @ContentEntityType( - * id = "content_workspace", - * label = @Translation("Content workspace"), - * label_singular = @Translation("content workspace"), - * label_plural = @Translation("content workspaces"), + * id = "workspace_association", + * label = @Translation("Workspace association"), + * label_singular = @Translation("workspace association"), + * label_plural = @Translation("workspace associations"), * label_count = @PluralTranslation( - * singular = "@count content workspace", - * plural = "@count content workspaces" + * singular = "@count workspace association", + * plural = "@count workspace associations" * ), - * base_table = "content_workspace", - * revision_table = "content_workspace_revision", + * base_table = "workspace_association", + * revision_table = "workspace_association_revision", * entity_keys = { * "id" = "id", * "revision" = "revision_id", @@ -32,7 +32,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; * This entity is marked internal because it should not be used directly to * alter the workspace an entity belongs to. */ -class ContentWorkspace extends ContentEntityBase { +class WorkspaceAssociation extends ContentEntityBase { /** * {@inheritdoc} diff --git a/src/EntityAccess.php b/src/EntityAccess.php index eb4a435..8e2f561 100644 --- a/src/EntityAccess.php +++ b/src/EntityAccess.php @@ -12,6 +12,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Service wrapper for hooks relating to entity access control. + * + * @internal */ class EntityAccess implements ContainerInjectionInterface { diff --git a/src/EntityOperations.php b/src/EntityOperations.php new file mode 100644 index 0000000..9f80b1c --- /dev/null +++ b/src/EntityOperations.php @@ -0,0 +1,251 @@ +entityTypeManager = $entity_type_manager; + $this->workspaceManager = $workspace_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('workspace.manager') + ); + } + + /** + * Acts on entities when loaded. + * + * @see hook_entity_load() + */ + public function entityLoad(array &$entities, $entity_type_id) { + // Only run if the entity type can belong to a workspace and we are in a + // non-default workspace. + if (!$this->workspaceManager->entityTypeCanBelongToWorkspaces($this->entityTypeManager->getDefinition($entity_type_id)) + || (($active_workspace = $this->workspaceManager->getActiveWorkspace()) && $active_workspace->isDefaultWorkspace())) { + return; + } + + // Get a list of revision IDs for entities that have a revision set for the + // current active workspace. If an entity has multiple revisions set for a + // workspace, only the one with the highest ID is returned. + $entity_ids = array_keys($entities); + $max_revision_id = 'max_content_entity_revision_id'; + $results = $this->entityTypeManager + ->getStorage('workspace_association') + ->getAggregateQuery() + ->allRevisions() + ->aggregate('content_entity_revision_id', 'MAX', NULL, $max_revision_id) + ->groupBy('content_entity_id') + ->condition('content_entity_type_id', $entity_type_id) + ->condition('content_entity_id', $entity_ids, 'IN') + ->condition('workspace', $active_workspace->id(), '=') + ->execute(); + + // Since hook_entity_load() is called on both regular entity load as well as + // entity revision load, we need to prevent infinite recursion by checking + // whether the default revisions were already swapped with the workspace + // revision. + // @todo This recursion protection should be removed when + // https://www.drupal.org/project/drupal/issues/2928888 is resolved. + if ($results) { + foreach ($results as $key => $result) { + if ($entities[$result['content_entity_id']]->getRevisionId() == $result[$max_revision_id]) { + unset($results[$key]); + } + } + } + + if ($results) { + /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage($entity_type_id); + + // Swap out every entity which has a revision set for the current active + // workspace. + $swap_revision_ids = array_column($results, $max_revision_id); + foreach ($storage->loadMultipleRevisions($swap_revision_ids) as $revision) { + $entities[$revision->id()] = $revision; + } + } + } + + /** + * Acts on an entity before it is created or updated. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being saved. + * + * @see hook_entity_presave() + */ + public function entityPresave(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->entityTypeCanBelongToWorkspaces($entity->getEntityType()) + || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { + return; + } + + // Force a new revision if the entity is not replicating. + if (!$entity->isNew() && !isset($entity->_isReplicating)) { + $entity->setNewRevision(TRUE); + + // All entities in the non-default workspace are pending revisions, + // regardless of their publishing status. This means that when creating + // a published pending revision in a non-default workspace it will also be + // a published pending revision in the default workspace, however, it will + // become the default revision only when it is replicated to the default + // workspace. + $entity->isDefaultRevision(FALSE); + } + + // When a new published entity is inserted in a non-default workspace, we + // actually want two revisions to be saved: + // - An unpublished default revision in the default ('live') workspace. + // - A published pending revision in the current workspace. + if ($entity->isNew() && $entity->isPublished()) { + // Keep track of the publishing status for workspace_entity_insert() and + // unpublish the default revision. + $entity->_initialPublished = TRUE; + $entity->setUnpublished(); + } + } + + /** + * Responds to the creation of a new entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that was just saved. + * + * @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->entityTypeCanBelongToWorkspaces($entity->getEntityType()) + || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { + return; + } + + // Handle the case when a new published entity was created in a non-default + // workspace and create a published pending revision for it. + if (isset($entity->_initialPublished)) { + // Operate on a clone to avoid changing the entity prior to subsequent + // hook_entity_insert() implementations. + $pending_revision = clone $entity; + $pending_revision->setPublished(); + $pending_revision->isDefaultRevision(FALSE); + $pending_revision->save(); + } + else { + $this->trackEntity($entity); + } + } + + /** + * Responds to updates to an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that was just saved. + * + * @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->entityTypeCanBelongToWorkspaces($entity->getEntityType()) + || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { + return; + } + + $this->trackEntity($entity); + } + + /** + * Updates or creates a WorkspaceAssociation entity for a given entity. + * + * If the passed-in entity can belong to a workspace and already has a + * WorkspaceAssociation entity, then a new revision of this will be created with + * the new information. Otherwise, a new WorkspaceAssociation entity is created to + * store the passed-in entity's information. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to update or create from. + */ + protected function trackEntity(EntityInterface $entity) { + /** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ + // If the entity is not new, check if there's an existing + // WorkspaceAssociation entity for it. + if (!$entity->isNew()) { + $workspace_associations = $this->entityTypeManager + ->getStorage('workspace_association') + ->loadByProperties([ + 'content_entity_type_id' => $entity->getEntityTypeId(), + 'content_entity_id' => $entity->id(), + ]); + + /** @var \Drupal\Core\Entity\ContentEntityInterface $workspace_association */ + $workspace_association = reset($workspace_associations); + } + + // If there was a WorkspaceAssociation entry create a new revision, + // otherwise create a new entity with the type and ID. + if (!empty($workspace_association)) { + $workspace_association->setNewRevision(TRUE); + } + else { + $workspace_association = WorkspaceAssociation::create([ + 'content_entity_type_id' => $entity->getEntityTypeId(), + 'content_entity_id' => $entity->id(), + ]); + } + + // Add the revision ID and the workspace ID. + $workspace_association->set('content_entity_revision_id', $entity->getRevisionId()); + $workspace_association->set('workspace', $this->workspaceManager->getActiveWorkspace()->id()); + + // Save without updating the tracked content entity. + $workspace_association->save(); + } + +} diff --git a/src/EntityQuery/Query.php b/src/EntityQuery/Query.php index 0e309a5..19479f1 100644 --- a/src/EntityQuery/Query.php +++ b/src/EntityQuery/Query.php @@ -38,11 +38,11 @@ class Query extends BaseQuery { $revision_field = $this->entityType->getKey('revision'); // Since the query is against the base table, we have to take into account - // that the revision ID might come from the content_workspace + // that the revision ID might come from the workspace_association // relationship, and, as a consequence, the revision ID field is no longer // a simple SQL field but an expression. $this->sqlFields = []; - $this->sqlExpressions[$revision_field] = "COALESCE(content_workspace.content_entity_revision_id, base_table.$revision_field)"; + $this->sqlExpressions[$revision_field] = "COALESCE(workspace_association.content_entity_revision_id, base_table.$revision_field)"; $this->sqlExpressions[$id_field] = "base_table.$id_field"; } diff --git a/src/EntityQuery/QueryAggregate.php b/src/EntityQuery/QueryAggregate.php index 6e75d1a..3a1f181 100644 --- a/src/EntityQuery/QueryAggregate.php +++ b/src/EntityQuery/QueryAggregate.php @@ -17,6 +17,10 @@ class QueryAggregate extends BaseQueryAggregate { * {@inheritdoc} */ public function prepare() { + // Aggregate entity queries do not return an array of entity IDs keyed by + // revision IDs, they only return the values of the aggregated fields, so we + // don't need to add any expressions like we do in + // \Drupal\workspace\EntityQuery\Query::prepare(). $this->traitPrepare(); // Throw away the ID fields. diff --git a/src/EntityQuery/QueryTrait.php b/src/EntityQuery/QueryTrait.php index c3b50a9..6606106 100644 --- a/src/EntityQuery/QueryTrait.php +++ b/src/EntityQuery/QueryTrait.php @@ -59,11 +59,11 @@ trait QueryTrait { $this->sqlQuery->addMetaData('active_workspace_id', $active_workspace->id()); $this->sqlQuery->addMetaData('simple_query', FALSE); - // LEFT JOIN 'content_workspace' to the base table of the query so we can - // properly include live content along with a possible workspace-specific + // LEFT JOIN 'workspace_association' to the base table of the query so we + // can properly include live content along with a possible workspace // revision. $id_field = $this->entityType->getKey('id'); - $this->sqlQuery->leftJoin('content_workspace', 'content_workspace', "%alias.content_entity_type_id = '{$this->entityTypeId}' AND %alias.content_entity_id = base_table.$id_field AND %alias.workspace = '{$active_workspace->id()}'"); + $this->sqlQuery->leftJoin('workspace_association', 'workspace_association', "%alias.content_entity_type_id = '{$this->entityTypeId}' AND %alias.content_entity_id = base_table.$id_field AND %alias.workspace = '{$active_workspace->id()}'"); } return $this; diff --git a/src/EntityQuery/Tables.php b/src/EntityQuery/Tables.php index eb7c607..ed0eb01 100644 --- a/src/EntityQuery/Tables.php +++ b/src/EntityQuery/Tables.php @@ -20,7 +20,7 @@ class Tables extends BaseTables { protected $workspaceManager; /** - * Content workspace table array, key is base table name, value is alias. + * Workspace association table array, key is base table name, value is alias. * * @var array */ @@ -44,11 +44,11 @@ class Tables extends BaseTables { $this->workspaceManager = \Drupal::service('workspace.manager'); - // The join between the first 'content_workspace' table and base table of - // the query is done \Drupal\workspace\EntityQuery\QueryTrait::prepare(), so - // we need to initialize its entry manually. + // The join between the first 'workspace_association' table and base table + // of the query is done \Drupal\workspace\EntityQuery\QueryTrait::prepare(), + // so we need to initialize its entry manually. if ($this->sqlQuery->getMetaData('active_workspace_id')) { - $this->contentWorkspaceTables['base_table'] = 'content_workspace'; + $this->contentWorkspaceTables['base_table'] = 'workspace_association'; $this->baseTablesEntityType['base_table'] = $this->sqlQuery->getMetaData('entity_type'); } } @@ -98,8 +98,8 @@ class Tables extends BaseTables { $revision_key = $this->entityManager->getDefinition($entity_type_id)->getKey('revision'); if ($id_field === $revision_key || $id_field === 'revision_id') { - $content_workspace_table = $this->contentWorkspaceTables[$base_table]; - $join_condition = "{$condition_parts[0]} = COALESCE($content_workspace_table.content_entity_revision_id, {$condition_parts[1]})"; + $workspace_association_table = $this->contentWorkspaceTables[$base_table]; + $join_condition = "{$condition_parts[0]} = COALESCE($workspace_association_table.content_entity_revision_id, {$condition_parts[1]})"; } } } @@ -115,14 +115,14 @@ class Tables extends BaseTables { $active_workspace_id = $this->sqlQuery->getMetaData('active_workspace_id'); if ($active_workspace_id && $this->workspaceManager->entityTypeCanBelongToWorkspaces($entity_type)) { - $this->addContentWorkspaceJoin($entity_type->id(), $next_base_table_alias, $active_workspace_id); + $this->addWorkspaceAssociationJoin($entity_type->id(), $next_base_table_alias, $active_workspace_id); } return $next_base_table_alias; } /** - * Adds a new join to the 'content_workspace' table for an entity base table. + * Adds a new join to the 'workspace_association' table for an entity base table. * * This method assumes that the active workspace has already been determined * to be a non-default workspace. @@ -137,14 +137,14 @@ class Tables extends BaseTables { * @return string * The alias of the joined table. */ - public function addContentWorkspaceJoin($entity_type_id, $base_table_alias, $active_workspace_id) { + public function addWorkspaceAssociationJoin($entity_type_id, $base_table_alias, $active_workspace_id) { if (!isset($this->contentWorkspaceTables[$base_table_alias])) { $entity_type = $this->entityManager->getDefinition($entity_type_id); $id_field = $entity_type->getKey('id'); - // LEFT join the Content Workspace entity's table so we can properly + // 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('content_workspace', NULL, "%alias.content_entity_type_id = '$entity_type_id' AND %alias.content_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.content_entity_type_id = '$entity_type_id' AND %alias.content_entity_id = $base_table_alias.$id_field AND %alias.workspace = '$active_workspace_id'"); $this->baseTablesEntityType[$base_table_alias] = $entity_type->id(); } diff --git a/src/Plugin/RepositoryHandler/LocalWorkspaceRepositoryHandler.php b/src/Plugin/RepositoryHandler/LocalWorkspaceRepositoryHandler.php index f468031..d06e4f2 100644 --- a/src/Plugin/RepositoryHandler/LocalWorkspaceRepositoryHandler.php +++ b/src/Plugin/RepositoryHandler/LocalWorkspaceRepositoryHandler.php @@ -150,15 +150,15 @@ class LocalWorkspaceRepositoryHandler extends RepositoryHandlerBase implements R // relative to the source workspace. $this->workspaceManager->setActiveWorkspace($source_workspace); - $content_workspace_ids = []; + $workspace_association_ids = []; foreach ($this->workspaceManager->getSupportedEntityTypes() as $entity_type_id => $entity_type) { // Get all entity revision IDs for all entities which are in only one // of either the source or the target workspaces. We assume that this // means the revision is in the source, but not the target, and the // revision has not been replicated yet. $select = $this->database - ->select('content_workspace_revision', 'cwr') - ->fields('cwr', ['content_entity_revision_id']); + ->select('workspace_association_revision', 'war') + ->fields('war', ['content_entity_revision_id']); $select->condition('content_entity_type_id', $entity_type_id); $select->condition('workspace', [$source_workspace->id(), $target_workspace->id()], 'IN'); $select->groupBy('content_entity_revision_id'); @@ -166,10 +166,10 @@ class LocalWorkspaceRepositoryHandler extends RepositoryHandlerBase implements R $revision_difference = $select->execute()->fetchCol(); if (!empty($revision_difference)) { - // Get the content workspace IDs for all of the entity revision IDs + // Get the workspace association IDs for all of the entity revision IDs // which are not yet in the target workspace. - $content_workspace_ids[$entity_type_id] = $this->entityTypeManager - ->getStorage('content_workspace') + $workspace_association_ids[$entity_type_id] = $this->entityTypeManager + ->getStorage('workspace_association') ->getQuery() ->allRevisions() ->condition('content_entity_type_id', $entity_type_id) @@ -180,19 +180,19 @@ class LocalWorkspaceRepositoryHandler extends RepositoryHandlerBase implements R } $entities = []; - foreach ($content_workspace_ids as $entity_type_id => $ids) { + foreach ($workspace_association_ids as $entity_type_id => $ids) { foreach ($ids as $revision_id => $entity_id) { - // Get the content workspace entity for revision that is in the source + // Get the workspace association entity for revision that is in the source // workspace. - /** @var \Drupal\Core\Entity\ContentEntityInterface $content_workspace */ - $content_workspace = $this->entityTypeManager->getStorage('content_workspace')->loadRevision($revision_id); + /** @var \Drupal\Core\Entity\ContentEntityInterface $workspace_association */ + $workspace_association = $this->entityTypeManager->getStorage('workspace_association')->loadRevision($revision_id); if ($target_workspace->isDefaultWorkspace()) { // If the target workspace is the default workspace, the revision // needs to be set to the default revision. /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionableInterface $entity */ $entity = $this->entityTypeManager - ->getStorage($content_workspace->content_entity_type_id->value) - ->loadRevision($content_workspace->content_entity_revision_id->value); + ->getStorage($workspace_association->content_entity_type_id->value) + ->loadRevision($workspace_association->content_entity_revision_id->value); $entity->_isReplicating = TRUE; $entity->isDefaultRevision(TRUE); $entities[] = $entity; @@ -201,9 +201,9 @@ class LocalWorkspaceRepositoryHandler extends RepositoryHandlerBase implements R // If the target workspace is not the default workspace, the content // workspace link entity can simply be updated with the target // workspace. - $content_workspace->setNewRevision(TRUE); - $content_workspace->workspace->target_id = $target_workspace->id(); - $content_workspace->save(); + $workspace_association->setNewRevision(TRUE); + $workspace_association->workspace->target_id = $target_workspace->id(); + $workspace_association->save(); } } } diff --git a/src/ViewsQueryAlter.php b/src/ViewsQueryAlter.php new file mode 100644 index 0000000..6da6ae6 --- /dev/null +++ b/src/ViewsQueryAlter.php @@ -0,0 +1,423 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->workspaceManager = $workspace_manager; + $this->viewsData = $views_data; + $this->viewsJoinPluginManager = $views_join_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('workspace.manager'), + $container->get('views.views_data'), + $container->get('plugin.manager.views.join') + ); + } + + /** + * Implements a hook bridge for hook_views_query_alter(). + * + * @see hook_views_query_alter() + */ + public function alterQuery(ViewExecutable $view, QueryPluginBase $query) { + // Don't alter any views queries if we're in the default workspace. + if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { + return; + } + + // Don't alter any non-sql views queries. + if (!$query instanceof Sql) { + return; + } + + // Find out what entity types are represented in this query. + $entity_type_ids = []; + foreach ($query->relationships as $info) { + $table_data = $this->viewsData->get($info['base']); + if (empty($table_data['table']['entity type'])) { + continue; + } + $entity_type_id = $table_data['table']['entity type']; + // This construct ensures each entity type exists only once. + $entity_type_ids[$entity_type_id] = $entity_type_id; + } + + $entity_type_definitions = $this->entityTypeManager->getDefinitions(); + foreach ($entity_type_ids as $entity_type_id) { + if ($this->workspaceManager->entityTypeCanBelongToWorkspaces($entity_type_definitions[$entity_type_id])) { + $this->alterQueryForEntityType($view, $query, $entity_type_definitions[$entity_type_id]); + } + } + } + + /** + * Alters the entity type tables for a Views query. + * + * @param \Drupal\views\ViewExecutable $view + * The view object about to be processed. + * @param \Drupal\views\Plugin\views\query\Sql $query + * The query plugin object for the query. + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + */ + protected function alterQueryForEntityType(ViewExecutable $view, Sql $query, EntityTypeInterface $entity_type) { + // This is only called after we determined that this entity type is involved + // in the query, and that a non-default workspace is in use. + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->entityTypeManager->getStorage($entity_type->id())->getTableMapping(); + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); + $dedicated_field_storage_definitions = array_filter($field_storage_definitions, function ($definition) use ($table_mapping) { + return $table_mapping->requiresDedicatedTableStorage($definition); + }); + $dedicated_field_data_tables = array_map(function ($definition) use ($table_mapping) { + return $table_mapping->getDedicatedDataTableName($definition); + }, $dedicated_field_storage_definitions); + + $move_workspace_tables = []; + $table_queue =& $query->getTableQueue(); + foreach ($table_queue as $alias => &$table_info) { + // If we reach the workspace_association array item before any candidates, + // then we do not need to move it. + if ($table_info['table'] == 'workspace_association') { + break; + } + + // Any dedicated field table is a candidate. + if ($field_name = array_search($table_info['table'], $dedicated_field_data_tables, TRUE)) { + $relationship = $table_info['relationship']; + + // There can be reverse relationships used. If so, Workspace can't do + // anything with them. Detect this and skip. + if ($table_info['join']->field != 'entity_id') { + continue; + } + + // Get the dedicated revision table name. + $new_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]); + + // Now add the workspace_association table. + $workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship); + + // Update the join to use our COALESCE. + $revision_field = $entity_type->getKey('revision'); + $table_info['join']->leftTable = NULL; + $table_info['join']->leftField = "COALESCE($workspace_association_table.content_entity_revision_id, $relationship.$revision_field)"; + + // Update the join and the table info to our new table name, and to join + // on the revision key. + $table_info['table'] = $new_table_name; + $table_info['join']->table = $new_table_name; + $table_info['join']->field = 'revision_id'; + + // Finally, if we added the workspace_association table we have to move + // it in the table queue so that it comes before this field. + if (empty($move_workspace_tables[$workspace_association_table])) { + $move_workspace_tables[$workspace_association_table] = $alias; + } + } + } + + // JOINs must be in order. i.e, any tables you mention in the ON clause of a + // JOIN must appear prior to that JOIN. Since we're modifying a JOIN in + // place, and adding a new table, we must ensure that the new table appears + // prior to this one. So we recorded at what index we saw that table, and + // then use array_splice() to move the workspace_association table join to + // the correct position. + foreach ($move_workspace_tables as $workspace_association_table => $alias) { + $this->moveEntityTable($query, $workspace_association_table, $alias); + } + + $base_entity_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable(); + + $base_fields = array_diff($table_mapping->getFieldNames($entity_type->getBaseTable()), [$entity_type->getKey('langcode')]); + $revisionable_fields = array_diff($table_mapping->getFieldNames($entity_type->getRevisionDataTable()), $base_fields); + + // Go through and look to see if we have to modify fields and filters. + foreach ($query->fields as &$field_info) { + // Some fields don't actually have tables, meaning they're formulae and + // whatnot. At this time we are going to ignore those. + if (empty($field_info['table'])) { + continue; + } + + // Dereference the alias into the actual table. + $table = $table_queue[$field_info['table']]['table']; + if ($table == $base_entity_table && in_array($field_info['field'], $revisionable_fields)) { + $relationship = $table_queue[$field_info['table']]['alias']; + $alias = $this->ensureRevisionTable($entity_type, $query, $relationship); + if ($alias) { + // Change the base table to use the revision table instead. + $field_info['table'] = $alias; + } + } + } + + $relationships = []; + // Build a list of all relationships that might be for our table. + foreach ($query->relationships as $relationship => $info) { + if ($info['base'] == $base_entity_table) { + $relationships[] = $relationship; + } + } + + // Now we have to go through our where clauses and modify any of our fields. + foreach ($query->where as &$clauses) { + foreach ($clauses['conditions'] as &$where_info) { + // Build a matrix of our possible relationships against fields we need to + // switch. + foreach ($relationships as $relationship) { + foreach ($revisionable_fields as $field) { + if (is_string($where_info['field']) && $where_info['field'] == "$relationship.$field") { + $alias = $this->ensureRevisionTable($entity_type, $query, $relationship); + if ($alias) { + // Change the base table to use the revision table instead. + $where_info['field'] = "$alias.$field"; + } + } + } + } + } + } + + // @todo Handle $query->orderby, $query->groupby, $query->having, + // $query->count_field. + } + + /** + * Adds the 'workspace_association' table to a views query. + * + * @param string $entity_type_id + * The ID of the entity type to join. + * @param \Drupal\views\Plugin\views\query\Sql $query + * The query plugin object for the query. + * @param string $relationship + * The primary table alias this table is related to. + * + * @return string + * The alias of the 'workspace_association' table. + */ + protected function ensureWorkspaceAssociationTable($entity_type_id, Sql $query, $relationship) { + if (isset($query->tables[$relationship]['workspace_association'])) { + return $query->tables[$relationship]['workspace_association']['alias']; + } + + $table_data = $this->viewsData->get($query->relationships[$relationship]['base']); + + // Construct the join. + $definition = [ + 'table' => 'workspace_association', + 'field' => 'content_entity_id', + 'left_table' => $relationship, + 'left_field' => $table_data['table']['base']['field'], + 'extra' => [ + [ + 'field' => 'content_entity_type_id', + 'value' => $entity_type_id, + ], + [ + 'field' => 'workspace', + 'value' => $this->workspaceManager->getActiveWorkspace()->id(), + ], + ], + 'type' => 'LEFT', + ]; + + $join = $this->viewsJoinPluginManager->createInstance('standard', $definition); + $join->adjusted = TRUE; + + return $query->queueTable('workspace_association', $relationship, $join); + } + + /** + * Adds the revision table of an entity type to a query object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\views\Plugin\views\query\Sql $query + * The query plugin object for the query. + * @param string $relationship + * The name of the relationship. + * + * @return string + * The alias of the relationship. + */ + protected function ensureRevisionTable(EntityTypeInterface $entity_type, Sql $query, $relationship) { + // Get the alias for the 'workspace_association' table we chain off of in the + // COALESCE. + $workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship); + + // Get the name of the revision table and revision key. + $base_revision_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable(); + $revision_field = $entity_type->getKey('revision'); + + // If the table was already added and has a join against the same field on + // the revision table, reuse that rather than adding a new join. + if (isset($query->tables[$relationship][$base_revision_table])) { + $table_queue =& $query->getTableQueue(); + $alias = $query->tables[$relationship][$base_revision_table]['alias']; + if (isset($table_queue[$alias]['join']->field) && $table_queue[$alias]['join']->field == $revision_field) { + // If this table previously existed, but was not added by us, we need + // to modify the join and make sure that 'workspace_association' comes first. + if (empty($table_queue[$alias]['join']->workspace_adjusted)) { + $table_queue[$alias]['join'] = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table); + // We also have to ensure that our 'workspace_association' comes before + // this. + $this->moveEntityTable($query, $workspace_association_table, $alias); + } + + return $alias; + } + } + + // Construct a new join. + $join = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table); + return $query->queueTable($base_revision_table, $relationship, $join); + } + + /** + * Fetches a join for a revision table using the workspace_association table. + * + * @param string $relationship + * The relationship to use in the view. + * @param string $table + * The table name. + * @param string $field + * The field to join on. + * @param string $workspace_association_table + * The alias of the 'workspace_association' table joined to the main entity + * table. + * + * @return \Drupal\views\Plugin\views\join\JoinPluginInterface + * An adjusted views join object to add to the query. + */ + protected function getRevisionTableJoin($relationship, $table, $field, $workspace_association_table) { + $definition = [ + 'table' => $table, + 'field' => $field, + // Making this explicitly null allows the left table to be a formula. + 'left_table' => NULL, + 'left_field' => "COALESCE($workspace_association_table.content_entity_revision_id, $relationship.$field)", + ]; + + /** @var \Drupal\views\Plugin\views\join\JoinPluginInterface $join */ + $join = $this->viewsJoinPluginManager->createInstance('standard', $definition); + $join->adjusted = TRUE; + $join->workspace_adjusted = TRUE; + + return $join; + } + + /** + * Moves a 'workspace_association' table to appear before the given alias. + * + * Because Workspace chains possibly pre-existing tables onto the + * 'workspace_association' table, we have to ensure that the + * 'workspace_association' table appears in the query before the alias it's + * chained on or the SQL is invalid. This uses array_slice() to reconstruct + * the table queue of the query. + * + * @param \Drupal\views\Plugin\views\query\Sql $query + * The SQL query object. + * @param string $workspace_association_table + * The alias of the 'workspace_association' table. + * @param string $alias + * The alias of the table it needs to appear before. + */ + protected function moveEntityTable(Sql $query, $workspace_association_table, $alias) { + $table_queue =& $query->getTableQueue(); + $keys = array_keys($table_queue); + $current_index = array_search($workspace_association_table, $keys); + $index = array_search($alias, $keys); + + // If it's already before our table, we don't need to move it, as we could + // accidentally move it forward. + if ($current_index < $index) { + return; + } + $splice = [$workspace_association_table => $table_queue[$workspace_association_table]]; + unset($table_queue[$workspace_association_table]); + + // Now move the item to the proper location in the array. Don't use + // array_splice() because that breaks indices. + $table_queue = array_slice($table_queue, 0, $index, TRUE) + + $splice + + array_slice($table_queue, $index, NULL, TRUE); + } + +} diff --git a/src/WorkspaceManager.php b/src/WorkspaceManager.php index 516ca5b..68dbf32 100644 --- a/src/WorkspaceManager.php +++ b/src/WorkspaceManager.php @@ -3,13 +3,11 @@ namespace Drupal\workspace; use Drupal\Core\DependencyInjection\ClassResolverInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\workspace\Entity\ContentWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -34,7 +32,7 @@ class WorkspaceManager implements WorkspaceManagerInterface { * @var string[] */ protected $blacklist = [ - 'content_workspace', + 'workspace_association', 'replication_log', 'workspace', ]; @@ -193,42 +191,4 @@ class WorkspaceManager implements WorkspaceManagerInterface { return $this; } - /** - * {@inheritdoc} - */ - public function updateOrCreateFromEntity(EntityInterface $entity) { - // If the entity is not new, check if there's an existing ContentWorkspace - // entity for it. - if (!$entity->isNew()) { - $content_workspaces = $this->entityTypeManager - ->getStorage('content_workspace') - ->loadByProperties([ - 'content_entity_type_id' => $entity->getEntityTypeId(), - 'content_entity_id' => $entity->id(), - ]); - - /** @var \Drupal\Core\Entity\ContentEntityInterface $content_workspace */ - $content_workspace = reset($content_workspaces); - } - - // If there was a ContentWorkspace entry create a new revision, otherwise - // create a new entity with the type and ID. - if (!empty($content_workspace)) { - $content_workspace->setNewRevision(TRUE); - } - else { - $content_workspace = ContentWorkspace::create([ - 'content_entity_type_id' => $entity->getEntityTypeId(), - 'content_entity_id' => $entity->id(), - ]); - } - - // Add the revision ID and the workspace ID. - $content_workspace->set('content_entity_revision_id', $entity->getRevisionId()); - $content_workspace->set('workspace', $this->getActiveWorkspace()->id()); - - // Save without updating the content entity. - $content_workspace->save(); - } - } diff --git a/src/WorkspaceManagerInterface.php b/src/WorkspaceManagerInterface.php index 6feadb0..0467089 100644 --- a/src/WorkspaceManagerInterface.php +++ b/src/WorkspaceManagerInterface.php @@ -2,7 +2,6 @@ namespace Drupal\workspace; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; /** @@ -50,17 +49,4 @@ interface WorkspaceManagerInterface { */ public function setActiveWorkspace(WorkspaceInterface $workspace); - /** - * Update or create a ContentWorkspace entity from another entity. - * - * If the passed-in entity can belong to a workspace and already has a - * ContentWorkspace entity, then a new revision of this will be created with - * the new information. Otherwise, a new ContentWorkspace entity is created to - * store the passed-in entity's information. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to update or create from. - */ - public function updateOrCreateFromEntity(EntityInterface $entity); - } diff --git a/tests/src/Kernel/WorkspaceIntegrationTest.php b/tests/src/Kernel/WorkspaceIntegrationTest.php index bb50147..4babd2c 100644 --- a/tests/src/Kernel/WorkspaceIntegrationTest.php +++ b/tests/src/Kernel/WorkspaceIntegrationTest.php @@ -99,7 +99,7 @@ class WorkspaceIntegrationTest extends KernelTestBase { $this->entityTypeManager = \Drupal::entityTypeManager(); $this->installEntitySchema('workspace'); - $this->installEntitySchema('content_workspace'); + $this->installEntitySchema('workspace_association'); $this->installEntitySchema('replication_log'); // Create two workspaces by default, 'live' and 'stage'. diff --git a/tests/src/Kernel/WorkspaceInternalResourceTest.php b/tests/src/Kernel/WorkspaceInternalResourceTest.php index 08c14fc..632140a 100644 --- a/tests/src/Kernel/WorkspaceInternalResourceTest.php +++ b/tests/src/Kernel/WorkspaceInternalResourceTest.php @@ -20,14 +20,14 @@ class WorkspaceInternalResourceTest extends KernelTestBase { public static $modules = ['user', 'serialization', 'rest', 'workspace']; /** - * Tests enabling content workspaces for REST throws an exception. + * Tests enabling workspace associations for REST throws an exception. * * @see workspace_rest_resource_alter() */ - public function testCreateContentWorkspaceResource() { - $this->setExpectedException(PluginNotFoundException::class, 'The "entity:content_workspace" plugin does not exist.'); + public function testCreateWorkspaceAssociationResource() { + $this->setExpectedException(PluginNotFoundException::class, 'The "entity:workspace_association" plugin does not exist.'); RestResourceConfig::create([ - 'id' => 'entity.content_workspace', + 'id' => 'entity.workspace_association', 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY, 'configuration' => [ 'methods' => ['GET'], diff --git a/workspace.module b/workspace.module index c673f0e..708aad4 100644 --- a/workspace.module +++ b/workspace.module @@ -9,13 +9,13 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Cache\Cache; use Drupal\Core\Url; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\views\Plugin\views\query\QueryPluginBase; -use Drupal\views\Plugin\views\query\Sql; use Drupal\views\ViewExecutable; use Drupal\workspace\EntityAccess; +use Drupal\workspace\EntityOperations; +use Drupal\workspace\ViewsQueryAlter; /** * Implements hook_help(). @@ -35,145 +35,36 @@ function workspace_help($route_name, RouteMatchInterface $route_match) { * Implements hook_entity_load(). */ function workspace_entity_load(array &$entities, $entity_type_id) { - /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ - $workspace_manager = \Drupal::service('workspace.manager'); - $entity_type_manager = \Drupal::entityTypeManager(); - - // Only run if the entity type can belong to a workspace and we are in a - // non-default workspace. - if (!$workspace_manager->entityTypeCanBelongToWorkspaces($entity_type_manager->getDefinition($entity_type_id)) - || (($active_workspace = $workspace_manager->getActiveWorkspace()) && $active_workspace->isDefaultWorkspace())) { - return; - } - - // Get a list of revision IDs for entities that have a revision set for the - // current active workspace. If an entity has multiple revisions set for a - // workspace, only the one with the highest ID is returned. - $entity_ids = array_keys($entities); - $max_revision_id = 'max_content_entity_revision_id'; - $results = $entity_type_manager - ->getStorage('content_workspace') - ->getAggregateQuery() - ->allRevisions() - ->aggregate('content_entity_revision_id', 'MAX', NULL, $max_revision_id) - ->groupBy('content_entity_id') - ->condition('content_entity_type_id', $entity_type_id) - ->condition('content_entity_id', $entity_ids, 'IN') - ->condition('workspace', $active_workspace->id(), '=') - ->execute(); - - // Since hook_entity_load() is called on both regular entity load as well as - // entity revision load, we need to prevent infinite recursion by checking - // whether the default revisions were already swapped with the workspace - // revision. - // @todo This recursion protection should be removed when - // https://www.drupal.org/project/drupal/issues/2928888 is resolved. - if ($results) { - foreach ($results as $key => $result) { - if ($entities[$result['content_entity_id']]->getRevisionId() == $result[$max_revision_id]) { - unset($results[$key]); - } - } - } - - if ($results) { - /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ - $storage = $entity_type_manager->getStorage($entity_type_id); - - // Swap out every entity which has a revision set for the current active - // workspace. - $swap_revision_ids = array_column($results, $max_revision_id); - foreach ($storage->loadMultipleRevisions($swap_revision_ids) as $revision) { - $entities[$revision->id()] = $revision; - } - } + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityOperations::class) + ->entityLoad($entities, $entity_type_id); } /** * Implements hook_entity_presave(). */ function workspace_entity_presave(EntityInterface $entity) { - /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ - /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ - $workspace_manager = \Drupal::service('workspace.manager'); - - // Only run if the entity type can belong to a workspace and we are in a - // non-default workspace. - if (!$workspace_manager->entityTypeCanBelongToWorkspaces($entity->getEntityType()) - || $workspace_manager->getActiveWorkspace()->isDefaultWorkspace()) { - return; - } - - // Force a new revision if the entity is not replicating. - if (!$entity->isNew() && !isset($entity->_isReplicating)) { - $entity->setNewRevision(TRUE); - - // All entities in the non-default workspace are pending revisions, - // regardless of their publishing status. This means that when creating - // a published pending revision in a non-default workspace it will also be - // a published pending revision in the default workspace, however, it will - // become the default revision only when it is replicated to the default - // workspace. - $entity->isDefaultRevision(FALSE); - } - - // When a new published entity is inserted in a non-default workspace, we - // actually want two revisions to be saved: - // - An unpublished default revision in the default ('live') workspace. - // - A published pending revision in the current workspace. - if ($entity->isNew() && $entity->isPublished()) { - // Keep track of the publishing status for workspace_entity_insert() and - // unpublish the default revision. - $entity->_initialPublished = TRUE; - $entity->setUnpublished(); - } + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityOperations::class) + ->entityPresave($entity); } /** * Implements hook_entity_insert(). */ function workspace_entity_insert(EntityInterface $entity) { - /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ - /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ - $workspace_manager = \Drupal::service('workspace.manager'); - - // Only run if the entity type can belong to a workspace and we are in a - // non-default workspace. - if (!$workspace_manager->entityTypeCanBelongToWorkspaces($entity->getEntityType()) - || $workspace_manager->getActiveWorkspace()->isDefaultWorkspace()) { - return; - } - - // Handle the case when a new published entity was created in a non-default - // workspace and create a published pending revision for it. - if (isset($entity->_initialPublished)) { - // Operate on a clone to avoid changing the entity prior to subsequent - // hook_entity_insert() implementations. - $pending_revision = clone $entity; - $pending_revision->setPublished(); - $pending_revision->isDefaultRevision(FALSE); - $pending_revision->save(); - } - else { - $workspace_manager->updateOrCreateFromEntity($entity); - } + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityOperations::class) + ->entityInsert($entity); } /** * Implements hook_entity_update(). */ function workspace_entity_update(EntityInterface $entity) { - /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ - $workspace_manager = \Drupal::service('workspace.manager'); - - // Only run if the entity type can belong to a workspace and we are in a - // non-default workspace. - if (!$workspace_manager->entityTypeCanBelongToWorkspaces($entity->getEntityType()) - || $workspace_manager->getActiveWorkspace()->isDefaultWorkspace()) { - return; - } - - $workspace_manager->updateOrCreateFromEntity($entity); + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityOperations::class) + ->entityUpdate($entity); } /** @@ -202,348 +93,18 @@ function workspace_entity_create_access(AccountInterface $account, array $contex * Implements hook_views_query_alter(). */ function workspace_views_query_alter(ViewExecutable $view, QueryPluginBase $query) { - /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ - $workspace_manager = \Drupal::service('workspace.manager'); - - // Don't alter any views queries if we're in the default workspace. - $active_workspace = $workspace_manager->getActiveWorkspace(); - if ($active_workspace->isDefaultWorkspace()) { - return; - } - - // Don't alter any non-sql views queries. - if (!$query instanceof Sql) { - return; - } - - /** @var \Drupal\views\ViewsData $views_data */ - $views_data = \Drupal::service('views.views_data'); - - // Find out what entity types are represented in this query. - $entity_type_ids = []; - /** @var \Drupal\views\Plugin\views\query\Sql $query */ - foreach ($query->relationships as $info) { - $table_data = $views_data->get($info['base']); - if (empty($table_data['table']['entity type'])) { - continue; - } - $entity_type_id = $table_data['table']['entity type']; - // This construct ensures each entity type exists only once. - $entity_type_ids[$entity_type_id] = $entity_type_id; - } - - $entity_type_definitions = \Drupal::entityTypeManager()->getDefinitions(); - foreach ($entity_type_ids as $entity_type_id) { - if ($workspace_manager->entityTypeCanBelongToWorkspaces($entity_type_definitions[$entity_type_id])) { - _workspace_views_query_alter_entity_type($view, $query, $entity_type_definitions[$entity_type_id]); - } - } -} - -/** - * Alters the entity type tables for a Views query. - * - * @param \Drupal\views\ViewExecutable $view - * The view object about to be processed. - * @param \Drupal\views\Plugin\views\query\Sql $query - * The query plugin object for the query. - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type definition. - * - * @internal - */ -function _workspace_views_query_alter_entity_type(ViewExecutable $view, Sql $query, EntityTypeInterface $entity_type) { - // This is only called after we determined that this entity type is involved - // in the query, and that a non-default workspace is in use. - /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ - $table_mapping = \Drupal::entityTypeManager()->getStorage($entity_type->id())->getTableMapping(); - $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type->id()); - $dedicated_field_storage_definitions = array_filter($field_storage_definitions, function ($definition) use ($table_mapping) { - return $table_mapping->requiresDedicatedTableStorage($definition); - }); - $dedicated_field_data_tables = array_map(function ($definition) use ($table_mapping) { - return $table_mapping->getDedicatedDataTableName($definition); - }, $dedicated_field_storage_definitions); - - $move_workspace_tables = []; - foreach ($query->tableQueue as $alias => &$table_info) { - // If we reach the content_workspace array item before any candidates, then - // we do not need to move it. - if ($table_info['table'] == 'content_workspace') { - break; - } - - // Any dedicated field table is a candidate. - if ($field_name = array_search($table_info['table'], $dedicated_field_data_tables, TRUE)) { - $relationship = $table_info['relationship']; - - // There can be reverse relationships used. If so, Workspace can't do - // anything with them. Detect this and skip. - if ($table_info['join']->field != 'entity_id') { - continue; - } - - // Get the dedicated revision table name. - $new_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]); - - // Now add the content_workspace table. - $content_workspace_table = _workspace_ensure_content_workspace_table($entity_type->id(), $query, $relationship); - - // Update the join to use our COALESCE. - $revision_field = $entity_type->getKey('revision'); - $table_info['join']->leftTable = NULL; - $table_info['join']->leftField = "COALESCE($content_workspace_table.content_entity_revision_id, $relationship.$revision_field)"; - - // Update the join and the table info to our new table name, and to join - // on the revision key. - $table_info['table'] = $new_table_name; - $table_info['join']->table = $new_table_name; - $table_info['join']->field = 'revision_id'; - - // Finally, if we added the content_workspace table we have to move it in - // the table queue so that it comes before this field. - if (empty($move_workspace_tables[$content_workspace_table])) { - $move_workspace_tables[$content_workspace_table] = $alias; - } - } - } - - // JOINs must be in order. i.e, any tables you mention in the ON clause of a - // JOIN must appear prior to that JOIN. Since we're modifying a JOIN in place, - // and adding a new table, we must ensure that the new table appears prior to - // this one. So we recorded at what index we saw that table, and then use - // array_splice() to move the content_workspace table join to the correct - // position. - foreach ($move_workspace_tables as $content_workspace_table => $alias) { - _workspace_move_entity_table($query, $content_workspace_table, $alias); - } - - $base_entity_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable(); - - $base_fields = array_diff($table_mapping->getFieldNames($entity_type->getBaseTable()), [$entity_type->getKey('langcode')]); - $revisionable_fields = array_diff($table_mapping->getFieldNames($entity_type->getRevisionDataTable()), $base_fields); - - // Go through and look to see if we have to modify fields and filters. - foreach ($query->fields as &$field_info) { - // Some fields don't actually have tables, meaning they're formulae and - // whatnot. At this time we are going to ignore those. - if (empty($field_info['table'])) { - continue; - } - - // Dereference the alias into the actual table. - $table = $query->tableQueue[$field_info['table']]['table']; - if ($table == $base_entity_table && in_array($field_info['field'], $revisionable_fields)) { - $relationship = $query->tableQueue[$field_info['table']]['alias']; - $alias = _workspace_ensure_revision_table($entity_type, $query, $relationship); - if ($alias) { - // Change the base table to use the revision table instead. - $field_info['table'] = $alias; - } - } - } - - $relationships = []; - // Build a list of all relationships that might be for our table. - foreach ($query->relationships as $relationship => $info) { - if ($info['base'] == $base_entity_table) { - $relationships[] = $relationship; - } - } - - // Now we have to go through our where clauses and modify any of our fields. - foreach ($query->where as &$clauses) { - foreach ($clauses['conditions'] as &$where_info) { - // Build a matrix of our possible relationships against fields we need to - // switch. - foreach ($relationships as $relationship) { - foreach ($revisionable_fields as $field) { - if (is_string($where_info['field']) && $where_info['field'] == "$relationship.$field") { - $alias = _workspace_ensure_revision_table($entity_type, $query, $relationship); - if ($alias) { - // Change the base table to use the revision table instead. - $where_info['field'] = "$alias.$field"; - } - } - } - } - } - } - - // @todo Handle $query->orderby, $query->groupby, $query->having, $query->count_field -} - -/** - * Adds the 'content_workspace' table to a views query. - * - * @param string $entity_type_id - * The ID of the entity type to join. - * @param \Drupal\views\Plugin\views\query\Sql $query - * The query plugin object for the query. - * @param string $relationship - * The primary table alias this table is related to. - * - * @return string - * The alias of the 'content_workspace' table. - * - * @internal - */ -function _workspace_ensure_content_workspace_table($entity_type_id, Sql $query, $relationship) { - if (isset($query->tables[$relationship]['content_workspace'])) { - return $query->tables[$relationship]['content_workspace']['alias']; - } - - $table_data = \Drupal::service('views.views_data')->get($query->relationships[$relationship]['base']); - - // Construct the join. - $definition = [ - 'table' => 'content_workspace', - 'field' => 'content_entity_id', - 'left_table' => $relationship, - 'left_field' => $table_data['table']['base']['field'], - 'extra' => [ - [ - 'field' => 'content_entity_type_id', - 'value' => $entity_type_id, - ], - [ - 'field' => 'workspace', - 'value' => \Drupal::service('workspace.manager')->getActiveWorkspace()->id(), - ], - ], - 'type' => 'LEFT', - ]; - - $join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $definition); - $join->adjusted = TRUE; - - return $query->queueTable('content_workspace', $relationship, $join); -} - -/** - * Adds the revision table of an entity type to a query object. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type definition. - * @param \Drupal\views\Plugin\views\query\Sql $query - * The query plugin object for the query. - * @param string $relationship - * The name of the relationship. - * - * @return string - * The alias of the relationship. - * - * @internal - */ -function _workspace_ensure_revision_table(EntityTypeInterface $entity_type, Sql $query, $relationship) { - // Get the alias for the 'content_workspace' table we chain off of in the - // COALESCE. - $content_workspace_table = _workspace_ensure_content_workspace_table($entity_type->id(), $query, $relationship); - - // Get the name of the revision table and revision key. - $base_revision_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable(); - $revision_field = $entity_type->getKey('revision'); - - // If the table was already added and has a join against the same field on - // the revision table, reuse that rather than adding a new join. - if (isset($query->tables[$relationship][$base_revision_table])) { - $alias = $query->tables[$relationship][$base_revision_table]['alias']; - if (isset($query->tableQueue[$alias]['join']->field) && $query->tableQueue[$alias]['join']->field == $revision_field) { - // If this table previously existed, but was not added by us, we need - // to modify the join and make sure that 'content_workspace' comes first. - if (empty($query->tableQueue[$alias]['join']->workspace_adjusted)) { - $query->tableQueue[$alias]['join'] = _workspace_get_revision_table_join($relationship, $base_revision_table, $revision_field, $content_workspace_table); - // We also have to ensure that our 'content_workspace' comes before - // this. - _workspace_move_entity_table($query, $content_workspace_table, $alias); - } - - return $alias; - } - } - - // Construct a new join. - $join = _workspace_get_revision_table_join($relationship, $base_revision_table, $revision_field, $content_workspace_table); - return $query->queueTable($base_revision_table, $relationship, $join); -} - -/** - * Fetches a join for a revision table using the 'content_workspace' table. - * - * @param string $relationship - * The relationship to use in the view. - * @param string $table - * The table name. - * @param string $field - * The field to join on. - * @param string $content_workspace_table - * The alias of the 'content_workspace' table joined to the main entity table. - * - * @return \Drupal\views\Plugin\views\join\JoinPluginInterface - * An adjusted views join object to add to the query. - * - * @internal - */ -function _workspace_get_revision_table_join($relationship, $table, $field, $content_workspace_table) { - $definition = [ - 'table' => $table, - 'field' => $field, - // Making this explicitly null allows the left table to be a formula. - 'left_table' => NULL, - 'left_field' => "COALESCE($content_workspace_table.content_entity_revision_id, $relationship.$field)", - ]; - - $join = \Drupal::service('plugin.manager.views.join')->createInstance('standard', $definition); - $join->adjusted = TRUE; - $join->workspace_adjusted = TRUE; - - return $join; -} - -/** - * Moves a 'content_workspace' table to appear before the given alias. - * - * Because Workspace chains possibly pre-existing tables onto the - * 'content_workspace' table, we have to ensure that the 'content_workspace' - * table appears in the query before the alias it's chained on or the SQL is - * invalid. This uses array_slice() to reconstruct the table queue of the query. - * - * @param \Drupal\views\Plugin\views\query\Sql $query - * The SQL query object. - * @param string $content_workspace_table - * The alias of the 'content_workspace' table. - * @param string $alias - * The alias of the table it needs to appear before. - * - * @internal - */ -function _workspace_move_entity_table(Sql $query, $content_workspace_table, $alias) { - $keys = array_keys($query->tableQueue); - $current_index = array_search($content_workspace_table, $keys); - $index = array_search($alias, $keys); - - // If it's already before our table, we don't need to move it, as we could - // accidentally move it forward. - if ($current_index < $index) { - return; - } - $splice = [$content_workspace_table => $query->tableQueue[$content_workspace_table]]; - unset($query->tableQueue[$content_workspace_table]); - - // Now move the item to the proper location in the array. Don't use - // array_splice() because that breaks indices. - $query->tableQueue = array_slice($query->tableQueue, 0, $index, TRUE) + - $splice + - array_slice($query->tableQueue, $index, NULL, TRUE); + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(ViewsQueryAlter::class) + ->alterQuery($view, $query); } /** * Implements hook_rest_resource_alter(). */ function workspace_rest_resource_alter(&$definitions) { - // ContentWorkspace and ReplicationLog are internal entity types, therefore - // they should not be exposed via REST. - unset($definitions['entity:content_workspace']); + // WorkspaceAssociation and ReplicationLog are internal entity types, + // therefore they should not be exposed via REST. + unset($definitions['entity:workspace_association']); unset($definitions['entity:replication_log']); }