DefaultTableMapping.php 13.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
<?php

/**
 * @file
 * Contains \Drupal\Core\Entity\Sql\DefaultTableMapping.
 */

namespace Drupal\Core\Entity\Sql;

10
use Drupal\Core\Entity\ContentEntityTypeInterface;
11
12
use Drupal\Core\Field\FieldStorageDefinitionInterface;

13
14
15
16
17
/**
 * Defines a default table mapping class.
 */
class DefaultTableMapping implements TableMappingInterface {

18
19
20
21
22
23
24
  /**
   * The entity type definition.
   *
   * @var \Drupal\Core\Entity\ContentEntityTypeInterface
   */
  protected $entityType;

25
  /**
26
   * The field storage definitions of this mapping.
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
   *
   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
   */
  protected $fieldStorageDefinitions = array();

  /**
   * A list of field names per table.
   *
   * This corresponds to the return value of
   * TableMappingInterface::getFieldNames() except that this variable is
   * additionally keyed by table name.
   *
   * @var array[]
   */
  protected $fieldNames = array();

  /**
   * A list of database columns which store denormalized data per table.
   *
   * This corresponds to the return value of
   * TableMappingInterface::getExtraColumns() except that this variable is
   * additionally keyed by table name.
   *
   * @var array[]
   */
  protected $extraColumns = array();

  /**
   * A mapping of column names per field name.
   *
   * This corresponds to the return value of
   * TableMappingInterface::getColumnNames() except that this variable is
   * additionally keyed by field name.
   *
   * This data is derived from static::$storageDefinitions, but is stored
   * separately to avoid repeated processing.
   *
   * @var array[]
   */
  protected $columnMapping = array();

  /**
   * A list of all database columns per table.
   *
   * This corresponds to the return value of
   * TableMappingInterface::getAllColumns() except that this variable is
   * additionally keyed by table name.
   *
   * This data is derived from static::$storageDefinitions, static::$fieldNames,
   * and static::$extraColumns, but is stored separately to avoid repeated
   * processing.
   *
   * @var array[]
   */
  protected $allColumns = array();

  /**
   * Constructs a DefaultTableMapping.
   *
86
87
   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
   *   The entity type definition.
88
89
90
91
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
   *   A list of field storage definitions that should be available for the
   *   field columns of this table mapping.
   */
92
93
  public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
    $this->entityType = $entity_type;
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    $this->fieldStorageDefinitions = $storage_definitions;
  }

  /**
   * {@inheritdoc}
   */
  public function getTableNames() {
    return array_unique(array_merge(array_keys($this->fieldNames), array_keys($this->extraColumns)));
  }

  /**
   * {@inheritdoc}
   */
  public function getAllColumns($table_name) {
    if (!isset($this->allColumns[$table_name])) {
      $this->allColumns[$table_name] = array();

      foreach ($this->getFieldNames($table_name) as $field_name) {
        $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name)));
      }

115
116
117
118
119
120
121
122
123
124
      // There is just one field for each dedicated storage table, thus
      // $field_name can only refer to it.
      if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) {
        // Unlike in shared storage tables, in dedicated ones field columns are
        // positioned last.
        $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]);
      }
      else {
        $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name));
      }
125
126
127
128
129
130
131
132
133
134
135
136
137
138
    }
    return $this->allColumns[$table_name];
  }

  /**
   * {@inheritdoc}
   */
  public function getFieldNames($table_name) {
    if (isset($this->fieldNames[$table_name])) {
      return $this->fieldNames[$table_name];
    }
    return array();
  }

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
  /**
   * {@inheritdoc}
   */
  public function getFieldTableName($field_name) {
    $result = NULL;

    if (isset($this->fieldStorageDefinitions[$field_name])) {
      // Since a field may be stored in more than one table, we inspect tables
      // in order of relevance: the data table if present is the main place
      // where field data is stored, otherwise the base table is responsible for
      // storing field data. Revision metadata is an exception as it's stored
      // only in the revision table.
      // @todo The table mapping itself should know about entity tables. See
      //   https://www.drupal.org/node/2274017.
      /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
      $storage = \Drupal::entityManager()->getStorage($this->entityType->id());
      $table_names = array(
        $storage->getDataTable(),
        $storage->getBaseTable(),
        $storage->getRevisionTable(),
      );

      // Collect field columns.
      $field_columns = array();
      $storage_definition = $this->fieldStorageDefinitions[$field_name];
      foreach (array_keys($storage_definition->getColumns()) as $property_name) {
        $field_columns[] = $this->getFieldColumnName($storage_definition, $property_name);
      }

      foreach (array_filter($table_names) as $table_name) {
        $columns = $this->getAllColumns($table_name);
        // We assume finding one field column belonging to the mapping is enough
        // to identify the field table.
        if (array_intersect($columns, $field_columns)) {
          $result = $table_name;
          break;
        }
      }
    }

    if (!isset($result)) {
180
      throw new SqlContentEntityStorageException("Table information not available for the '$field_name' field.");
181
182
183
184
185
    }

    return $result;
  }

186
187
188
189
190
  /**
   * {@inheritdoc}
   */
  public function getColumnNames($field_name) {
    if (!isset($this->columnMapping[$field_name])) {
191
      $this->columnMapping[$field_name] = array();
192
193
194
195
      if (isset($this->fieldStorageDefinitions[$field_name])) {
        foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
          $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
        }
196
197
198
199
200
      }
    }
    return $this->columnMapping[$field_name];
  }

