class Tables

Same name in this branch
  1. 11.x core/modules/workspaces/src/EntityQuery/Tables.php \Drupal\workspaces\EntityQuery\Tables
Same name in other branches
  1. 9 core/modules/workspaces/src/EntityQuery/Tables.php \Drupal\workspaces\EntityQuery\Tables
  2. 9 core/lib/Drupal/Core/Entity/Query/Sql/Tables.php \Drupal\Core\Entity\Query\Sql\Tables
  3. 8.9.x core/modules/workspaces/src/EntityQuery/Tables.php \Drupal\workspaces\EntityQuery\Tables
  4. 8.9.x core/lib/Drupal/Core/Entity/Query/Sql/Tables.php \Drupal\Core\Entity\Query\Sql\Tables
  5. 10 core/modules/workspaces/src/EntityQuery/Tables.php \Drupal\workspaces\EntityQuery\Tables
  6. 10 core/lib/Drupal/Core/Entity/Query/Sql/Tables.php \Drupal\Core\Entity\Query\Sql\Tables

Adds database tables and fields to the SQL entity query.

Hierarchy

Expanded class hierarchy of Tables

1 file declares its use of Tables
Tables.php in core/modules/workspaces/src/EntityQuery/Tables.php
1 string reference to 'Tables'
Query::getTables in core/lib/Drupal/Core/Entity/Query/Sql/Query.php
Gets the Tables object for this query.

File

core/lib/Drupal/Core/Entity/Query/Sql/Tables.php, line 17

Namespace

Drupal\Core\Entity\Query\Sql
View source
class Tables implements TablesInterface {
    
    /**
     * @var \Drupal\Core\Database\Query\SelectInterface
     */
    protected $sqlQuery;
    
    /**
     * Entity table array.
     *
     * This array contains at most two entries: one for the data, one for the
     * properties. Its keys are unique references to the tables, values are
     * aliases.
     *
     * @var array
     *
     * @see \Drupal\Core\Entity\Query\Sql\Tables::ensureEntityTable().
     */
    protected $entityTables = [];
    
    /**
     * Field table array, key is table name, value is alias.
     *
     * This array contains one entry per field table.
     *
     * @var array
     */
    protected $fieldTables = [];
    
    /**
     * The entity type manager.
     *
     * @var \Drupal\Core\Entity\EntityTypeManagerInterface
     */
    protected $entityTypeManager;
    
    /**
     * The entity field manager.
     *
     * @var \Drupal\Core\Entity\EntityFieldManagerInterface
     */
    protected $entityFieldManager;
    
    /**
     * List of case sensitive fields.
     *
     * @var array
     */
    protected $caseSensitiveFields = [];
    
    /**
     * @param \Drupal\Core\Database\Query\SelectInterface $sql_query
     *   The SQL query.
     */
    public function __construct(SelectInterface $sql_query) {
        $this->sqlQuery = $sql_query;
        $this->entityTypeManager = \Drupal::entityTypeManager();
        $this->entityFieldManager = \Drupal::service('entity_field.manager');
    }
    
