DefaultTableMapping.php

Same filename in other branches
  1. 9 core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
  2. 8.9.x core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php
  3. 11.x core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php

Namespace

Drupal\Core\Entity\Sql

File

core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php

View source
<?php

namespace Drupal\Core\Entity\Sql;

use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;

/**
 * Defines a default table mapping class.
 */
class DefaultTableMapping implements TableMappingInterface {
    
    /**
     * The entity type definition.
     *
     * @var \Drupal\Core\Entity\ContentEntityTypeInterface
     */
    protected $entityType;
    
    /**
     * The field storage definitions of this mapping.
     *
     * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
     */
    protected $fieldStorageDefinitions = [];
    
    /**
     * The prefix to be used by all the tables of this mapping.
     *
     * @var string
     */
    protected $prefix;
    
    /**
     * The base table of the entity.
     *
     * @var string
     */
    protected $baseTable;
    
    /**
     * The table that stores revisions, if the entity supports revisions.
     *
     * @var string
     */
    protected $revisionTable;
    
    /**
     * The table that stores field data, if the entity has multilingual support.
     *
     * @var string
     */
    protected $dataTable;
    
    /**
     * The table that stores revision field data.
     *
     * Only used if the entity supports revisions and has multilingual support.
     *
     * @var string
     */
    protected $revisionDataTable;
    
