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