Tables.php 10.5 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\Core\Entity\Query\Sql\Tables.
6 7
 */

8
namespace Drupal\Core\Entity\Query\Sql;
9 10

use Drupal\Core\Database\Query\SelectInterface;
11
use Drupal\Core\Entity\ContentEntityTypeInterface;
12 13
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityDatabaseStorage;
14
use Drupal\Core\Entity\Query\QueryException;
15
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
16 17
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\FieldStorageConfigInterface;
18 19 20 21

/**
 * Adds tables and fields to the SQL entity query.
 */
22
class Tables implements TablesInterface {
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

  /**
   * @var \Drupal\Core\Database\Query\SelectInterface
   */
  protected $sqlQuery;

  /**
   * Entity table array, key is table name, value is alias.
   *
   * This array contains at most two entries: one for the data, one for the
   * properties.
   *
   * @var array
   */
  protected $entityTables = array();

  /**
   * Field table array, key is table name, value is alias.
   *
   * This array contains one entry per field table.
   *
   * @var array
   */
  protected $fieldTables = array();

48 49 50 51 52 53 54
  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityManager
   */
  protected $entityManager;

55 56 57
  /**
   * @param \Drupal\Core\Database\Query\SelectInterface $sql_query
   */
58
  public function __construct(SelectInterface $sql_query) {
59
    $this->sqlQuery = $sql_query;
60
    $this->entityManager = \Drupal::entityManager();
61 62 63
  }

  /**
64
   * {@inheritdoc}
65
   */
66
  public function addField($field, $type, $langcode) {
67
    $entity_type_id = $this->sqlQuery->getMetaData('entity_type');
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
    $age = $this->sqlQuery->getMetaData('age');
    // This variable ensures grouping works correctly. For example:
    // ->condition('tags', 2, '>')
    // ->condition('tags', 20, '<')
    // ->condition('node_reference.nid.entity.tags', 2)
    // The first two should use the same table but the last one needs to be a
    // new table. So for the first two, the table array index will be 'tags'
    // while the third will be 'node_reference.nid.tags'.
    $index_prefix = '';
    $specifiers = explode('.', $field);
    $base_table = 'base_table';
    $count = count($specifiers) - 1;
    // This will contain the definitions of the last specifier seen by the
    // system.
    $propertyDefinitions = array();
83
    $entity_type = $this->entityManager->getDefinition($entity_type_id);
84 85 86 87 88

    $field_storage_definitions = array();
    // @todo Needed for menu links, make this implementation content entity
    //   specific after https://drupal.org/node/2256521.
    if ($entity_type instanceof ContentEntityTypeInterface) {
89
      $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
90
    }
91 92 93
    for ($key = 0; $key <= $count; $key ++) {
      // If there is revision support and only the current revision is being
      // queried then use the revision id. Otherwise, the entity id will do.
94
      if (($revision_key = $entity_type->getKey('revision')) && $age == EntityStorageInterface::FIELD_LOAD_CURRENT) {
95 96
        // This contains the relevant SQL field to be used when joining entity
        // tables.
97
        $entity_id_field = $revision_key;
98 99 100 101 102
        // This contains the relevant SQL field to be used when joining field
        // tables.
        $field_id_field = 'revision_id';
      }
      else {
103
        $entity_id_field = $entity_type->getKey('id');
104 105
        $field_id_field = 'entity_id';
      }
106 107
      // This can either be the name of an entity base field or a configurable
      // field.
108
      $specifier = $specifiers[$key];
109
      if (isset($field_storage_definitions[$specifier])) {
110
        $field_storage = $field_storage_definitions[$specifier];
111
      }
112
      else {
113
        $field_storage = FALSE;
114
      }
115
      // If we managed to retrieve a configurable field, process it.
116
      if ($field_storage instanceof FieldStorageConfigInterface) {
117
        // Find the field column.
118
        $column = $field_storage->getMainPropertyName();
119 120 121
        if ($key < $count) {
          $next = $specifiers[$key + 1];
          // Is this a field column?
122 123
          $columns = $field_storage->getColumns();
          if (isset($columns[$next]) || in_array($next, FieldStorageConfig::getReservedColumns())) {
124 125 126 127 128 129 130 131 132 133 134 135 136 137
            // Use it.
            $column = $next;
            // Do not process it again.
            $key++;
          }
          // If there are more specifiers, the next one must be a
          // relationship. Either the field name followed by a relationship
          // specifier, for example $node->field_image->entity. Or a field
          // column followed by a relationship specifier, for example
          // $node->field_image->fid->entity. In both cases, prepare the
          // property definitions for the relationship. In the first case,
          // also use the property definitions for column.
          if ($key < $count) {
            $relationship_specifier = $specifiers[$key + 1];
138
            $propertyDefinitions = $field_storage->getPropertyDefinitions();
139

140 141 142 143
            // Prepare the next index prefix.
            $next_index_prefix = "$relationship_specifier.$column";
          }
        }
144 145
        $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field);
        $sql_column = ContentEntityDatabaseStorage::_fieldColumnName($field_storage, $column);
146
      }
147
      // This is an entity base field (non-configurable field).
148 149 150
      else {
        // ensureEntityTable() decides whether an entity property will be
        // queried from the data table or the base table based on where it
151
        // finds the property first. The data table is preferred, which is why
152 153
        // it gets added before the base table.
        $entity_tables = array();
154
        if ($data_table = $entity_type->getDataTable()) {
155
          $this->sqlQuery->addMetaData('simple_query', FALSE);
156
          $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
157
        }
158
        $entity_base_table = $entity_type->getBaseTable();
159
        $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
160 161 162 163
        $sql_column = $specifier;
        $table = $this->ensureEntityTable($index_prefix, $specifier, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
      }
      // If there are more specifiers to come, it's a relationship.
164
      if ($field_storage && $key < $count) {
165 166 167
        // Computed fields have prepared their property definition already, do
        // it for properties as well.
        if (!$propertyDefinitions) {
168
          $propertyDefinitions = $field_storage->getPropertyDefinitions();
169 170 171 172
          $relationship_specifier = $specifiers[$key + 1];
          $next_index_prefix = $relationship_specifier;
        }
        // Check for a valid relationship.
173
        if (isset($propertyDefinitions[$relationship_specifier]) && $field_storage->getPropertyDefinition('entity')->getDataType() == 'entity_reference' ) {
174
          // If it is, use the entity type.
175
          $entity_type_id = $propertyDefinitions[$relationship_specifier]->getTargetDefinition()->getEntityTypeId();
176 177
          $entity_type = $this->entityManager->getDefinition($entity_type_id);
          $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
178
          // Add the new entity base table using the table and sql column.
179 180
          $join_condition= '%alias.' . $entity_type->getKey('id') . " = $table.$sql_column";
          $base_table = $this->sqlQuery->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);
181 182 183 184 185
          $propertyDefinitions = array();
          $key++;
          $index_prefix .= "$next_index_prefix.";
        }
        else {
186
          throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $relationship_specifier)));
187 188
        }
      }
