1<?php
2
3namespace Drupal\Core\Entity\Sql;
4
5use Drupal\Core\Entity\ContentEntityTypeInterface;
6use Drupal\Core\Entity\EntityTypeInterface;
7use Drupal\Core\Field\FieldStorageDefinitionInterface;
8
9/**
10 * Defines a default table mapping class.
11 */
12class DefaultTableMapping implements TableMappingInterface {
13
14  /**
15   * The entity type definition.
16   *
17   * @var \Drupal\Core\Entity\ContentEntityTypeInterface
18   */
19  protected $entityType;
20
21  /**
22   * The field storage definitions of this mapping.
23   *
24   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
25   */
26  protected $fieldStorageDefinitions = [];
27
28  /**
29   * The prefix to be used by all the tables of this mapping.
30   *
31   * @var string
32   */
33  protected $prefix;
34
35  /**
36   * The base table of the entity.
37   *
38   * @var string
39   */
40  protected $baseTable;
41
42  /**
43   * The table that stores revisions, if the entity supports revisions.
44   *
45   * @var string
46   */
47  protected $revisionTable;
48
49  /**
50   * The table that stores field data, if the entity has multilingual support.
51   *
52   * @var string
53   */
54  protected $dataTable;
55
56  /**
57   * The table that stores revision field data if the entity supports revisions
58   * and has multilingual support.
59   *
60   * @var string
61   */
62  protected $revisionDataTable;
63
64  /**
65   * A list of field names per table.
66   *
67   * This corresponds to the return value of
68   * TableMappingInterface::getFieldNames() except that this variable is
69   * additionally keyed by table name.
70   *
71   * @var array[]
72   */
73  protected $fieldNames = [];
74
75  /**
76   * A list of database columns which store denormalized data per table.
77   *
78   * This corresponds to the return value of
79   * TableMappingInterface::getExtraColumns() except that this variable is
80   * additionally keyed by table name.
81   *
82   * @var array[]
83   */
84  protected $extraColumns = [];
85
86  /**
87   * A mapping of column names per field name.
88   *
89   * This corresponds to the return value of
90   * TableMappingInterface::getColumnNames() except that this variable is
91   * additionally keyed by field name.
92   *
93   * This data is derived from static::$storageDefinitions, but is stored
94   * separately to avoid repeated processing.
95   *
96   * @var array[]
97   */
98  protected $columnMapping = [];
99
100  /**
101   * A list of all database columns per table.
102   *
103   * This corresponds to the return value of
104   * TableMappingInterface::getAllColumns() except that this variable is
105   * additionally keyed by table name.
106   *
107   * This data is derived from static::$storageDefinitions, static::$fieldNames,
108   * and static::$extraColumns, but is stored separately to avoid repeated
109   * processing.
110   *
111   * @var array[]
112   */
113  protected $allColumns = [];
114
115  /**
116   * Constructs a DefaultTableMapping.
117   *
118   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
119   *   The entity type definition.
120   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
121   *   A list of field storage definitions that should be available for the
122   *   field columns of this table mapping.
123   * @param string $prefix
124   *   (optional) A prefix to be used by all the tables of this mapping.
125   *   Defaults to an empty string.
126   */
127  public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
128    $this->entityType = $entity_type;
129    $this->fieldStorageDefinitions = $storage_definitions;
130    $this->prefix = $prefix;
131
132    // @todo Remove table names from the entity type definition in
133    //   https://www.drupal.org/node/2232465.
134    $this->baseTable = $this->prefix . $entity_type->getBaseTable() ?: $entity_type->id();
135    if ($entity_type->isRevisionable()) {
136      $this->revisionTable = $this->prefix . $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
137    }
138    if ($entity_type->isTranslatable()) {
139      $this->dataTable = $this->prefix . $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
140    }
141    if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
142      $this->revisionDataTable = $this->prefix . $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
143    }
144  }
145
146  /**
147   * Initializes the table mapping.
148   *
149   * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
150   *   The entity type definition.
151   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
152   *   A list of field storage definitions that should be available for the
153   *   field columns of this table mapping.
154   * @param string $prefix
155   *   (optional) A prefix to be used by all the tables of this mapping.
156   *   Defaults to an empty string.
157   *
158   * @return static
159   *
160   * @internal
161   */
162  public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
163    $table_mapping = new static($entity_type, $storage_definitions, $prefix);
164
165    $revisionable = $entity_type->isRevisionable();
166    $translatable = $entity_type->isTranslatable();
167
168    $id_key = $entity_type->getKey('id');
169    $revision_key = $entity_type->getKey('revision');
170    $bundle_key = $entity_type->getKey('bundle');
171    $uuid_key = $entity_type->getKey('uuid');
172    $langcode_key = $entity_type->getKey('langcode');
173
174    $shared_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
175      return $table_mapping->allowsSharedTableStorage($definition);
176    });
177
178    $key_fields = array_values(array_filter([$id_key, $revision_key, $bundle_key, $uuid_key, $langcode_key]));
179    $all_fields = array_keys($shared_table_definitions);
180    $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
181      return $definition->isRevisionable();
182    }));
183    // Make sure the key fields come first in the list of fields.
184    $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
185
186    $revision_metadata_fields = $revisionable ? array_values($entity_type->getRevisionMetadataKeys()) : [];
187    $revision_metadata_fields = array_intersect($revision_metadata_fields, array_keys($storage_definitions));
188
189    if (!$revisionable && !$translatable) {
190      // The base layout stores all the base field values in the base table.
191      $table_mapping->setFieldNames($table_mapping->baseTable, $all_fields);
192    }
193    elseif ($revisionable && !$translatable) {
194      // The revisionable layout stores all the base field values in the base
195      // table, except for revision metadata fields. Revisionable fields
196      // denormalized in the base table but also stored in the revision table
197      // together with the entity ID and the revision ID as identifiers.
198      $table_mapping->setFieldNames($table_mapping->baseTable, array_diff($all_fields, $revision_metadata_fields));
199      $revision_key_fields = [$id_key, $revision_key];
200      $table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
201    }
202    elseif (!$revisionable && $translatable) {
203      // Multilingual layouts store key field values in the base table. The
204      // other base field values are stored in the data table, no matter
205      // whether they are translatable or not. The data table holds also a
206      // denormalized copy of the bundle field value to allow for more
207      // performant queries. This means that only the UUID is not stored on
208      // the data table.
209      $table_mapping
210        ->setFieldNames($table_mapping->baseTable, $key_fields)
211        ->setFieldNames($table_mapping->dataTable, array_values(array_diff($all_fields, [$uuid_key])));
212    }
213    elseif ($revisionable && $translatable) {
214      // The revisionable multilingual layout stores key field values in the
215      // base table and the revision table holds the entity ID, revision ID and
216      // langcode ID along with revision metadata. The revision data table holds
217      // data field values for all the revisionable fields and the data table
218      // holds the data field values for all non-revisionable fields. The data
219      // field values of revisionable fields are denormalized in the data
220      // table, as well.
221      $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields);
222
223      // Like in the multilingual, non-revisionable case the UUID is not
224      // in the data table. Additionally, do not store revision metadata
225      // fields in the data table.
226      $data_fields = array_values(array_diff($all_fields, [$uuid_key], $revision_metadata_fields));
227      $table_mapping->setFieldNames($table_mapping->dataTable, $data_fields);
228
229      $revision_base_fields = array_merge([$id_key, $revision_key, $langcode_key], $revision_metadata_fields);
230      $table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields);
231
232      $revision_data_key_fields = [$id_key, $revision_key, $langcode_key];
233      $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$langcode_key]);
234      $table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
235    }
236
237    // Add dedicated tables.
238    $dedicated_table_definitions = array_filter($table_mapping->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
239      return $table_mapping->requiresDedicatedTableStorage($definition);
240    });
241    $extra_columns = [
242      'bundle',
243      'deleted',
244      'entity_id',
245      'revision_id',
246      'langcode',
247      'delta',
248    ];
249    foreach ($dedicated_table_definitions as $field_name => $definition) {
250      $tables = [$table_mapping->getDedicatedDataTableName($definition)];
251      if ($revisionable && $definition->isRevisionable()) {
252        $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
253      }
254      foreach ($tables as $table_name) {
255        $table_mapping->setFieldNames($table_name, [$field_name]);
256        $table_mapping->setExtraColumns($table_name, $extra_columns);
257      }
258    }
259
260    return $table_mapping;
261  }
262
263  /**
264   * Gets the base table name.
265   *
266   * @return string
267   *   The base table name.
268   *
269   * @internal
270   */
271  public function getBaseTable() {
272    return $this->baseTable;
273  }
274
275  /**
276   * Gets the revision table name.
277   *
278   * @return string|null
279   *   The revision table name.
280   *
281   * @internal
282   */
283  public function getRevisionTable() {
284    return $this->revisionTable;
285  }
286
287  /**
288   * Gets the data table name.
289   *
290   * @return string|null
291   *   The data table name.
292   *
293   * @internal
294   */
295  public function getDataTable() {
296    return $this->dataTable;
297  }
298
299  /**
300   * Gets the revision data table name.
301   *
302   * @return string|null
303   *   The revision data table name.
304   *
305   * @internal
306   */
307  public function getRevisionDataTable() {
308    return $this->revisionDataTable;
309  }
310
311  /**
312   * {@inheritdoc}
313   */
314  public function getTableNames() {
315    return array_unique(array_merge(array_keys($this->fieldNames), array_keys($this->extraColumns)));
316  }
317
318  /**
319   * {@inheritdoc}
320   */
321  public function getAllColumns($table_name) {
322    if (!isset($this->allColumns[$table_name])) {
323      $this->allColumns[$table_name] = [];
324
325      foreach ($this->getFieldNames($table_name) as $field_name) {
326        $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name)));
327      }
328
329      // There is just one field for each dedicated storage table, thus
330      // $field_name can only refer to it.
331      if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) {
332        // Unlike in shared storage tables, in dedicated ones field columns are
333        // positioned last.
334        $this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]);
335      }
336      else {
337        $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name));
338      }
339    }
340    return $this->allColumns[$table_name];
341  }
342
343  /**
344   * {@inheritdoc}
345   */
346  public function getFieldNames($table_name) {
347    if (isset($this->fieldNames[$table_name])) {
348      return $this->fieldNames[$table_name];
349    }
350    return [];
351  }
352
353  /**
354   * {@inheritdoc}
355   */
356  public function getFieldTableName($field_name) {
357    $result = NULL;
358
359    if (isset($this->fieldStorageDefinitions[$field_name])) {
360      // Since a field may be stored in more than one table, we inspect tables
361      // in order of relevance: the data table if present is the main place
362      // where field data is stored, otherwise the base table is responsible for
363      // storing field data. Revision metadata is an exception as it's stored
364      // only in the revision table.
365      $storage_definition = $this->fieldStorageDefinitions[$field_name];
366      $table_names = array_filter([
367        $this->dataTable,
368        $this->baseTable,
369        $this->revisionTable,
370        $this->getDedicatedDataTableName($storage_definition),
371      ]);
372
373      // Collect field columns.
374      $field_columns = [];
375      foreach (array_keys($storage_definition->getColumns()) as $property_name) {
376        $field_columns[] = $this->getFieldColumnName($storage_definition, $property_name);
377      }
378
379      foreach ($table_names as $table_name) {
380        $columns = $this->getAllColumns($table_name);
381        // We assume finding one field column belonging to the mapping is enough
382        // to identify the field table.
383        if (array_intersect($columns, $field_columns)) {
384          $result = $table_name;
385          break;
386        }
387      }
388    }
389
390    if (!isset($result)) {
391      throw new SqlContentEntityStorageException("Table information not available for the '$field_name' field.");
392    }
393
394    return $result;
395  }
396
397  /**
398   * {@inheritdoc}
399   */
400  public function getAllFieldTableNames($field_name) {
401    return array_keys(array_filter($this->fieldNames, function ($table_fields) use ($field_name) {
402      return in_array($field_name, $table_fields, TRUE);
403    }));
404  }
405
406  /**
407   * {@inheritdoc}
408   */
409  public function getColumnNames($field_name) {
410    if (!isset($this->columnMapping[$field_name])) {
411      $this->columnMapping[$field_name] = [];
412      if (isset($this->fieldStorageDefinitions[$field_name]) && !$this->fieldStorageDefinitions[$field_name]->hasCustomStorage()) {
413        foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
414          $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
415        }
416      }
417    }
418    return $this->columnMapping[$field_name];
419  }
420
421  /**
422   * {@inheritdoc}
423   */
424  public function getFieldColumnName(FieldStorageDefinitionInterface $storage_definition, $property_name) {
425    $field_name = $storage_definition->getName();
426
427    if ($this->allowsSharedTableStorage($storage_definition)) {
428      $column_name = count($storage_definition->getColumns()) == 1 ? $field_name : $field_name . '__' . $property_name;
429    }
430    elseif ($this->requiresDedicatedTableStorage($storage_definition)) {
431      if ($property_name == TableMappingInterface::DELTA) {
432        $column_name = 'delta';
433      }
434      else {
435        $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
436      }
437    }
438    else {
439      throw new SqlContentEntityStorageException("Column information not available for the '$field_name' field.");
440    }
441
442    return $column_name;
443  }
444
445  /**
446   * Adds field columns for a table to the table mapping.
447   *
448   * @param string $table_name
449   *   The name of the table to add the field column for.
450   * @param string[] $field_names
451   *   A list of field names to add the columns for.
452   *
453   * @return $this
454   *
455   * @internal
456   *
457   * @todo Make this method protected in drupal:9.0.0.
458   * @see https://www.drupal.org/node/3067336
459   */
460  public function setFieldNames($table_name, array $field_names) {
461    $this->fieldNames[$table_name] = $field_names;
462    // Force the re-computation of the column list.
463    unset($this->allColumns[$table_name]);
464    return $this;
465  }
466
467  /**
468   * {@inheritdoc}
469   */
470  public function getExtraColumns($table_name) {
471    if (isset($this->extraColumns[$table_name])) {
472      return $this->extraColumns[$table_name];
473    }
474    return [];
475  }
476
477  /**
478   * Adds extra columns for a table to the table mapping.
479   *
480   * @param string $table_name
481   *   The name of table to add the extra columns for.
482   * @param string[] $column_names
483   *   The list of column names.
484   *
485   * @return $this
486   *
487   * @internal
488   *
489   * @todo Make this method protected in drupal:9.0.0.
490   * @see https://www.drupal.org/node/3067336
491   */
492  public function setExtraColumns($table_name, array $column_names) {
493    $this->extraColumns[$table_name] = $column_names;
494    // Force the re-computation of the column list.
495    unset($this->allColumns[$table_name]);
496    return $this;
497  }
498
499  /**
500   * Checks whether the given field can be stored in a shared table.
501   *
502   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
503   *   The field storage definition.
504   *
505   * @return bool
506   *   TRUE if the field can be stored in a shared table, FALSE otherwise.
507   */
508  public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
509    return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple() && !$storage_definition->isDeleted();
510  }
511
512  /**
513   * Checks whether the given field has to be stored in a dedicated table.
514   *
515   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
516   *   The field storage definition.
517   *
518   * @return bool
519   *   TRUE if the field has to be stored in a dedicated table, FALSE otherwise.
520   */
521  public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
522    return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
523  }
524
525  /**
526   * Gets a list of dedicated table names for this mapping.
527   *
528   * @return string[]
529   *   An array of table names.
530   */
531  public function getDedicatedTableNames() {
532    $table_mapping = $this;
533    $definitions = array_filter($this->fieldStorageDefinitions, function ($definition) use ($table_mapping) {
534      return $table_mapping->requiresDedicatedTableStorage($definition);
535    });
536    $data_tables = array_map(function ($definition) use ($table_mapping) {
537      return $table_mapping->getDedicatedDataTableName($definition);
538    }, $definitions);
539    $revision_tables = array_map(function ($definition) use ($table_mapping) {
540      return $table_mapping->getDedicatedRevisionTableName($definition);
541    }, $definitions);
542    $dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables));
543    return $dedicated_tables;
544  }
545
546  /**
547   * {@inheritdoc}
548   */
549  public function getReservedColumns() {
550    return ['deleted'];
551  }
552
553  /**
554   * Generates a table name for a field data table.
555   *
556   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
557   *   The field storage definition.
558   * @param bool $is_deleted
559   *   (optional) Whether the table name holding the values of a deleted field
560   *   should be returned.
561   *
562   * @return string
563   *   A string containing the generated name for the database table.
564   */
565  public function getDedicatedDataTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
566    if ($is_deleted) {
567      // When a field is a deleted, the table is renamed to
568      // {field_deleted_data_UNIQUE_STORAGE_ID}. To make sure we don't end up
569      // with table names longer than 64 characters, we hash the unique storage
570      // identifier and return the first 10 characters so we end up with a short
571      // unique ID.
572      return "field_deleted_data_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
573    }
574    else {
575      return $this->generateFieldTableName($storage_definition, FALSE);
576    }
577  }
578
579  /**
580   * Generates a table name for a field revision archive table.
581   *
582   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
583   *   The field storage definition.
584   * @param bool $is_deleted
585   *   (optional) Whether the table name holding the values of a deleted field
586   *   should be returned.
587   *
588   * @return string
589   *   A string containing the generated name for the database table.
590   */
591  public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $storage_definition, $is_deleted = FALSE) {
592    if ($is_deleted) {
593      // When a field is a deleted, the table is renamed to
594      // {field_deleted_revision_UNIQUE_STORAGE_ID}. To make sure we don't end
595      // up with table names longer than 64 characters, we hash the unique
596      // storage identifier and return the first 10 characters so we end up with
597      // a short unique ID.
598      return "field_deleted_revision_" . substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
599    }
600    else {
601      return $this->generateFieldTableName($storage_definition, TRUE);
602    }
603  }
604
605  /**
606   * Generates a safe and unambiguous field table name.
607   *
608   * The method accounts for a maximum table name length of 64 characters, and
609   * takes care of disambiguation.
610   *
611   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
612   *   The field storage definition.
613   * @param bool $revision
614   *   TRUE for revision table, FALSE otherwise.
615   *
616   * @return string
617   *   The final table name.
618   */
619  protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
620    // The maximum length of an entity type ID is 32 characters.
621    $entity_type_id = substr($storage_definition->getTargetEntityTypeId(), 0, EntityTypeInterface::ID_MAX_LENGTH);
622    $separator = $revision ? '_revision__' : '__';
623
624    $table_name = $this->prefix . $entity_type_id . $separator . $storage_definition->getName();
625    // Limit the string to 48 characters, keeping a 16 characters margin for db
626    // prefixes.
627    if (strlen($table_name) > 48) {
628      // Use a shorter separator and a hash of the field storage unique
629      // identifier.
630      $separator = $revision ? '_r__' : '__';
631      $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
632
633      $table_name = $this->prefix . $entity_type_id . $separator . $field_hash;
634
635      // If the resulting table name is still longer than 48 characters, use the
636      // following pattern:
637      // - prefix: max 34 chars;
638      // - separator: max 4 chars;
639      // - field_hash: max 10 chars.
640      if (strlen($table_name) > 48) {
641        $prefix = substr($this->prefix, 0, 34);
642        $table_name = $prefix . $separator . $field_hash;
643      }
644    }
645    return $table_name;
646  }
647
648}
649