Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / Entity / Query / Sql / Tables.php
1 <?php
2
3 namespace Drupal\Core\Entity\Query\Sql;
4
5 use Drupal\Core\Database\Query\SelectInterface;
6 use Drupal\Core\Entity\EntityType;
7 use Drupal\Core\Entity\Query\QueryException;
8 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
9 use Drupal\Core\Entity\Sql\TableMappingInterface;
10 use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
11 use Drupal\Core\Field\FieldStorageDefinitionInterface;
12 use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
13
14 /**
15  * Adds tables and fields to the SQL entity query.
16  */
17 class Tables implements TablesInterface {
18
19   /**
20    * @var \Drupal\Core\Database\Query\SelectInterface
21    */
22   protected $sqlQuery;
23
24   /**
25    * Entity table array.
26    *
27    * This array contains at most two entries: one for the data, one for the
28    * properties. Its keys are unique references to the tables, values are
29    * aliases.
30    *
31    * @see \Drupal\Core\Entity\Query\Sql\Tables::ensureEntityTable().
32    *
33    * @var array
34    */
35   protected $entityTables = [];
36
37   /**
38    * Field table array, key is table name, value is alias.
39    *
40    * This array contains one entry per field table.
41    *
42    * @var array
43    */
44   protected $fieldTables = [];
45
46   /**
47    * The entity manager.
48    *
49    * @var \Drupal\Core\Entity\EntityManager
50    */
51   protected $entityManager;
52
53   /**
54    * List of case sensitive fields.
55    *
56    * @var array
57    */
58   protected $caseSensitiveFields = [];
59
60   /**
61    * @param \Drupal\Core\Database\Query\SelectInterface $sql_query
62    */
63   public function __construct(SelectInterface $sql_query) {
64     $this->sqlQuery = $sql_query;
65     $this->entityManager = \Drupal::entityManager();
66   }
67
68   /**
69    * {@inheritdoc}
70    */
71   public function addField($field, $type, $langcode) {
72     $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
73     $all_revisions = $this->sqlQuery->getMetaData('all_revisions');
74     // This variable ensures grouping works correctly. For example:
75     // ->condition('tags', 2, '>')
76     // ->condition('tags', 20, '<')
77     // ->condition('node_reference.nid.entity.tags', 2)
78     // The first two should use the same table but the last one needs to be a
79     // new table. So for the first two, the table array index will be 'tags'
80     // while the third will be 'node_reference.nid.tags'.
81     $index_prefix = '';
82     $specifiers = explode('.', $field);
83     $base_table = 'base_table';
84     $count = count($specifiers) - 1;
85     // This will contain the definitions of the last specifier seen by the
86     // system.
87     $propertyDefinitions = [];
88     $entity_type = $this->entityManager->getDefinition($entity_type_id);
89
90     $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
91     for ($key = 0; $key <= $count; $key++) {
92       // This can either be the name of an entity base field or a configurable
93       // field.
94       $specifier = $specifiers[$key];
95       if (isset($field_storage_definitions[$specifier])) {
96         $field_storage = $field_storage_definitions[$specifier];
97         $column = $field_storage->getMainPropertyName();
98       }
99       else {
100         $field_storage = FALSE;
101         $column = NULL;
102       }
103
104       // If there is revision support, only the current revisions are being
105       // queried, and the field is revisionable then use the revision id.
106       // Otherwise, the entity id will do.
107       if (($revision_key = $entity_type->getKey('revision')) && $all_revisions && $field_storage && $field_storage->isRevisionable()) {
108         // This contains the relevant SQL field to be used when joining entity
109         // tables.
110         $entity_id_field = $revision_key;
111         // This contains the relevant SQL field to be used when joining field
112         // tables.
113         $field_id_field = 'revision_id';
114       }
115       else {
116         $entity_id_field = $entity_type->getKey('id');
117         $field_id_field = 'entity_id';
118       }
119
120       /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
121       $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
122
123       // Check whether this field is stored in a dedicated table.
124       if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
125         $delta = NULL;
126
127         if ($key < $count) {
128           $next = $specifiers[$key + 1];
129           // If this is a numeric specifier we're adding a condition on the
130           // specific delta.
131           if (is_numeric($next)) {
132             $delta = $next;
133             $index_prefix .= ".$delta";
134             // Do not process it again.
135             $key++;
136             $next = $specifiers[$key + 1];
137           }
138           // If this specifier is the reserved keyword "%delta" we're adding a
139           // condition on a delta range.
140           elseif ($next == TableMappingInterface::DELTA) {
141             $index_prefix .= TableMappingInterface::DELTA;
142             // Do not process it again.
143             $key++;
144             // If there are more specifiers to work with then continue
145             // processing. If this is the last specifier then use the reserved
146             // keyword as a column name.
147             if ($key < $count) {
148               $next = $specifiers[$key + 1];
149             }
150             else {
151               $column = TableMappingInterface::DELTA;
152             }
153           }
154           // Is this a field column?
155           $columns = $field_storage->getColumns();
156           if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
157             // Use it.
158             $column = $next;
159             // Do not process it again.
160             $key++;
161           }
162           // If there are more specifiers, the next one must be a
163           // relationship. Either the field name followed by a relationship
164           // specifier, for example $node->field_image->entity. Or a field
165           // column followed by a relationship specifier, for example
166           // $node->field_image->fid->entity. In both cases, prepare the
167           // property definitions for the relationship. In the first case,
168           // also use the property definitions for column.
169           if ($key < $count) {
170             $relationship_specifier = $specifiers[$key + 1];
171             $propertyDefinitions = $field_storage->getPropertyDefinitions();
172
173             // Prepare the next index prefix.
174             $next_index_prefix = "$relationship_specifier.$column";
175           }
176         }
177         $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta);
178         $sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
179       }
180       // The field is stored in a shared table.
181       else {
182         // ensureEntityTable() decides whether an entity property will be
183         // queried from the data table or the base table based on where it
184         // finds the property first. The data table is preferred, which is why
185         // it gets added before the base table.
186         $entity_tables = [];
187         $revision_table = NULL;
188         if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
189           $data_table = $entity_type->getRevisionDataTable();
190           $entity_base_table = $entity_type->getRevisionTable();
191         }
192         else {
193           $data_table = $entity_type->getDataTable();
194           $entity_base_table = $entity_type->getBaseTable();
195
196           if ($field_storage && $field_storage->isRevisionable() && in_array($field_storage->getName(), $entity_type->getRevisionMetadataKeys())) {
197             $revision_table = $entity_type->getRevisionTable();
198           }
199         }
200         if ($data_table) {
201           $this->sqlQuery->addMetaData('simple_query', FALSE);
202           $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
203         }
204         if ($revision_table) {
205           $entity_tables[$revision_table] = $this->getTableMapping($revision_table, $entity_type_id);
206         }
207         $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
208         $sql_column = $specifier;
209
210         // If there are more specifiers, get the right sql column name if the
211         // next one is a column of this field.
212         if ($key < $count) {
213           $next = $specifiers[$key + 1];
214           // If this specifier is the reserved keyword "%delta" we're adding a
215           // condition on a delta range.
216           if ($next == TableMappingInterface::DELTA) {
217             $key++;
218             if ($key < $count) {
219               $next = $specifiers[$key + 1];
220             }
221             else {
222               return 0;
223             }
224           }
225           // If this is a numeric specifier we're adding a condition on the
226           // specific delta. Since we know that this is a single value base
227           // field no other value than 0 makes sense.
228           if (is_numeric($next)) {
229             if ($next > 0) {
230               $this->sqlQuery->condition('1 <> 1');
231             }
232             $key++;
233             $next = $specifiers[$key + 1];
234           }
235           // Is this a field column?
236           $columns = $field_storage->getColumns();
237           if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
238             // Use it.
239             $sql_column = $table_mapping->getFieldColumnName($field_storage, $next);
240             // Do not process it again.
241             $key++;
242           }
243         }
244
245         $table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
246       }
247
248       // If there is a field storage (some specifiers are not) and a field
249       // column, check for case sensitivity.
250       if ($field_storage && $column) {
251         $property_definitions = $field_storage->getPropertyDefinitions();
252         if (isset($property_definitions[$column])) {
253           $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
254         }
255       }
256
257       // If there are more specifiers to come, it's a relationship.
258       if ($field_storage && $key < $count) {
259         // Computed fields have prepared their property definition already, do
260         // it for properties as well.
261         if (!$propertyDefinitions) {
262           $propertyDefinitions = $field_storage->getPropertyDefinitions();
263           $relationship_specifier = $specifiers[$key + 1];
264           $next_index_prefix = $relationship_specifier;
265         }
266         $entity_type_id = NULL;
267         // Relationship specifier can also contain the entity type ID, i.e.
268         // entity:node, entity:user or entity:taxonomy.
269         if (strpos($relationship_specifier, ':') !== FALSE) {
270           list($relationship_specifier, $entity_type_id) = explode(':', $relationship_specifier, 2);
271         }
272         // Check for a valid relationship.
273         if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) {
274           // If it is, use the entity type if specified already, otherwise use
275           // the definition.
276           $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition();
277           if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) {
278             $entity_type_id = $target_definition->getEntityTypeId();
279           }
280           $entity_type = $this->entityManager->getDefinition($entity_type_id);
281           $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
282           // Add the new entity base table using the table and sql column.
283           $base_table = $this->addNextBaseTable($entity_type, $table, $sql_column, $field_storage);
284           $propertyDefinitions = [];
285           $key++;
286           $index_prefix .= "$next_index_prefix.";
287         }
288         else {
289           throw new QueryException("Invalid specifier '$relationship_specifier'");
290         }
291       }
292     }
293     return "$table.$sql_column";
294   }
295
296   /**
297    * {@inheritdoc}
298    */
299   public function isFieldCaseSensitive($field_name) {
300     if (isset($this->caseSensitiveFields[$field_name])) {
301       return $this->caseSensitiveFields[$field_name];
302     }
303   }
304
305   /**
306    * Joins the entity table, if necessary, and returns the alias for it.
307    *
308    * @param string $index_prefix
309    *   The table array index prefix. For a base table this will be empty,
310    *   for a target entity reference like 'field_tags.entity:taxonomy_term.name'
311    *   this will be 'entity:taxonomy_term.target_id.'.
312    * @param string $property
313    *   The field property/column.
314    * @param string $type
315    *   The join type, can either be INNER or LEFT.
316    * @param string $langcode
317    *   The langcode we use on the join.
318    * @param string $base_table
319    *   The table to join to. It can be either the table name, its alias or the
320    *   'base_table' placeholder.
321    * @param string $id_field
322    *   The name of the ID field/property for the current entity. For instance:
323    *   tid, nid, etc.
324    * @param array $entity_tables
325    *   Array of entity tables (data and base tables) where decide the entity
326    *   property will be queried from. The first table containing the property
327    *   will be used, so the order is important and the data table is always
328    *   preferred.
329    *
330    * @return string
331    *   The alias of the joined table.
332    *
333    * @throws \Drupal\Core\Entity\Query\QueryException
334    *   When an invalid property has been passed.
335    */
336   protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
337     foreach ($entity_tables as $table => $mapping) {
338       if (isset($mapping[$property])) {
339         // Ensure a table joined multiple times through different index prefixes
340         // has unique entityTables entries by concatenating the index prefix
341         // and the base table alias. In this way i.e. if we join to the same
342         // entity table several times for different entity reference fields,
343         // each join gets a separate alias.
344         $key = $index_prefix . ($base_table === 'base_table' ? $table : $base_table);
345         if (!isset($this->entityTables[$key])) {
346           $this->entityTables[$key] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
347         }
348         return $this->entityTables[$key];
349       }
350     }
351     throw new QueryException("'$property' not found");
352   }
353
354   /**
355    * Join field table if necessary.
356    *
357    * @param $field_name
358    *   Name of the field.
359    * @return string
360    * @throws \Drupal\Core\Entity\Query\QueryException
361    */
362   protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) {
363     $field_name = $field->getName();
364     if (!isset($this->fieldTables[$index_prefix . $field_name])) {
365       $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
366       /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
367       $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
368       $table = !$this->sqlQuery->getMetaData('all_revisions') ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
369       if ($field->getCardinality() != 1) {
370         $this->sqlQuery->addMetaData('simple_query', FALSE);
371       }
372       $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta);
373     }
374     return $this->fieldTables[$index_prefix . $field_name];
375   }
376
377   /**
378    * Adds a join to a given table.
379    *
380    * @param string $type
381    *   The join type.
382    * @param string $table
383    *   The table to join to.
384    * @param string $join_condition
385    *   The condition on which to join to.
386    * @param string $langcode
387    *   The langcode we use on the join.
388    * @param string|null $delta
389    *   (optional) A delta which should be used as additional condition.
390    *
391    * @return string
392    *   Returns the alias of the joined table.
393    */
394   protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
395     $arguments = [];
396     if ($langcode) {
397       $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
398       $entity_type = $this->entityManager->getDefinition($entity_type_id);
399       // Only the data table follows the entity language key, dedicated field
400       // tables have an hard-coded 'langcode' column.
401       $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode';
402       $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder();
403       $join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder;
404       $arguments[$placeholder] = $langcode;
405     }
406     if (isset($delta)) {
407       $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder();
408       $join_condition .= ' AND %alias.delta = ' . $placeholder;
409       $arguments[$placeholder] = $delta;
410     }
411     return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
412   }
413
414   /**
415    * Gets the schema for the given table.
416    *
417    * @param string $table
418    *   The table name.
419    *
420    * @return array|false
421    *   An associative array of table field mapping for the given table, keyed by
422    *   columns name and values are just incrementing integers. If the table
423    *   mapping is not available, FALSE is returned.
424    */
425   protected function getTableMapping($table, $entity_type_id) {
426     $storage = $this->entityManager->getStorage($entity_type_id);
427     if ($storage instanceof SqlEntityStorageInterface) {
428       $mapping = $storage->getTableMapping()->getAllColumns($table);
429     }
430     else {
431       return FALSE;
432     }
433     return array_flip($mapping);
434   }
435
436   /**
437    * Add the next entity base table.
438    *
439    * For example, when building the SQL query for
440    * @code
441    * condition('uid.entity.name', 'foo', 'CONTAINS')
442    * @endcode
443    *
444    * this adds the users table.
445    *
446    * @param \Drupal\Core\Entity\EntityType $entity_type
447    *   The entity type being joined, in the above example, User.
448    * @param string $table
449    *   This is the table being joined, in the above example, {users}.
450    * @param string $sql_column
451    *   This is the SQL column in the existing table being joined to.
452    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage
453    *   The field storage definition for the field referencing this column.
454    *
455    * @return string
456    *   The alias of the next entity table joined in.
457    */
458   protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column, FieldStorageDefinitionInterface $field_storage) {
459     $join_condition = '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column";
460     return $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);
461   }
462
463 }