Chris@17: entityTypeManager = $entity_type_manager; Chris@17: $this->entityFieldManager = $entity_field_manager; Chris@17: $this->workspaceManager = $workspace_manager; Chris@17: $this->viewsData = $views_data; Chris@17: $this->viewsJoinPluginManager = $views_join_plugin_manager; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public static function create(ContainerInterface $container) { Chris@17: return new static( Chris@17: $container->get('entity_type.manager'), Chris@17: $container->get('entity_field.manager'), Chris@17: $container->get('workspaces.manager'), Chris@17: $container->get('views.views_data'), Chris@17: $container->get('plugin.manager.views.join') Chris@17: ); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Implements a hook bridge for hook_views_query_alter(). Chris@17: * Chris@17: * @see hook_views_query_alter() Chris@17: */ Chris@17: public function alterQuery(ViewExecutable $view, QueryPluginBase $query) { Chris@17: // Don't alter any views queries if we're in the default workspace. Chris@17: if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) { Chris@17: return; Chris@17: } Chris@17: Chris@17: // Don't alter any non-sql views queries. Chris@17: if (!$query instanceof Sql) { Chris@17: return; Chris@17: } Chris@17: Chris@17: // Find out what entity types are represented in this query. Chris@17: $entity_type_ids = []; Chris@17: foreach ($query->relationships as $info) { Chris@17: $table_data = $this->viewsData->get($info['base']); Chris@17: if (empty($table_data['table']['entity type'])) { Chris@17: continue; Chris@17: } Chris@17: $entity_type_id = $table_data['table']['entity type']; Chris@17: // This construct ensures each entity type exists only once. Chris@17: $entity_type_ids[$entity_type_id] = $entity_type_id; Chris@17: } Chris@17: Chris@17: $entity_type_definitions = $this->entityTypeManager->getDefinitions(); Chris@17: foreach ($entity_type_ids as $entity_type_id) { Chris@17: if ($this->workspaceManager->isEntityTypeSupported($entity_type_definitions[$entity_type_id])) { Chris@17: $this->alterQueryForEntityType($query, $entity_type_definitions[$entity_type_id]); Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Alters the entity type tables for a Views query. Chris@17: * Chris@17: * This should only be called after determining that this entity type is Chris@17: * involved in the query, and that a non-default workspace is in use. Chris@17: * Chris@17: * @param \Drupal\views\Plugin\views\query\Sql $query Chris@17: * The query plugin object for the query. Chris@17: * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type Chris@17: * The entity type definition. Chris@17: */ Chris@17: protected function alterQueryForEntityType(Sql $query, EntityTypeInterface $entity_type) { Chris@17: /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ Chris@17: $table_mapping = $this->entityTypeManager->getStorage($entity_type->id())->getTableMapping(); Chris@17: $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); Chris@17: $dedicated_field_storage_definitions = array_filter($field_storage_definitions, function ($definition) use ($table_mapping) { Chris@17: return $table_mapping->requiresDedicatedTableStorage($definition); Chris@17: }); Chris@17: $dedicated_field_data_tables = array_map(function ($definition) use ($table_mapping) { Chris@17: return $table_mapping->getDedicatedDataTableName($definition); Chris@17: }, $dedicated_field_storage_definitions); Chris@17: Chris@17: $move_workspace_tables = []; Chris@17: $table_queue =& $query->getTableQueue(); Chris@17: foreach ($table_queue as $alias => &$table_info) { Chris@17: // If we reach the workspace_association array item before any candidates, Chris@17: // then we do not need to move it. Chris@17: if ($table_info['table'] == 'workspace_association') { Chris@17: break; Chris@17: } Chris@17: Chris@17: // Any dedicated field table is a candidate. Chris@17: if ($field_name = array_search($table_info['table'], $dedicated_field_data_tables, TRUE)) { Chris@17: $relationship = $table_info['relationship']; Chris@17: Chris@17: // There can be reverse relationships used. If so, Workspaces can't do Chris@17: // anything with them. Detect this and skip. Chris@17: if ($table_info['join']->field != 'entity_id') { Chris@17: continue; Chris@17: } Chris@17: Chris@17: // Get the dedicated revision table name. Chris@17: $new_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]); Chris@17: Chris@17: // Now add the workspace_association table. Chris@17: $workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship); Chris@17: Chris@17: // Update the join to use our COALESCE. Chris@17: $revision_field = $entity_type->getKey('revision'); Chris@17: $table_info['join']->leftTable = NULL; Chris@17: $table_info['join']->leftField = "COALESCE($workspace_association_table.target_entity_revision_id, $relationship.$revision_field)"; Chris@17: Chris@17: // Update the join and the table info to our new table name, and to join Chris@17: // on the revision key. Chris@17: $table_info['table'] = $new_table_name; Chris@17: $table_info['join']->table = $new_table_name; Chris@17: $table_info['join']->field = 'revision_id'; Chris@17: Chris@17: // Finally, if we added the workspace_association table we have to move Chris@17: // it in the table queue so that it comes before this field. Chris@17: if (empty($move_workspace_tables[$workspace_association_table])) { Chris@17: $move_workspace_tables[$workspace_association_table] = $alias; Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: // JOINs must be in order. i.e, any tables you mention in the ON clause of a Chris@17: // JOIN must appear prior to that JOIN. Since we're modifying a JOIN in Chris@17: // place, and adding a new table, we must ensure that the new table appears Chris@17: // prior to this one. So we recorded at what index we saw that table, and Chris@17: // then use array_splice() to move the workspace_association table join to Chris@17: // the correct position. Chris@17: foreach ($move_workspace_tables as $workspace_association_table => $alias) { Chris@17: $this->moveEntityTable($query, $workspace_association_table, $alias); Chris@17: } Chris@17: Chris@17: $base_entity_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable(); Chris@17: Chris@17: $base_fields = array_diff($table_mapping->getFieldNames($entity_type->getBaseTable()), [$entity_type->getKey('langcode')]); Chris@17: $revisionable_fields = array_diff($table_mapping->getFieldNames($entity_type->getRevisionDataTable()), $base_fields); Chris@17: Chris@17: // Go through and look to see if we have to modify fields and filters. Chris@17: foreach ($query->fields as &$field_info) { Chris@17: // Some fields don't actually have tables, meaning they're formulae and Chris@17: // whatnot. At this time we are going to ignore those. Chris@17: if (empty($field_info['table'])) { Chris@17: continue; Chris@17: } Chris@17: Chris@17: // Dereference the alias into the actual table. Chris@17: $table = $table_queue[$field_info['table']]['table']; Chris@17: if ($table == $base_entity_table && in_array($field_info['field'], $revisionable_fields)) { Chris@17: $relationship = $table_queue[$field_info['table']]['alias']; Chris@17: $alias = $this->ensureRevisionTable($entity_type, $query, $relationship); Chris@17: if ($alias) { Chris@17: // Change the base table to use the revision table instead. Chris@17: $field_info['table'] = $alias; Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: $relationships = []; Chris@17: // Build a list of all relationships that might be for our table. Chris@17: foreach ($query->relationships as $relationship => $info) { Chris@17: if ($info['base'] == $base_entity_table) { Chris@17: $relationships[] = $relationship; Chris@17: } Chris@17: } Chris@17: Chris@17: // Now we have to go through our where clauses and modify any of our fields. Chris@17: foreach ($query->where as &$clauses) { Chris@17: foreach ($clauses['conditions'] as &$where_info) { Chris@17: // Build a matrix of our possible relationships against fields we need Chris@17: // to switch. Chris@17: foreach ($relationships as $relationship) { Chris@17: foreach ($revisionable_fields as $field) { Chris@17: if (is_string($where_info['field']) && $where_info['field'] == "$relationship.$field") { Chris@17: $alias = $this->ensureRevisionTable($entity_type, $query, $relationship); Chris@17: if ($alias) { Chris@17: // Change the base table to use the revision table instead. Chris@17: $where_info['field'] = "$alias.$field"; Chris@17: } Chris@17: } Chris@17: } Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: // @todo Handle $query->orderby, $query->groupby, $query->having and Chris@17: // $query->count_field in https://www.drupal.org/node/2968165. Chris@17: } Chris@17: Chris@17: /** Chris@17: * Adds the 'workspace_association' table to a views query. Chris@17: * Chris@17: * @param string $entity_type_id Chris@17: * The ID of the entity type to join. Chris@17: * @param \Drupal\views\Plugin\views\query\Sql $query Chris@17: * The query plugin object for the query. Chris@17: * @param string $relationship Chris@17: * The primary table alias this table is related to. Chris@17: * Chris@17: * @return string Chris@17: * The alias of the 'workspace_association' table. Chris@17: */ Chris@17: protected function ensureWorkspaceAssociationTable($entity_type_id, Sql $query, $relationship) { Chris@17: if (isset($query->tables[$relationship]['workspace_association'])) { Chris@17: return $query->tables[$relationship]['workspace_association']['alias']; Chris@17: } Chris@17: Chris@17: $table_data = $this->viewsData->get($query->relationships[$relationship]['base']); Chris@17: Chris@17: // Construct the join. Chris@17: $definition = [ Chris@17: 'table' => 'workspace_association', Chris@17: 'field' => 'target_entity_id', Chris@17: 'left_table' => $relationship, Chris@17: 'left_field' => $table_data['table']['base']['field'], Chris@17: 'extra' => [ Chris@17: [ Chris@17: 'field' => 'target_entity_type_id', Chris@17: 'value' => $entity_type_id, Chris@17: ], Chris@17: [ Chris@17: 'field' => 'workspace', Chris@17: 'value' => $this->workspaceManager->getActiveWorkspace()->id(), Chris@17: ], Chris@17: ], Chris@17: 'type' => 'LEFT', Chris@17: ]; Chris@17: Chris@17: $join = $this->viewsJoinPluginManager->createInstance('standard', $definition); Chris@17: $join->adjusted = TRUE; Chris@17: Chris@17: return $query->queueTable('workspace_association', $relationship, $join); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Adds the revision table of an entity type to a query object. Chris@17: * Chris@17: * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type Chris@17: * The entity type definition. Chris@17: * @param \Drupal\views\Plugin\views\query\Sql $query Chris@17: * The query plugin object for the query. Chris@17: * @param string $relationship Chris@17: * The name of the relationship. Chris@17: * Chris@17: * @return string Chris@17: * The alias of the relationship. Chris@17: */ Chris@17: protected function ensureRevisionTable(EntityTypeInterface $entity_type, Sql $query, $relationship) { Chris@17: // Get the alias for the 'workspace_association' table we chain off of in Chris@17: // the COALESCE. Chris@17: $workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship); Chris@17: Chris@17: // Get the name of the revision table and revision key. Chris@17: $base_revision_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable(); Chris@17: $revision_field = $entity_type->getKey('revision'); Chris@17: Chris@17: // If the table was already added and has a join against the same field on Chris@17: // the revision table, reuse that rather than adding a new join. Chris@17: if (isset($query->tables[$relationship][$base_revision_table])) { Chris@17: $table_queue =& $query->getTableQueue(); Chris@17: $alias = $query->tables[$relationship][$base_revision_table]['alias']; Chris@17: if (isset($table_queue[$alias]['join']->field) && $table_queue[$alias]['join']->field == $revision_field) { Chris@17: // If this table previously existed, but was not added by us, we need Chris@17: // to modify the join and make sure that 'workspace_association' comes Chris@17: // first. Chris@17: if (empty($table_queue[$alias]['join']->workspace_adjusted)) { Chris@17: $table_queue[$alias]['join'] = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table); Chris@17: // We also have to ensure that our 'workspace_association' comes before Chris@17: // this. Chris@17: $this->moveEntityTable($query, $workspace_association_table, $alias); Chris@17: } Chris@17: Chris@17: return $alias; Chris@17: } Chris@17: } Chris@17: Chris@17: // Construct a new join. Chris@17: $join = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table); Chris@17: return $query->queueTable($base_revision_table, $relationship, $join); Chris@17: } Chris@17: Chris@17: /** Chris@17: * Fetches a join for a revision table using the workspace_association table. Chris@17: * Chris@17: * @param string $relationship Chris@17: * The relationship to use in the view. Chris@17: * @param string $table Chris@17: * The table name. Chris@17: * @param string $field Chris@17: * The field to join on. Chris@17: * @param string $workspace_association_table Chris@17: * The alias of the 'workspace_association' table joined to the main entity Chris@17: * table. Chris@17: * Chris@17: * @return \Drupal\views\Plugin\views\join\JoinPluginInterface Chris@17: * An adjusted views join object to add to the query. Chris@17: */ Chris@17: protected function getRevisionTableJoin($relationship, $table, $field, $workspace_association_table) { Chris@17: $definition = [ Chris@17: 'table' => $table, Chris@17: 'field' => $field, Chris@17: // Making this explicitly null allows the left table to be a formula. Chris@17: 'left_table' => NULL, Chris@17: 'left_field' => "COALESCE($workspace_association_table.target_entity_revision_id, $relationship.$field)", Chris@17: ]; Chris@17: Chris@17: /** @var \Drupal\views\Plugin\views\join\JoinPluginInterface $join */ Chris@17: $join = $this->viewsJoinPluginManager->createInstance('standard', $definition); Chris@17: $join->adjusted = TRUE; Chris@17: $join->workspace_adjusted = TRUE; Chris@17: Chris@17: return $join; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Moves a 'workspace_association' table to appear before the given alias. Chris@17: * Chris@17: * Because Workspace chains possibly pre-existing tables onto the Chris@17: * 'workspace_association' table, we have to ensure that the Chris@17: * 'workspace_association' table appears in the query before the alias it's Chris@17: * chained on or the SQL is invalid. Chris@17: * Chris@17: * @param \Drupal\views\Plugin\views\query\Sql $query Chris@17: * The SQL query object. Chris@17: * @param string $workspace_association_table Chris@17: * The alias of the 'workspace_association' table. Chris@17: * @param string $alias Chris@17: * The alias of the table it needs to appear before. Chris@17: */ Chris@17: protected function moveEntityTable(Sql $query, $workspace_association_table, $alias) { Chris@17: $table_queue =& $query->getTableQueue(); Chris@17: $keys = array_keys($table_queue); Chris@17: $current_index = array_search($workspace_association_table, $keys); Chris@17: $index = array_search($alias, $keys); Chris@17: Chris@17: // If it's already before our table, we don't need to move it, as we could Chris@17: // accidentally move it forward. Chris@17: if ($current_index < $index) { Chris@17: return; Chris@17: } Chris@17: $splice = [$workspace_association_table => $table_queue[$workspace_association_table]]; Chris@17: unset($table_queue[$workspace_association_table]); Chris@17: Chris@17: // Now move the item to the proper location in the array. Don't use Chris@17: // array_splice() because that breaks indices. Chris@17: $table_queue = array_slice($table_queue, 0, $index, TRUE) + Chris@17: $splice + Chris@17: array_slice($table_queue, $index, NULL, TRUE); Chris@17: } Chris@17: Chris@17: }