    /**
     * 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 = [];
    
    /**
     * 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 = [];
    
    /**
     * 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 = [];
    
    /**
     * 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 = [];
    
    /**
     * Constructs a DefaultTableMapping.
     *
     * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
     *   The entity type definition.
     * @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.
     * @param string $prefix
     *   (optional) A prefix to be used by all the tables of this mapping.
     *   Defaults to an empty string.
     */
    public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
        $this->entityType = $entity_type;
        $this->fieldStorageDefinitions = $storage_definitions;
        $this->prefix = $prefix;
        // @todo Remove table names from the entity type definition in
        //   https://www.drupal.org/node/2232465.
        $this->baseTable = $this->prefix . $entity_type->getBaseTable() ?: $entity_type->id();
        if ($entity_type->isRevisionable()) {
            $this->revisionTable = $this->prefix . $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
        }
        if ($entity_type->isTranslatable()) {
            $this->dataTable = $this->prefix . $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
        }
        if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
            $this->revisionDataTable = $this->prefix . $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
        }
    }
    
    /**
     * Initializes the table mapping.
     *
     * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
     *   The entity type definition.
     * @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.
     * @param string $prefix
     *   (optional) A prefix to be used by all the tables of this mapping.
     *   Defaults to an empty string.
     *
     * @return static
     *
     * @internal
     */
    public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
        $table_mapping = new static($entity_type, $storage_definitions, $prefix);
        $revisionable = $entity_type->isRevisionable();
        $translatable = $entity_type->isTranslatable();
        $id_key = $entity_type->getKey('id');
        $revision_key = $entity_type->getKey('revision');
        $bundle_key = $entity_type->getKey('bundle');
        $uuid_key = $entity_type->getKey('uuid');
        $langcode_key = $entity_type->getKey('langcode');
        $shared_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
            return $table_mapping->allowsSharedTableStorage($definition);
        });
        $key_fields = array_values(array_filter([
            $id_key,
            $revision_key,
            $bundle_key,
            $uuid_key,
            $langcode_key,
        ]));
        $all_fields = array_keys($shared_table_definitions);
        $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
            return $definition->isRevisionable();
        }));
        // Make sure the key fields come first in the list of fields.
        $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
        $revision_metadata_fields = $revisionable ? array_values($entity_type->getRevisionMetadataKeys()) : [];
        $revision_metadata_fields = array_intersect($revision_metadata_fields, array_keys($storage_definitions));
        if (!$revisionable && !$translatable) {
            // The base layout stores all the base field values in the base table.
            $table_mapping->setFieldNames($table_mapping->baseTable, $all_fields);
        }
        elseif ($revisionable && !$translatable) {
            // The revisionable layout stores all the base field values in the base
            // table, except for revision metadata fields. Revisionable fields
            // denormalized in the base table but also stored in the revision table
            // together with the entity ID and the revision ID as identifiers.
            $table_mapping->setFieldNames($table_mapping->baseTable, array_diff($all_fields, $revision_metadata_fields));
            $revision_key_fields = [
                $id_key,
                $revision_key,
            ];
            $table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
        }
        elseif (!$revisionable && $translatable) {
            // Multilingual layouts store key field values in the base table. The
            // other base field values are stored in the data table, no matter
            // whether they are translatable or not. The data table holds also a
            // denormalized copy of the bundle field value to allow for more
            // performant queries. This means that only the UUID is not stored on
            // the data table.
            $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields)
                ->setFieldNames($table_mapping->dataTable, array_values(array_diff($all_fields, [
                $uuid_key,
            ])));
        }
        elseif ($revisionable && $translatable) {
            // The revisionable multilingual layout stores key field values in the
            // base table and the revision table holds the entity ID, revision ID and
            // langcode ID along with revision metadata. The revision data table holds
            // data field values for all the revisionable fields and the data table
            // holds the data field values for all non-revisionable fields. The data
            // field values of revisionable fields are denormalized in the data
            // table, as well.
            $table_mapping->setFieldNames($table_mapping->baseTable, $key_fields);
            // Like in the multilingual, non-revisionable case the UUID is not
            // in the data table. Additionally, do not store revision metadata
            // fields in the data table.
            $data_fields = array_values(array_diff($all_fields, [
                $uuid_key,
            ], $revision_metadata_fields));
            $table_mapping->setFieldNames($table_mapping->dataTable, $data_fields);
            $revision_base_fields = array_merge([
                $id_key,
                $revision_key,
                $langcode_key,
            ], $revision_metadata_fields);
            $table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields);
            $revision_data_key_fields = [
                $id_key,
                $revision_key,
                $langcode_key,
            ];
            $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [
                $langcode_key,
            ]);
            $table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
        }
        // Add dedicated tables.
        $dedicated_table_definitions = array_filter($table_mapping->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
            return $table_mapping->requiresDedicatedTableStorage($definition);
        });
        $extra_columns = [
            'bundle',
            'deleted',
            'entity_id',
            'revision_id',
            'langcode',
            'delta',
        ];
        foreach ($dedicated_table_definitions as $field_name => $definition) {
            $tables = [
                $table_mapping->getDedicatedDataTableName($definition),
            ];
            if ($revisionable && $definition->isRevisionable()) {
                $tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
            }
            foreach ($tables as $table_name) {
                $table_mapping->setFieldNames($table_name, [
                    $field_name,
                ]);
                $table_mapping->setExtraColumns($table_name, $extra_columns);
            }
        }
        return $table_mapping;
    }
    
    /**
     * Gets the base table name.
     *
     * @return string
     *   The base table name.
     *
     * @internal
     */
    public function getBaseTable() {
        return $this->baseTable;
    }
    
    /**
     * Gets the revision table name.
     *
     * @return string|null
     *   The revision table name.
     *
     * @internal
     */
    public function getRevisionTable() {
        return $this->revisionTable;
    }
    
    /**
     * Gets the data table name.
     *
     * @return string|null
     *   The data table name.
     *
     * @internal
     */
    public function getDataTable() {
        return $this->dataTable;
    }
    
    /**
     * Gets the revision data table name.
     *
     * @return string|null
     *   The revision data table name.
     *
     * @internal
     */
    public function getRevisionDataTable() {
        return $this->revisionDataTable;
    }
    
    /**
     * {@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] = [];
            foreach ($this->getFieldNames($table_name) as $field_name) {
                $this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name)));
            }
            // 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));
            }
        }
        return $this->allColumns[$table_name];
    }
    
    /**
     * {@inheritdoc}
     */
    public function getFieldNames($table_name) {
        if (isset($this->fieldNames[$table_name])) {
            return $this->fieldNames[$table_name];
        }
        return [];
    }
    
    /**
     * {@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.
            $storage_definition = $this->fieldStorageDefinitions[$field_name];
            $table_names = array_filter([
                $this->dataTable,
                $this->baseTable,
                $this->revisionTable,
                $this->getDedicatedDataTableName($storage_definition),
            ]);
            // Collect field columns.
            $field_columns = [];
            foreach (array_keys($storage_definition->getColumns()) as $property_name) {
                $field_columns[] = $this->getFieldColumnName($storage_definition, $property_name);
            }
            foreach ($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)) {
            throw new SqlContentEntityStorageException("Table information not available for the '{$field_name}' field.");
        }
        return $result;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getAllFieldTableNames($field_name) {
        return array_keys(array_filter($this->fieldNames, function ($table_fields) use ($field_name) {
            return in_array($field_name, $table_fields, TRUE);
        }));
    }
    
    /**
     * {@inheritdoc}
     */
    public function getColumnNames($field_name) {
        if (!isset($this->columnMapping[$field_name])) {
            $this->columnMapping[$field_name] = [];
            if (isset($this->fieldStorageDefinitions[$field_name]) && !$this->fieldStorageDefinitions[$field_name]
                ->hasCustomStorage()) {
                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);
                }
            }
        }
        return $this->columnMapping[$field_name];
    }
    
    /**
     * {@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)) {
            if ($property_name == TableMappingInterface::DELTA) {
                $column_name = 'delta';
            }
            else {
                $column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
            }
        }
        else {
            throw new SqlContentEntityStorageException("Column information not available for the '{$field_name}' field.");
        }
        return $column_name;
    }
    
    /**
     * 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
     *
     * @internal
     *
     * @todo Make this method protected in drupal:9.0.0.
     * @see https://www.drupal.org/node/3067336
     */
    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 [];
    }
    
    /**
     * Adds 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
     *
     * @internal
     *
     * @todo Make this method protected in drupal:9.0.0.
     * @see https://www.drupal.org/node/3067336
     */
    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;
    }
    
    /**
     * 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 shared table, FALSE otherwise.
     */
    public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
        return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple() && !$storage_definition->isDeleted();
    }
    
    /**
     * 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 has to be stored in a dedicated table, FALSE otherwise.
     */
    public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
        return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
    }
    
    /**
     * Gets a list of dedicated table names for this mapping.
     *
     * @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;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getReservedColumns() {
        return [
            '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_UNIQUE_STORAGE_ID}. 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_UNIQUE_STORAGE_ID}. 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) {
        // The maximum length of an entity type ID is 32 characters.
        $entity_type_id = substr($storage_definition->getTargetEntityTypeId(), 0, EntityTypeInterface::ID_MAX_LENGTH);
        $separator = $revision ? '_revision__' : '__';
        $table_name = $this->prefix . $entity_type_id . $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 and a hash of the field storage unique
            // identifier.
            $separator = $revision ? '_r__' : '__';
            $field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
            $table_name = $this->prefix . $entity_type_id . $separator . $field_hash;
            // If the resulting table name is still longer than 48 characters, use the
            // following pattern:
            // - prefix: max 34 chars;
            // - separator: max 4 chars;
            // - field_hash: max 10 chars.
            if (strlen($table_name) > 48) {
                $prefix = substr($this->prefix, 0, 34);
                $table_name = $prefix . $separator . $field_hash;
            }
        }
        return $table_name;
    }

}

Classes

Title Deprecated Summary
DefaultTableMapping Defines a default table mapping class.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.