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