    /**
     * {@inheritdoc}
     */
    public function addField($field, $type, $langcode) {
        $entity_type_id = $this->sqlQuery
            ->getMetaData('entity_type');
        $all_revisions = $this->sqlQuery
            ->getMetaData('all_revisions');
        // This variable ensures grouping works correctly. For example, given the
        // following conditions:
        // ->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 = [];
        $entity_type = $this->entityTypeManager
            ->getActiveDefinition($entity_type_id);
        $field_storage_definitions = $this->entityFieldManager
            ->getActiveFieldStorageDefinitions($entity_type_id);
        for ($key = 0; $key <= $count; $key++) {
            // This can either be the name of an entity base field or a configurable
            // field.
            $specifier = $specifiers[$key];
            if (isset($field_storage_definitions[$specifier])) {
                $field_storage = $field_storage_definitions[$specifier];
                $column = $field_storage->getMainPropertyName();
            }
            else {
                $field_storage = FALSE;
                $column = NULL;
            }
            // If there is revision support, all the revisions are being queried, and
            // the field is revisionable or the revision ID field itself, then use the
            // revision ID. Otherwise, the entity ID will do.
            // Where there is revision support, all the revisions are being queried:
            // so if the field is revisionable then use the revision ID, otherwise use
            // the entity ID.
            $query_revisions = $all_revisions && $field_storage && ($field_storage->isRevisionable() || $field_storage->getName() === $entity_type->getKey('revision'));
            if ($query_revisions) {
                // This contains the relevant SQL field to be used when joining entity
                // tables.
                $entity_id_field = $entity_type->getKey('revision');
                // This contains the relevant SQL field to be used when joining field
                // tables.
                $field_id_field = 'revision_id';
            }
            else {
                $entity_id_field = $entity_type->getKey('id');
                $field_id_field = 'entity_id';
            }
            
            /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
            $table_mapping = $this->entityTypeManager
                ->getStorage($entity_type_id)
                ->getTableMapping();
            // Check whether this field is stored in a dedicated table.
            if ($field_storage && $table_mapping->requiresDedicatedTableStorage($field_storage)) {
                $delta = NULL;
                if ($key < $count) {
                    $next = $specifiers[$key + 1];
                    // If this is a numeric specifier we're adding a condition on the
                    // specific delta.
                    if (is_numeric($next)) {
                        $delta = $next;
                        $index_prefix .= ".{$delta}";
                        // Do not process it again.
                        $key++;
                        $next = $specifiers[$key + 1];
                    }
                    elseif ($next == TableMappingInterface::DELTA) {
                        $index_prefix .= TableMappingInterface::DELTA;
                        // Do not process it again.
                        $key++;
                        // If there are more specifiers to work with then continue
                        // processing. If this is the last specifier then use the reserved
                        // keyword as a column name.
                        if ($key < $count) {
                            $next = $specifiers[$key + 1];
                        }
                        else {
                            $column = TableMappingInterface::DELTA;
                        }
                    }
                    // Is this a field column?
                    $columns = $field_storage->getColumns();
                    if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
                        // 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];
                        $propertyDefinitions = $field_storage->getPropertyDefinitions();
                        // Prepare the next index prefix.
                        $next_index_prefix = "{$relationship_specifier}.{$column}";
                    }
                }
                $table = $this->ensureFieldTable($index_prefix, $field_storage, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta);
                $sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
            }
            else {
                // ensureEntityTable() determines whether an entity property will be
                // queried from the data table or the base table depending on where it
                // first finds the property. The data table is preferred, which is why
                // it gets added before the base table.
                $entity_tables = [];
                $revision_table = NULL;
                if ($query_revisions) {
                    $data_table = $entity_type->getRevisionDataTable();
                    $entity_base_table = $entity_type->getRevisionTable();
                }
                else {
                    $data_table = $entity_type->getDataTable();
                    $entity_base_table = $entity_type->getBaseTable();
                    if ($field_storage && $field_storage->isRevisionable() && in_array($field_storage->getName(), $entity_type->getRevisionMetadataKeys())) {
                        $revision_table = $entity_type->getRevisionTable();
                    }
                }
                if ($data_table) {
                    $this->sqlQuery
                        ->addMetaData('simple_query', FALSE);
                    $entity_tables[$data_table] = $this->getTableMapping($data_table, $entity_type_id);
                }
                if ($revision_table) {
                    $entity_tables[$revision_table] = $this->getTableMapping($revision_table, $entity_type_id);
                }
                $entity_tables[$entity_base_table] = $this->getTableMapping($entity_base_table, $entity_type_id);
                $sql_column = $specifier;
                // If there are more specifiers, get the right sql column name if the
                // next one is a column of this field.
                if ($key < $count) {
                    $next = $specifiers[$key + 1];
                    // If this specifier is the reserved keyword "%delta" we're adding a
                    // condition on a delta range.
                    if ($next == TableMappingInterface::DELTA) {
                        $key++;
                        if ($key < $count) {
                            $next = $specifiers[$key + 1];
                        }
                        else {
                            return 0;
                        }
                    }
                    // If this is a numeric specifier then, add a condition on the
                    // specific delta. Since this is a single value base field, the only
                    // value that makes sense is 0.
                    if (is_numeric($next)) {
                        if ($next > 0) {
                            $this->sqlQuery
                                ->alwaysFalse();
                        }
                        $key++;
                        $next = $specifiers[$key + 1];
                    }
                    // Is this a field column?
                    $columns = $field_storage->getColumns();
                    if (isset($columns[$next]) || in_array($next, $table_mapping->getReservedColumns())) {
                        // Use it.
                        $sql_column = $table_mapping->getFieldColumnName($field_storage, $next);
                        // Do not process it again.
                        $key++;
                    }
                }
                elseif ($field_storage && $column) {
                    $columns = $field_storage->getColumns();
                    if (isset($columns[$column])) {
                        $sql_column = $table_mapping->getFieldColumnName($field_storage, $column);
                    }
                }
                $table = $this->ensureEntityTable($index_prefix, $sql_column, $type, $langcode, $base_table, $entity_id_field, $entity_tables);
            }
            // If there is both a field storage and a field column then check for case
            // sensitivity.
            if ($field_storage && $column) {
                $property_definitions = $field_storage->getPropertyDefinitions();
                if (isset($property_definitions[$column])) {
                    $this->caseSensitiveFields[$field] = $property_definitions[$column]->getSetting('case_sensitive');
                }
            }
            // If there are more specifiers to come, it is a relationship.
            if ($field_storage && $key < $count) {
                // Computed fields have prepared their property definition already, do
                // it for properties as well.
                if (!$propertyDefinitions) {
                    $propertyDefinitions = $field_storage->getPropertyDefinitions();
                    $relationship_specifier = $specifiers[$key + 1];
                    $next_index_prefix = $relationship_specifier;
                }
                $entity_type_id = NULL;
                // Relationship specifier can also contain the entity type ID, i.e.
                // entity:node, entity:user or entity:taxonomy.
                if (str_contains($relationship_specifier, ':')) {
                    [
                        $relationship_specifier,
                        $entity_type_id,
                    ] = explode(':', $relationship_specifier, 2);
                }
                // Check for a valid relationship.
                if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) {
                    // If it is valid then use the entity type if it is already specified,
                    // otherwise use the definition.
                    $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition();
                    if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) {
                        $entity_type_id = $target_definition->getEntityTypeId();
                    }
                    $entity_type = $this->entityTypeManager
                        ->getActiveDefinition($entity_type_id);
                    $field_storage_definitions = $this->entityFieldManager
                        ->getActiveFieldStorageDefinitions($entity_type_id);
                    // Add the new entity base table using the table and sql column.
                    $base_table = $this->addNextBaseTable($entity_type, $table, $sql_column, $field_storage);
                    $propertyDefinitions = [];
                    $key++;
                    $index_prefix .= "{$next_index_prefix}.";
                }
                else {
                    throw new QueryException("Invalid specifier '{$relationship_specifier}'");
                }
            }
        }
        return "{$table}.{$sql_column}";
    }
    
    /**
     * {@inheritdoc}
     */
    public function isFieldCaseSensitive($field_name) {
        if (isset($this->caseSensitiveFields[$field_name])) {
            return $this->caseSensitiveFields[$field_name];
        }
    }
    
    /**
     * Joins the entity table, if necessary, and returns the alias for it.
     *
     * @param string $index_prefix
     *   The table array index prefix. For a base table this will be empty,
     *   for a target entity reference like 'field_tags.entity:taxonomy_term.name'
     *   this will be 'entity:taxonomy_term.target_id.'.
     * @param string $property
     *   The field property/column.
     * @param string $type
     *   The join type, can either be INNER or LEFT.
     * @param string $langcode
     *   The langcode that is used on the join.
     * @param string $base_table
     *   The table to join to. It can be either the table name, its alias or the
     *   'base_table' placeholder.
     * @param string $id_field
     *   The name of the ID field/property for the current entity. For instance:
     *   tid, nid, etc.
     * @param array $entity_tables
     *   Array of entity tables (data and base tables) where the entity property
     *   will be queried from. The first table containing the property will be
     *   used, so the order is important and the data table is always preferred.
     *
     * @return string
     *   The alias of the joined table.
     *
     * @throws \Drupal\Core\Entity\Query\QueryException
     *   When an invalid property has been passed.
     */
    protected function ensureEntityTable($index_prefix, $property, $type, $langcode, $base_table, $id_field, $entity_tables) {
        foreach ($entity_tables as $table => $mapping) {
            if (isset($mapping[$property])) {
                // Ensure a table joined multiple times through different index prefixes
                // has unique entityTables entries by concatenating the index prefix
                // and the base table alias. This way, if the same entity table is
                // joined several times for different entity reference fields, each join
                // gets a unique alias.
                $key = $index_prefix . ($base_table === 'base_table' ? $table : $base_table);
                if (!isset($this->entityTables[$key])) {
                    $this->entityTables[$key] = $this->addJoin($type, $table, "[%alias].[{$id_field}] = [{$base_table}].[{$id_field}]", $langcode);
                }
                return $this->entityTables[$key];
            }
        }
        throw new QueryException("'{$property}' not found");
    }
    
    /**
     * Ensure the field table is joined if necessary.
     *
     * @param string $index_prefix
     *   The table array index prefix. For a base table this will be empty,
     *   for a target entity reference like 'field_tags.entity:taxonomy_term.name'
     *   this will be 'entity:taxonomy_term.target_id.'.
     * @param \Drupal\Core\Field\FieldStorageDefinitionInterface &$field
     *   The field storage definition for the field being joined.
     * @param string $type
     *   The join type.
     * @param string $langcode
     *   The langcode we use on the join.
     * @param string $base_table
     *   The table to join to. It can be either the table name, its alias or the
     *   'base_table' placeholder.
     * @param string $entity_id_field
     *   The name of the ID field/property for the current entity. For instance:
     *   tid, nid, etc.
     * @param string $field_id_field
     *   The column representing the id for the field. For example, 'revision_id'
     *   or 'entity_id'.
     * @param string $delta
     *   A delta which should be used as additional condition.
     *
     * @return string
     *   The alias of the joined table.
     */
    protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $base_table, $entity_id_field, $field_id_field, $delta) {
        $field_name = $field->getName();
        if (!isset($this->fieldTables[$index_prefix . $field_name])) {
            $entity_type_id = $this->sqlQuery
                ->getMetaData('entity_type');
            
            /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
            $table_mapping = $this->entityTypeManager
                ->getStorage($entity_type_id)
                ->getTableMapping();
            $table = !$this->sqlQuery
                ->getMetaData('all_revisions') ? $table_mapping->getDedicatedDataTableName($field) : $table_mapping->getDedicatedRevisionTableName($field);
            if ($field->getCardinality() != 1) {
                $this->sqlQuery
                    ->addMetaData('simple_query', FALSE);
            }
            $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "[%alias].[{$field_id_field}] = [{$base_table}].[{$entity_id_field}]", $langcode, $delta);
        }
        return $this->fieldTables[$index_prefix . $field_name];
    }
    
    /**
     * Adds a join to a given table.
     *
     * @param string $type
     *   The join type.
     * @param string $table
     *   The table to join to.
     * @param string $join_condition
     *   The condition on which to join to.
     * @param string $langcode
     *   The langcode used on the join.
     * @param string|null $delta
     *   (optional) A delta which should be used as additional condition.
     *
     * @return string
     *   Returns the alias of the joined table.
     */
    protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
        $arguments = [];
        if ($langcode) {
            $entity_type_id = $this->sqlQuery
                ->getMetaData('entity_type');
            $entity_type = $this->entityTypeManager
                ->getActiveDefinition($entity_type_id);
            // For a data table, get the entity language key from the entity type.
            // A dedicated field table has a hard-coded 'langcode' column.
            $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode';
            $placeholder = ':langcode' . $this->sqlQuery
                ->nextPlaceholder();
            $join_condition .= ' AND [%alias].[' . $langcode_key . '] = ' . $placeholder;
            $arguments[$placeholder] = $langcode;
        }
        if (isset($delta)) {
            $placeholder = ':delta' . $this->sqlQuery
                ->nextPlaceholder();
            $join_condition .= ' AND [%alias].[delta] = ' . $placeholder;
            $arguments[$placeholder] = $delta;
        }
        return $this->sqlQuery
            ->addJoin($type, $table, NULL, $join_condition, $arguments);
    }
    
    /**
     * Gets the schema for the given table.
     *
     * @param string $table
     *   The table name.
     * @param string $entity_type_id
     *   The entity type ID.
     *
     * @return array|false
     *   An associative array of table field mappings for the given table, keyed
     *   by column name and where values are incrementing integers. If the table
     *   mapping is not available, then FALSE is returned.
     */
    protected function getTableMapping($table, $entity_type_id) {
        $storage = $this->entityTypeManager
            ->getStorage($entity_type_id);
        if ($storage instanceof SqlEntityStorageInterface) {
            $mapping = $storage->getTableMapping()
                ->getAllColumns($table);
        }
        else {
            return FALSE;
        }
        return array_flip($mapping);
    }
    
    /**
     * Add the next entity base table.
     *
     * For example, when building the SQL query for
     * @code
     * condition('uid.entity.name', 'foo', 'CONTAINS')
     * @endcode
     * this adds the users table.
     *
     * @param \Drupal\Core\Entity\EntityType $entity_type
     *   The entity type being joined, in the above example, User.
     * @param string $table
     *   This is the table being joined, in the above example, {users}.
     * @param string $sql_column
     *   This is the SQL column in the existing table being joined to.
     * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage
     *   The field storage definition for the field referencing this column.
     *
     * @return string
     *   The alias of the next entity table joined in.
     */
    protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column, FieldStorageDefinitionInterface $field_storage) {
        $join_condition = '[%alias].[' . $entity_type->getKey('id') . "] = [{$table}].[{$sql_column}]";
        return $this->sqlQuery
            ->leftJoin($entity_type->getBaseTable(), NULL, $join_condition);
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
Tables::$caseSensitiveFields protected property List of case sensitive fields.
Tables::$entityFieldManager protected property The entity field manager.
Tables::$entityTables protected property Entity table array.
Tables::$entityTypeManager protected property The entity type manager.
Tables::$fieldTables protected property Field table array, key is table name, value is alias.
Tables::$sqlQuery protected property
Tables::addField public function Adds a field to a database query. Overrides TablesInterface::addField 1
Tables::addJoin protected function Adds a join to a given table. 1
Tables::addNextBaseTable protected function Add the next entity base table. 1
Tables::ensureEntityTable protected function Joins the entity table, if necessary, and returns the alias for it.
Tables::ensureFieldTable protected function Ensure the field table is joined if necessary.
Tables::getTableMapping protected function Gets the schema for the given table.
Tables::isFieldCaseSensitive public function Determines whether the given field is case sensitive. Overrides TablesInterface::isFieldCaseSensitive
Tables::__construct public function 1

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