0c54dc45884f494c8e1c101b5021dddfa7951e87
[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       }
98       else {
99         $field_storage = FALSE;
100       }
101
102       // If there is revision support, only the current revisions are being
103       // queried, and the field is revisionable then use the revision id.
104       // Otherwise, the entity id will do.
105       if (($revision_key = $entity_type->getKey('revision')) && $all_revisions && $field_storage && $field_storage->isRevisionable()) {
106         // This contains the relevant SQL field to be used when joining entity
107         // tables.
108         $entity_id_field = $revision_key;
109         // This contains the relevant SQL field to be used when joining field
110         // tables.
111         $field_id_field = 'revision_id';
112       }
113       else {
114         $entity_id_field = $entity_type->getKey('id');
115         $field_id_field = 'entity_id';
116       }
117
118       /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
119       $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
120
121       // Check whether this field is stored in a dedicated table.
122       if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
123         $delta = NULL;
124         // Find the field column.
125         $column = $field_storage->getMainPropertyName();
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         $property_definitions = $field_storage->getPropertyDefinitions();
180         if (isset($property_definitions[$column])) {
181           $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
182         }
183       }
184       // The field is stored in a shared table.
185       else {
186         // ensureEntityTable() decides whether an entity property will be
187         // queried from the data table or the base table based on where it
188         // finds the property first. The data table is preferred, which is why
189         // it gets added before the base table.
190         $entity_tables = [];
191         if ($all_revisions && $field_storage && $field_storage->isRevisionable()) {
192           $data_table = $entity_type->getRevisionDataTable();
193           $entity_base_table = $entity_type->getRevisionTable();
194         }
195         else {
196           $data_table = $entity_type->getDataTable();
197           $entity_base_table = $entity_type->getBaseTable();
198         }
199         if ($data_table) {
200           $this->sqlQuery->addMetaData('simple_query', FALSE);
201           $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
202         }
203         $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
204         $sql_column = $specifier;
205
206         // If there are more specifiers, get the right sql column name if the
207         // next one is a column of this field.
208         if ($key < $count) {
209           $next = $specifiers[$key + 1];
210           // If this specifier is the reserved keyword "%delta" we're adding a
211           // condition on a delta range.
212           if ($next == TableMappingInterface::DELTA) {
213             $key++;
214             if ($key < $count) {
215               $next = $specifiers[$key + 1];
216             }
217             else {
218               return 0;
219             }
220           }
221           // If this is a numeric specifier we're adding a condition on the
222           // specific delta. Since we know that this is a single value base
223           // field no other value than 0 makes sense.
224           if (is_numeric($next)) {
225             if ($next > 0) {
226               $this->sqlQuery->condition('1 <> 1');
227             }
228             $key++;
229             $next = $specifiers[$key + 1];
230           }
231           // Is this a field column?
232           $columns = $field_storage->getColumns();
233           if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
234             // Use it.
235             $sql_column = $table_mapping->getFieldColumnName($field_storage, $next);
236             // Do not process it again.
237             $key++;
238           }
239         }
240
241         $table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
242
243         // If there is a field storage (some specifiers are not), check for case
244         // sensitivity.
245         if ($field_storage) {
246           $column = $field_storage->getMainPropertyName();
247           $base_field_property_definitions = $field_storage->getPropertyDefinitions();
248           if (isset($base_field_property_definitions[$column])) {
249             $this->caseSensitiveFields[$field] = $base_field_property_definitions[$column]->getSetting('case_sensitive');
250           }
251         }
252
253       }
254       // If there are more specifiers to come, it's a relationship.
255       if ($field_storage && $key < $count) {
256         // Computed fields have prepared their property definition already, do
257         // it for properties as well.
258         if (!$propertyDefinitions) {
259           $propertyDefinitions = $field_storage->getPropertyDefinitions();
260           $relationship_specifier = $specifiers[$key + 1];
261           $next_index_prefix = $relationship_specifier;
262         }
263         $entity_type_id = NULL;
264         // Relationship specifier can also contain the entity type ID, i.e.
265         // entity:node, entity:user or entity:taxonomy.
266         if (strpos($relationship_specifier, ':') !== FALSE) {
267           list($relationship_specifier, $entity_type_id) = explode(':', $relationship_specifier, 2);
268         }
269         // Check for a valid relationship.
270         if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) {
271           // If it is, use the entity type if specified already, otherwise use
272           // the definition.
273           $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition();
274           if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) {
275             $entity_type_id = $target_definition->getEntityTypeId();
276           }
277           $entity_type = $this->entityManager->getDefinition($entity_type_id);
278           $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
279           // Add the new entity base table using the table and sql column.
280           $base_table = $this->addNextBaseTable($entity_type, $table, $sql_column, $field_storage);
281           $propertyDefinitions = [];
282           $key++;
283           $index_prefix .= "$next_index_prefix.";
284         }
285         else {
286           throw new QueryException("Invalid specifier '$relationship_specifier'");
287         }
288       }
289     }
290     return "$table.$sql_column";
291   }
292
293   /**
294    * {@inheritdoc}
295    */
296   public function isFieldCaseSensitive($field_name) {
297     if (isset($this->caseSensitiveFields[$field_name])) {
298       return $this->caseSensitiveFields[$field_name];
299     }
300   }
301
302   /**
303    * Joins the entity table, if necessary, and returns the alias for it.
304    *
305    * @param string $index_prefix
306    *   The table array index prefix. For a base table this will be empty,
307    *   for a target entity reference like 'field_tags.entity:taxonomy_term.name'
308    *   this will be 'entity:taxonomy_term.target_id.'.
309    * @param string $property
310    *   The field property/column.
311    * @param string $type
312    *   The join type, can either be INNER or LEFT.
313    * @param string $langcode
314    *   The langcode we use on the join.
315    * @param string $base_table
316    *   The table to join to. It can be either the table name, its alias or the
317    *   'base_table' placeholder.
318    * @param string $id_field
319    *   The name of the ID field/property for the current entity. For instance:
320    *   tid, nid, etc.
321    * @param array $entity_tables
322    *   Array of entity tables (data and base tables) where decide the entity
323    *   property will be queried from. The first table containing the property
324    *   will be used, so the order is important and the data table is always
325    *   preferred.
326    *
327    * @return string
328    *   The alias of the joined table.
329    *
330    * @throws \Drupal\Core\Entity\Query\QueryException
331    *   When an invalid property has been passed.
332    */
333   protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
334     foreach ($entity_tables as $table => $mapping) {
335       if (isset($mapping[$property])) {
336         // Ensure a table joined multiple times through different index prefixes
337         // has unique entityTables entries by concatenating the index prefix
338         // and the base table alias. In this way i.e. if we join to the same
339         // entity table several times for different entity reference fields,
340         // each join gets a separate alias.
341         $key = $index_prefix . ($base_table === 'base_table' ? $table : $base_table);
342         if (!isset($this->entityTables[$key])) {
343           $this->entityTables[$key] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
344         }
345         return $this->entityTables[$key];
346       }
347     }
348     throw new QueryException("'$property' not found");
349   }
350
351   /**
352    * Join field table if necessary.
353    *
354    * @param $field_name
355    *   Name of the field.
356    * @return string
357    * @throws \Drupal\Core\Entity\Query\QueryException
358    */
359   protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) {
360     $field_name = $field->getName();
361     if (!isset($this->fieldTables[$index_prefix . $field_name])) {
362       $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
363       /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
364       $table_mapping = $this->entityManager->getStorage($entity_type_id)->getTableMapping();
365       $table = !$this->sqlQuery->getMetaData('all_revisions') ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
366       if ($field->getCardinality() != 1) {
367         $this->sqlQuery->addMetaData('simple_query', FALSE);
368       }
369       $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta);
370     }
371     return $this->fieldTables[$index_prefix . $field_name];
372   }
373
374   /**
375    * Adds a join to a given table.
376    *
377    * @param string $type
378    *   The join type.
379    * @param string $table
380    *   The table to join to.
381    * @param string $join_condition
382    *   The condition on which to join to.
383    * @param string $langcode
384    *   The langcode we use on the join.
385    * @param string|null $delta
386    *   (optional) A delta which should be used as additional condition.
387    *
388    * @return string
389    *   Returns the alias of the joined table.
390    */
391   protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
392     $arguments = [];
393     if ($langcode) {
394       $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
395       $entity_type = $this->entityManager->getDefinition($entity_type_id);
396       // Only the data table follows the entity language key, dedicated field
397       // tables have an hard-coded 'langcode' column.
398       $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode';
399       $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder();
400       $join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder;
401       $arguments[$placeholder] = $langcode;
402     }
403     if (isset($delta)) {
404       $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder();
405       $join_condition .= ' AND %alias.delta = ' . $placeholder;
406       $arguments[$placeholder] = $delta;
407     }
408     return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
409   }
410
411   /**
412    * Gets the schema for the given table.
413    *
414    * @param string $table
415    *   The table name.
416    *
417    * @return array|bool
418    *   The table field mapping for the given table or FALSE if not available.
419    */
420   protected function getTableMapping($table, $entity_type_id) {
421     $storage = $this->entityManager->getStorage($entity_type_id);
422     if ($storage instanceof SqlEntityStorageInterface) {
423       $mapping = $storage->getTableMapping()->getAllColumns($table);
424     }
425     else {
426       return FALSE;
427     }
428     return array_flip($mapping);
429   }
430
431   /**
432    * Add the next entity base table.
433    *
434    * For example, when building the SQL query for
435    * @code
436    * condition('uid.entity.name', 'foo', 'CONTAINS')
437    * @endcode
438    *
439    * this adds the users table.
440    *
441    * @param \Drupal\Core\Entity\EntityType $entity_type
442    *   The entity type being joined, in the above example, User.
443    * @param string $table
444    *   This is the table being joined, in the above example, {users}.
445    * @param string $sql_column
446    *   This is the SQL column in the existing table being joined to.
447    * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage
448    *   The field storage definition for the field referencing this column.
449    *
450    * @return string
451    *   The alias of the next entity table joined in.
452    */
453   protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column, FieldStorageDefinitionInterface $field_storage) {
454     $join_condition = '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column";
455     return $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);
456   }
457
458 }