8ebf289d7cc47e915c87fd2b1121d31058bf66e0
[yaffs-website] / web / core / modules / workspaces / src / ViewsQueryAlter.php
1 <?php
2
3 namespace Drupal\workspaces;
4
5 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
6 use Drupal\Core\Entity\EntityFieldManagerInterface;
7 use Drupal\Core\Entity\EntityTypeInterface;
8 use Drupal\Core\Entity\EntityTypeManagerInterface;
9 use Drupal\views\Plugin\views\query\QueryPluginBase;
10 use Drupal\views\Plugin\views\query\Sql;
11 use Drupal\views\Plugin\ViewsHandlerManager;
12 use Drupal\views\ViewExecutable;
13 use Drupal\views\ViewsData;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15
16 /**
17  * Defines a class for altering views queries.
18  *
19  * @internal
20  */
21 class ViewsQueryAlter implements ContainerInjectionInterface {
22
23   /**
24    * The entity type manager service.
25    *
26    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
27    */
28   protected $entityTypeManager;
29
30   /**
31    * The entity field manager.
32    *
33    * @var \Drupal\Core\Entity\EntityFieldManagerInterface
34    */
35   protected $entityFieldManager;
36
37   /**
38    * The workspace manager service.
39    *
40    * @var \Drupal\workspaces\WorkspaceManagerInterface
41    */
42   protected $workspaceManager;
43
44   /**
45    * The views data.
46    *
47    * @var \Drupal\views\ViewsData
48    */
49   protected $viewsData;
50
51   /**
52    * A plugin manager which handles instances of views join plugins.
53    *
54    * @var \Drupal\views\Plugin\ViewsHandlerManager
55    */
56   protected $viewsJoinPluginManager;
57
58   /**
59    * Constructs a new ViewsQueryAlter instance.
60    *
61    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
62    *   The entity type manager service.
63    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
64    *   The entity field manager.
65    * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
66    *   The workspace manager service.
67    * @param \Drupal\views\ViewsData $views_data
68    *   The views data.
69    * @param \Drupal\views\Plugin\ViewsHandlerManager $views_join_plugin_manager
70    *   The views join plugin manager.
71    */
72   public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, WorkspaceManagerInterface $workspace_manager, ViewsData $views_data, ViewsHandlerManager $views_join_plugin_manager) {
73     $this->entityTypeManager = $entity_type_manager;
74     $this->entityFieldManager = $entity_field_manager;
75     $this->workspaceManager = $workspace_manager;
76     $this->viewsData = $views_data;
77     $this->viewsJoinPluginManager = $views_join_plugin_manager;
78   }
79
80   /**
81    * {@inheritdoc}
82    */
83   public static function create(ContainerInterface $container) {
84     return new static(
85       $container->get('entity_type.manager'),
86       $container->get('entity_field.manager'),
87       $container->get('workspaces.manager'),
88       $container->get('views.views_data'),
89       $container->get('plugin.manager.views.join')
90     );
91   }
92
93   /**
94    * Implements a hook bridge for hook_views_query_alter().
95    *
96    * @see hook_views_query_alter()
97    */
98   public function alterQuery(ViewExecutable $view, QueryPluginBase $query) {
99     // Don't alter any views queries if we're in the default workspace.
100     if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
101       return;
102     }
103
104     // Don't alter any non-sql views queries.
105     if (!$query instanceof Sql) {
106       return;
107     }
108
109     // Find out what entity types are represented in this query.
110     $entity_type_ids = [];
111     foreach ($query->relationships as $info) {
112       $table_data = $this->viewsData->get($info['base']);
113       if (empty($table_data['table']['entity type'])) {
114         continue;
115       }
116       $entity_type_id = $table_data['table']['entity type'];
117       // This construct ensures each entity type exists only once.
118       $entity_type_ids[$entity_type_id] = $entity_type_id;
119     }
120
121     $entity_type_definitions = $this->entityTypeManager->getDefinitions();
122     foreach ($entity_type_ids as $entity_type_id) {
123       if ($this->workspaceManager->isEntityTypeSupported($entity_type_definitions[$entity_type_id])) {
124         $this->alterQueryForEntityType($query, $entity_type_definitions[$entity_type_id]);
125       }
126     }
127   }
128
129   /**
130    * Alters the entity type tables for a Views query.
131    *
132    * This should only be called after determining that this entity type is
133    * involved in the query, and that a non-default workspace is in use.
134    *
135    * @param \Drupal\views\Plugin\views\query\Sql $query
136    *   The query plugin object for the query.
137    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
138    *   The entity type definition.
139    */
140   protected function alterQueryForEntityType(Sql $query, EntityTypeInterface $entity_type) {
141     /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
142     $table_mapping = $this->entityTypeManager->getStorage($entity_type->id())->getTableMapping();
143     $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
144     $dedicated_field_storage_definitions = array_filter($field_storage_definitions, function ($definition) use ($table_mapping) {
145       return $table_mapping->requiresDedicatedTableStorage($definition);
146     });
147     $dedicated_field_data_tables = array_map(function ($definition) use ($table_mapping) {
148       return $table_mapping->getDedicatedDataTableName($definition);
149     }, $dedicated_field_storage_definitions);
150
151     $move_workspace_tables = [];
152     $table_queue =& $query->getTableQueue();
153     foreach ($table_queue as $alias => &$table_info) {
154       // If we reach the workspace_association array item before any candidates,
155       // then we do not need to move it.
156       if ($table_info['table'] == 'workspace_association') {
157         break;
158       }
159
160       // Any dedicated field table is a candidate.
161       if ($field_name = array_search($table_info['table'], $dedicated_field_data_tables, TRUE)) {
162         $relationship = $table_info['relationship'];
163
164         // There can be reverse relationships used. If so, Workspaces can't do
165         // anything with them. Detect this and skip.
166         if ($table_info['join']->field != 'entity_id') {
167           continue;
168         }
169
170         // Get the dedicated revision table name.
171         $new_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]);
172
173         // Now add the workspace_association table.
174         $workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship);
175
176         // Update the join to use our COALESCE.
177         $revision_field = $entity_type->getKey('revision');
178         $table_info['join']->leftTable = NULL;
179         $table_info['join']->leftField = "COALESCE($workspace_association_table.target_entity_revision_id, $relationship.$revision_field)";
180
181         // Update the join and the table info to our new table name, and to join
182         // on the revision key.
183         $table_info['table'] = $new_table_name;
184         $table_info['join']->table = $new_table_name;
185         $table_info['join']->field = 'revision_id';
186
187         // Finally, if we added the workspace_association table we have to move
188         // it in the table queue so that it comes before this field.
189         if (empty($move_workspace_tables[$workspace_association_table])) {
190           $move_workspace_tables[$workspace_association_table] = $alias;
191         }
192       }
193     }
194
195     // JOINs must be in order. i.e, any tables you mention in the ON clause of a
196     // JOIN must appear prior to that JOIN. Since we're modifying a JOIN in
197     // place, and adding a new table, we must ensure that the new table appears
198     // prior to this one. So we recorded at what index we saw that table, and
199     // then use array_splice() to move the workspace_association table join to
200     // the correct position.
201     foreach ($move_workspace_tables as $workspace_association_table => $alias) {
202       $this->moveEntityTable($query, $workspace_association_table, $alias);
203     }
204
205     $base_entity_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable();
206
207     $base_fields = array_diff($table_mapping->getFieldNames($entity_type->getBaseTable()), [$entity_type->getKey('langcode')]);
208     $revisionable_fields = array_diff($table_mapping->getFieldNames($entity_type->getRevisionDataTable()), $base_fields);
209
210     // Go through and look to see if we have to modify fields and filters.
211     foreach ($query->fields as &$field_info) {
212       // Some fields don't actually have tables, meaning they're formulae and
213       // whatnot. At this time we are going to ignore those.
214       if (empty($field_info['table'])) {
215         continue;
216       }
217
218       // Dereference the alias into the actual table.
219       $table = $table_queue[$field_info['table']]['table'];
220       if ($table == $base_entity_table && in_array($field_info['field'], $revisionable_fields)) {
221         $relationship = $table_queue[$field_info['table']]['alias'];
222         $alias = $this->ensureRevisionTable($entity_type, $query, $relationship);
223         if ($alias) {
224           // Change the base table to use the revision table instead.
225           $field_info['table'] = $alias;
226         }
227       }
228     }
229
230     $relationships = [];
231     // Build a list of all relationships that might be for our table.
232     foreach ($query->relationships as $relationship => $info) {
233       if ($info['base'] == $base_entity_table) {
234         $relationships[] = $relationship;
235       }
236     }
237
238     // Now we have to go through our where clauses and modify any of our fields.
239     foreach ($query->where as &$clauses) {
240       foreach ($clauses['conditions'] as &$where_info) {
241         // Build a matrix of our possible relationships against fields we need
242         // to switch.
243         foreach ($relationships as $relationship) {
244           foreach ($revisionable_fields as $field) {
245             if (is_string($where_info['field']) && $where_info['field'] == "$relationship.$field") {
246               $alias = $this->ensureRevisionTable($entity_type, $query, $relationship);
247               if ($alias) {
248                 // Change the base table to use the revision table instead.
249                 $where_info['field'] = "$alias.$field";
250               }
251             }
252           }
253         }
254       }
255     }
256
257     // @todo Handle $query->orderby, $query->groupby, $query->having and
258     //   $query->count_field in https://www.drupal.org/node/2968165.
259   }
260
261   /**
262    * Adds the 'workspace_association' table to a views query.
263    *
264    * @param string $entity_type_id
265    *   The ID of the entity type to join.
266    * @param \Drupal\views\Plugin\views\query\Sql $query
267    *   The query plugin object for the query.
268    * @param string $relationship
269    *   The primary table alias this table is related to.
270    *
271    * @return string
272    *   The alias of the 'workspace_association' table.
273    */
274   protected function ensureWorkspaceAssociationTable($entity_type_id, Sql $query, $relationship) {
275     if (isset($query->tables[$relationship]['workspace_association'])) {
276       return $query->tables[$relationship]['workspace_association']['alias'];
277     }
278
279     $table_data = $this->viewsData->get($query->relationships[$relationship]['base']);
280
281     // Construct the join.
282     $definition = [
283       'table' => 'workspace_association',
284       'field' => 'target_entity_id',
285       'left_table' => $relationship,
286       'left_field' => $table_data['table']['base']['field'],
287       'extra' => [
288         [
289           'field' => 'target_entity_type_id',
290           'value' => $entity_type_id,
291         ],
292         [
293           'field' => 'workspace',
294           'value' => $this->workspaceManager->getActiveWorkspace()->id(),
295         ],
296       ],
297       'type' => 'LEFT',
298     ];
299
300     $join = $this->viewsJoinPluginManager->createInstance('standard', $definition);
301     $join->adjusted = TRUE;
302
303     return $query->queueTable('workspace_association', $relationship, $join);
304   }
305
306   /**
307    * Adds the revision table of an entity type to a query object.
308    *
309    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
310    *   The entity type definition.
311    * @param \Drupal\views\Plugin\views\query\Sql $query
312    *   The query plugin object for the query.
313    * @param string $relationship
314    *   The name of the relationship.
315    *
316    * @return string
317    *   The alias of the relationship.
318    */
319   protected function ensureRevisionTable(EntityTypeInterface $entity_type, Sql $query, $relationship) {
320     // Get the alias for the 'workspace_association' table we chain off of in
321     // the COALESCE.
322     $workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship);
323
324     // Get the name of the revision table and revision key.
325     $base_revision_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable();
326     $revision_field = $entity_type->getKey('revision');
327
328     // If the table was already added and has a join against the same field on
329     // the revision table, reuse that rather than adding a new join.
330     if (isset($query->tables[$relationship][$base_revision_table])) {
331       $table_queue =& $query->getTableQueue();
332       $alias = $query->tables[$relationship][$base_revision_table]['alias'];
333       if (isset($table_queue[$alias]['join']->field) && $table_queue[$alias]['join']->field == $revision_field) {
334         // If this table previously existed, but was not added by us, we need
335         // to modify the join and make sure that 'workspace_association' comes
336         // first.
337         if (empty($table_queue[$alias]['join']->workspace_adjusted)) {
338           $table_queue[$alias]['join'] = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table);
339           // We also have to ensure that our 'workspace_association' comes before
340           // this.
341           $this->moveEntityTable($query, $workspace_association_table, $alias);
342         }
343
344         return $alias;
345       }
346     }
347
348     // Construct a new join.
349     $join = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table);
350     return $query->queueTable($base_revision_table, $relationship, $join);
351   }
352
353   /**
354    * Fetches a join for a revision table using the workspace_association table.
355    *
356    * @param string $relationship
357    *   The relationship to use in the view.
358    * @param string $table
359    *   The table name.
360    * @param string $field
361    *   The field to join on.
362    * @param string $workspace_association_table
363    *   The alias of the 'workspace_association' table joined to the main entity
364    *   table.
365    *
366    * @return \Drupal\views\Plugin\views\join\JoinPluginInterface
367    *   An adjusted views join object to add to the query.
368    */
369   protected function getRevisionTableJoin($relationship, $table, $field, $workspace_association_table) {
370     $definition = [
371       'table' => $table,
372       'field' => $field,
373       // Making this explicitly null allows the left table to be a formula.
374       'left_table' => NULL,
375       'left_field' => "COALESCE($workspace_association_table.target_entity_revision_id, $relationship.$field)",
376     ];
377
378     /** @var \Drupal\views\Plugin\views\join\JoinPluginInterface $join */
379     $join = $this->viewsJoinPluginManager->createInstance('standard', $definition);
380     $join->adjusted = TRUE;
381     $join->workspace_adjusted = TRUE;
382
383     return $join;
384   }
385
386   /**
387    * Moves a 'workspace_association' table to appear before the given alias.
388    *
389    * Because Workspace chains possibly pre-existing tables onto the
390    * 'workspace_association' table, we have to ensure that the
391    * 'workspace_association' table appears in the query before the alias it's
392    * chained on or the SQL is invalid.
393    *
394    * @param \Drupal\views\Plugin\views\query\Sql $query
395    *   The SQL query object.
396    * @param string $workspace_association_table
397    *   The alias of the 'workspace_association' table.
398    * @param string $alias
399    *   The alias of the table it needs to appear before.
400    */
401   protected function moveEntityTable(Sql $query, $workspace_association_table, $alias) {
402     $table_queue =& $query->getTableQueue();
403     $keys = array_keys($table_queue);
404     $current_index = array_search($workspace_association_table, $keys);
405     $index = array_search($alias, $keys);
406
407     // If it's already before our table, we don't need to move it, as we could
408     // accidentally move it forward.
409     if ($current_index < $index) {
410       return;
411     }
412     $splice = [$workspace_association_table => $table_queue[$workspace_association_table]];
413     unset($table_queue[$workspace_association_table]);
414
415     // Now move the item to the proper location in the array. Don't use
416     // array_splice() because that breaks indices.
417     $table_queue = array_slice($table_queue, 0, $index, TRUE) +
418       $splice +
419       array_slice($table_queue, $index, NULL, TRUE);
420   }
421
422 }