189 190 191 192 193 194 195 196 197 198 199
    }
    return "$table.$sql_column";
  }

  /**
   * Join entity table if necessary and return the alias for it.
   *
   * @param string $property
   * @return string
   * @throws \Drupal\Core\Entity\Query\QueryException
   */
200
  protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
201 202
    foreach ($entity_tables as $table => $mapping) {
      if (isset($mapping[$property])) {
203 204
        if (!isset($this->entityTables[$index_prefix . $table])) {
          $this->entityTables[$index_prefix . $table] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode);
205
        }
206
        return $this->entityTables[$index_prefix . $table];
207 208 209 210 211 212 213 214 215 216 217 218 219
      }
    }
    throw new QueryException(format_string('@property not found', array('@property' => $property)));
  }

  /**
   * Join field table if necessary.
   *
   * @param $field_name
   *   Name of the field.
   * @return string
   * @throws \Drupal\Core\Entity\Query\QueryException
   */
220
  protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field) {
221
    $field_name = $field->getName();
222
    if (!isset($this->fieldTables[$index_prefix . $field_name])) {
223
      $table = $this->sqlQuery->getMetaData('age') == EntityStorageInterface::FIELD_LOAD_CURRENT ? ContentEntityDatabaseStorage::_fieldTableName($field) : ContentEntityDatabaseStorage::_fieldRevisionTableName($field);
224
      if ($field->getCardinality() != 1) {
225 226
        $this->sqlQuery->addMetaData('simple_query', FALSE);
      }
227
      $entity_type = $this->sqlQuery->getMetaData('entity_type');
228
      $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode);
229
    }
230
    return $this->fieldTables[$index_prefix . $field_name];
231 232 233 234 235 236 237 238 239 240 241 242
  }

  protected function addJoin($type, $table, $join_condition, $langcode) {
    $arguments = array();
    if ($langcode) {
      $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder();
      $join_condition .= ' AND %alias.langcode = ' . $placeholder;
      $arguments[$placeholder] = $langcode;
    }
    return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments);
  }

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
  /**
   * Returns the schema for the given table.
   *
   * @param string $table
   *   The table name.
   *
   * @return array|bool
   *   The table field mapping for the given table or FALSE if not available.
   */
  protected function getTableMapping($table, $entity_type_id) {
    $storage = $this->entityManager->getStorage($entity_type_id);
    if ($storage instanceof SqlEntityStorageInterface) {
      $mapping = $storage->getTableMapping()->getAllColumns($table);
    }
    else {
258
      return FALSE;
259 260 261 262
    }
    return array_flip($mapping);
  }

263
}