201
202
203
204
205
206
207
208
209
210
211
212
213
  /**
   * {@inheritdoc}
   */
  public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $property_name) {
    $field_name = $storage_definition->getName();

    if ($this->allowsSharedTableStorage($storage_definition)) {
      $column_name = count($storage_definition->getColumns()) == 1 ? $field_name :  $field_name . '__' . $property_name;
    }
    elseif ($this->requiresDedicatedTableStorage($storage_definition)) {
      $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
    }
    else {
214
      throw new SqlContentEntityStorageException("Column information not available for the '$field_name' field.");
215
216
217
218
219
    }

    return $column_name;
  }

220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
  /**
   * Adds field columns for a table to the table mapping.
   *
   * @param string $table_name
   *   The name of the table to add the field column for.
   * @param string[] $field_names
   *   A list of field names to add the columns for.
   *
   * @return $this
   */
  public function setFieldNames($table_name, array $field_names) {
    $this->fieldNames[$table_name] = $field_names;
    // Force the re-computation of the column list.
    unset($this->allColumns[$table_name]);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getExtraColumns($table_name) {
    if (isset($this->extraColumns[$table_name])) {
      return $this->extraColumns[$table_name];
    }
    return array();
  }

  /**
   * Adds a extra columns for a table to the table mapping.
   *
   * @param string $table_name
   *   The name of table to add the extra columns for.
   * @param string[] $column_names
   *   The list of column names.
   *
   * @return $this
   */
  public function setExtraColumns($table_name, array $column_names) {
    $this->extraColumns[$table_name] = $column_names;
    // Force the re-computation of the column list.
    unset($this->allColumns[$table_name]);
    return $this;
  }

264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
  /**
   * Checks whether the given field can be stored in a shared table.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
   *   The field storage definition.
   *
   * @return bool
   *   TRUE if the field can be stored in a dedicated table, FALSE otherwise.
   */
  public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
    return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple();
  }

  /**
   * Checks whether the given field has to be stored in a dedicated table.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
   *   The field storage definition.
   *
   * @return bool
   *   TRUE if the field can be stored in a dedicated table, FALSE otherwise.
   */
  public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
    return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
  }

  /**
291
   * Gets a list of dedicated table names for this mapping.
292
293
294
295
296
297
298
299
300
301
302
303
304
   *
   * @return string[]
   *   An array of table names.
   */
  public function getDedicatedTableNames() {
    $table_mapping = $this;
    $definitions = array_filter($this->fieldStorageDefinitions, function($definition) use ($table_mapping) { return $table_mapping->requiresDedicatedTableStorage($definition); });
    $data_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedDataTableName($definition); }, $definitions);
    $revision_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedRevisionTableName($definition); }, $definitions);
    $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables));
    return $dedicated_tables;
  }

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
  /**
   * {@inheritdoc}
   */
  public function getReservedColumns() {
    return array('deleted');
  }

  /**
   * Generates a table name for a field data table.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
   *   The field storage definition.
   * @param bool $is_deleted
   *   (optional) Whether the table name holding the values of a deleted field
   *   should be returned.
   *
   * @return string
   *   A string containing the generated name for the database table.
   */
  public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
    if ($is_deleted) {
      // When a field is a deleted, the table is renamed to
      // {field_deleted_data_FIELD_UUID}. To make sure we don't end up with
      // table names longer than 64 characters, we hash the unique storage
      // identifier and return the first 10 characters so we end up with a short
      // unique ID.
      return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
    }
    else {
      return $this->generateFieldTableName($storage_definition, FALSE);
    }
  }

  /**
   * Generates a table name for a field revision archive table.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
   *   The field storage definition.
   * @param bool $is_deleted
   *   (optional) Whether the table name holding the values of a deleted field
   *   should be returned.
   *
   * @return string
   *   A string containing the generated name for the database table.
   */
  public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
    if ($is_deleted) {
      // When a field is a deleted, the table is renamed to
      // {field_deleted_revision_FIELD_UUID}. To make sure we don't end up with
      // table names longer than 64 characters, we hash the unique storage
      // identifier and return the first 10 characters so we end up with a short
      // unique ID.
      return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
    }
    else {
      return $this->generateFieldTableName($storage_definition, TRUE);
    }
  }

  /**
   * Generates a safe and unambiguous field table name.
   *
   * The method accounts for a maximum table name length of 64 characters, and
   * takes care of disambiguation.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
   *   The field storage definition.
   * @param bool $revision
   *   TRUE for revision table, FALSE otherwise.
   *
   * @return string
   *   The final table name.
   */
  protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
    $separator = $revision ? '_revision__' : '__';
    $table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName();
    // Limit the string to 48 characters, keeping a 16 characters margin for db
    // prefixes.
    if (strlen($table_name) > 48) {
      // Use a shorter separator, a truncated entity_type, and a hash of the
      // field UUID.
      $separator = $revision ? '_r__' : '__';
      // Truncate to the same length for the current and revision tables.
      $entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
      $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
      $table_name = $entity_type . $separator . $field_hash;
    }
    return $table_name;
  }

395
}