You are here

class SqlContentEntityStorage in Zircon Profile 8.0

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php \Drupal\Core\Entity\Sql\SqlContentEntityStorage

A content entity database storage implementation.

This class can be used as-is by most content entity types. Entity types requiring special handling can extend the class.

The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema internally in order to automatically generate the database schema based on the defined base fields. Entity types can override the schema handler to customize the generated schema; e.g., to add additional indexes.

Hierarchy

Expanded class hierarchy of SqlContentEntityStorage

Related topics

14 files declare their use of SqlContentEntityStorage
CommentStorage.php in core/modules/comment/src/CommentStorage.php
Contains \Drupal\comment\CommentStorage.
ContentTranslationTestBase.php in core/modules/content_translation/src/Tests/ContentTranslationTestBase.php
Contains \Drupal\content_translation\Tests\ContentTranslationTestBase.
EntityTestNoLoadStorage.php in core/modules/system/tests/modules/entity_test/src/EntityTestNoLoadStorage.php
Contains \Drupal\entity_test\EntityTestNoLoadStorage.
FeedStorage.php in core/modules/aggregator/src/FeedStorage.php
Contains \Drupal\aggregator\FeedStorage.
FieldDataCountTest.php in core/modules/field/src/Tests/FieldDataCountTest.php
Contains \Drupal\field\Tests\FieldDataCountTest.

... See full list

File

core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php, line 44
Contains \Drupal\Core\Entity\Sql\SqlContentEntityStorage.

Namespace

Drupal\Core\Entity\Sql
View source
class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {

  /**
   * The mapping of field columns to SQL tables.
   *
   * @var \Drupal\Core\Entity\Sql\TableMappingInterface
   */
  protected $tableMapping;

  /**
   * Name of entity's revision database table field, if it supports revisions.
   *
   * Has the value FALSE if this entity does not use revisions.
   *
   * @var string
   */
  protected $revisionKey = FALSE;

  /**
   * The entity langcode key.
   *
   * @var string|bool
   */
  protected $langcodeKey = FALSE;

  /**
   * The default language entity key.
   *
   * @var string
   */
  protected $defaultLangcodeKey = FALSE;

  /**
   * 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 properties, if the entity has multilingual support.
   *
   * @var string
   */
  protected $dataTable;

  /**
   * The table that stores revision field data if the entity supports revisions.
   *
   * @var string
   */
  protected $revisionDataTable;

  /**
   * Active database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The entity type's storage schema object.
   *
   * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
   */
  protected $storageSchema;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static($entity_type, $container
      ->get('database'), $container
      ->get('entity.manager'), $container
      ->get('cache.entity'), $container
      ->get('language_manager'));
  }

  /**
   * Gets the base field definitions for a content entity type.
   *
   * @return \Drupal\Core\Field\FieldDefinitionInterface[]
   *   The array of base field definitions for the entity type, keyed by field
   *   name.
   */
  public function getFieldStorageDefinitions() {
    return $this->entityManager
      ->getBaseFieldDefinitions($this->entityTypeId);
  }

  /**
   * Constructs a SqlContentEntityStorage object.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   The cache backend to be used.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   */
  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
    parent::__construct($entity_type, $entity_manager, $cache);
    $this->database = $database;
    $this->languageManager = $language_manager;
    $this
      ->initTableLayout();
  }

  /**
   * Initializes table name variables.
   */
  protected function initTableLayout() {

    // Reset table field values to ensure changes in the entity type definition
    // are correctly reflected in the table layout.
    $this->tableMapping = NULL;
    $this->revisionKey = NULL;
    $this->revisionTable = NULL;
    $this->dataTable = NULL;
    $this->revisionDataTable = NULL;

    // @todo Remove table names from the entity type definition in
    //   https://www.drupal.org/node/2232465.
    $this->baseTable = $this->entityType
      ->getBaseTable() ?: $this->entityTypeId;
    $revisionable = $this->entityType
      ->isRevisionable();
    if ($revisionable) {
      $this->revisionKey = $this->entityType
        ->getKey('revision') ?: 'revision_id';
      $this->revisionTable = $this->entityType
        ->getRevisionTable() ?: $this->entityTypeId . '_revision';
    }
    $translatable = $this->entityType
      ->isTranslatable();
    if ($translatable) {
      $this->dataTable = $this->entityType
        ->getDataTable() ?: $this->entityTypeId . '_field_data';
      $this->langcodeKey = $this->entityType
        ->getKey('langcode');
      $this->defaultLangcodeKey = $this->entityType
        ->getKey('default_langcode');
    }
    if ($revisionable && $translatable) {
      $this->revisionDataTable = $this->entityType
        ->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
    }
  }

  /**
   * Gets the base table name.
   *
   * @return string
   *   The table name.
   */
  public function getBaseTable() {
    return $this->baseTable;
  }

  /**
   * Gets the revision table name.
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getRevisionTable() {
    return $this->revisionTable;
  }

  /**
   * Gets the data table name.
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getDataTable() {
    return $this->dataTable;
  }

  /**
   * Gets the revision data table name.
   *
   * @return string|false
   *   The table name or FALSE if it is not available.
   */
  public function getRevisionDataTable() {
    return $this->revisionDataTable;
  }

  /**
   * Gets the entity type's storage schema object.
   *
   * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
   *   The schema object.
   */
  protected function getStorageSchema() {
    if (!isset($this->storageSchema)) {
      $class = $this->entityType
        ->getHandlerClass('storage_schema') ?: 'Drupal\\Core\\Entity\\Sql\\SqlContentEntityStorageSchema';
      $this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
    }
    return $this->storageSchema;
  }

  /**
   * Updates the wrapped entity type definition.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The update entity type.
   *
   * @internal Only to be used internally by Entity API. Expected to be
   *   removed by https://www.drupal.org/node/2274017.
   */
  public function setEntityType(EntityTypeInterface $entity_type) {
    if ($this->entityType
      ->id() == $entity_type
      ->id()) {
      $this->entityType = $entity_type;
      $this
        ->initTableLayout();
    }
    else {
      throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getTableMapping(array $storage_definitions = NULL) {
    $table_mapping = $this->tableMapping;

    // If we are using our internal storage definitions, which is our main use
    // case, we can statically cache the computed table mapping. If a new set
    // of field storage definitions is passed, for instance when comparing old
    // and new storage schema, we compute the table mapping without caching.
    // @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
    //   easily instantiate a new table mapping whenever needed.
    if (!isset($this->tableMapping) || $storage_definitions) {
      $definitions = $storage_definitions ?: $this->entityManager
        ->getFieldStorageDefinitions($this->entityTypeId);
      $table_mapping = new DefaultTableMapping($this->entityType, $definitions);
      $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
        return $table_mapping
          ->allowsSharedTableStorage($definition);
      });
      $key_fields = array_values(array_filter(array(
        $this->idKey,
        $this->revisionKey,
        $this->bundleKey,
        $this->uuidKey,
        $this->langcodeKey,
      )));
      $all_fields = array_keys($definitions);
      $revisionable_fields = array_keys(array_filter($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));

      // Nodes have all three of these fields, while custom blocks only have
      // log.
      // @todo Provide automatic definitions for revision metadata fields in
      //   https://www.drupal.org/node/2248983.
      $revision_metadata_fields = array_intersect(array(
        'revision_timestamp',
        'revision_uid',
        'revision_log',
      ), $all_fields);
      $revisionable = $this->entityType
        ->isRevisionable();
      $translatable = $this->entityType
        ->isTranslatable();
      if (!$revisionable && !$translatable) {

        // The base layout stores all the base field values in the base table.
        $table_mapping
          ->setFieldNames($this->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($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
        $revision_key_fields = array(
          $this->idKey,
          $this->revisionKey,
        );
        $table_mapping
          ->setFieldNames($this->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($this->baseTable, $key_fields)
          ->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array(
          $this->uuidKey,
        ))));
      }
      elseif ($revisionable && $translatable) {

        // The revisionable multilingual layout stores key field values in the
        // base table, except for language, which is stored in the revision
        // table 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($this->baseTable, array_values($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, array(
          $this->uuidKey,
        ), $revision_metadata_fields));
        $table_mapping
          ->setFieldNames($this->dataTable, $data_fields);
        $revision_base_fields = array_merge(array(
          $this->idKey,
          $this->revisionKey,
          $this->langcodeKey,
        ), $revision_metadata_fields);
        $table_mapping
          ->setFieldNames($this->revisionTable, $revision_base_fields);
        $revision_data_key_fields = array(
          $this->idKey,
          $this->revisionKey,
          $this->langcodeKey,
        );
        $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array(
          $this->langcodeKey,
        ));
        $table_mapping
          ->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
      }

      // Add dedicated tables.
      $definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
        return $table_mapping
          ->requiresDedicatedTableStorage($definition);
      });
      $extra_columns = array(
        'bundle',
        'deleted',
        'entity_id',
        'revision_id',
        'langcode',
        'delta',
      );
      foreach ($definitions as $field_name => $definition) {
        foreach (array(
          $table_mapping
            ->getDedicatedDataTableName($definition),
          $table_mapping
            ->getDedicatedRevisionTableName($definition),
        ) as $table_name) {
          $table_mapping
            ->setFieldNames($table_name, array(
            $field_name,
          ));
          $table_mapping
            ->setExtraColumns($table_name, $extra_columns);
        }
      }

      // Cache the computed table mapping only if we are using our internal
      // storage definitions.
      if (!$storage_definitions) {
        $this->tableMapping = $table_mapping;
      }
    }
    return $table_mapping;
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadMultiple(array $ids = NULL) {

    // Attempt to load entities from the persistent cache. This will remove IDs
    // that were loaded from $ids.
    $entities_from_cache = $this
      ->getFromPersistentCache($ids);

    // Load any remaining entities from the database.
    if ($entities_from_storage = $this
      ->getFromStorage($ids)) {
      $this
        ->invokeStorageLoadHook($entities_from_storage);
      $this
        ->setPersistentCache($entities_from_storage);
    }
    return $entities_from_cache + $entities_from_storage;
  }

  /**
   * Gets entities from the storage.
   *
   * @param array|null $ids
   *   If not empty, return entities that match these IDs. Return all entities
   *   when NULL.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface[]
   *   Array of entities from the storage.
   */
  protected function getFromStorage(array $ids = NULL) {
    $entities = array();
    if (!empty($ids)) {

      // Sanitize IDs. Before feeding ID array into buildQuery, check whether
      // it is empty as this would load all entities.
      $ids = $this
        ->cleanIds($ids);
    }
    if ($ids === NULL || $ids) {

      // Build and execute the query.
      $query_result = $this
        ->buildQuery($ids)
        ->execute();
      $records = $query_result
        ->fetchAllAssoc($this->idKey);

      // Map the loaded records into entity objects and according fields.
      if ($records) {
        $entities = $this
          ->mapFromStorageRecords($records);
      }
    }
    return $entities;
  }

  /**
   * Maps from storage records to entity objects, and attaches fields.
   *
   * @param array $records
   *   Associative array of query results, keyed on the entity ID.
   * @param bool $load_from_revision
   *   Flag to indicate whether revisions should be loaded or not.
   *
   * @return array
   *   An array of entity objects implementing the EntityInterface.
   */
  protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
    if (!$records) {
      return array();
    }
    $values = array();
    foreach ($records as $id => $record) {
      $values[$id] = array();

      // Skip the item delta and item value levels (if possible) but let the
      // field assign the value as suiting. This avoids unnecessary array
      // hierarchies and saves memory here.
      foreach ($record as $name => $value) {

        // Handle columns named [field_name]__[column_name] (e.g for field types
        // that store several properties).
        if ($field_name = strstr($name, '__', TRUE)) {
          $property_name = substr($name, strpos($name, '__') + 2);
          $values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
        }
        else {

          // Handle columns named directly after the field (e.g if the field
          // type only stores one property).
          $values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
        }
      }
    }

    // Initialize translations array.
    $translations = array_fill_keys(array_keys($values), array());

    // Load values from shared and dedicated tables.
    $this
      ->loadFromSharedTables($values, $translations);
    $this
      ->loadFromDedicatedTables($values, $load_from_revision);
    $entities = array();
    foreach ($values as $id => $entity_values) {
      $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : FALSE;

      // Turn the record into an entity class.
      $entities[$id] = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
    }
    return $entities;
  }

  /**
   * Loads values for fields stored in the shared data tables.
   *
   * @param array &$values
   *   Associative array of entities values, keyed on the entity ID.
   * @param array &$translations
   *   List of translations, keyed on the entity ID.
   */
  protected function loadFromSharedTables(array &$values, array &$translations) {
    if ($this->dataTable) {

      // If a revision table is available, we need all the properties of the
      // latest revision. Otherwise we fall back to the data table.
      $table = $this->revisionDataTable ?: $this->dataTable;
      $alias = $this->revisionDataTable ? 'revision' : 'data';
      $query = $this->database
        ->select($table, $alias, array(
        'fetch' => \PDO::FETCH_ASSOC,
      ))
        ->fields($alias)
        ->condition($alias . '.' . $this->idKey, array_keys($values), 'IN')
        ->orderBy($alias . '.' . $this->idKey);
      $table_mapping = $this
        ->getTableMapping();
      if ($this->revisionDataTable) {

        // Find revisioned fields that are not entity keys. Exclude the langcode
        // key as the base table holds only the default language.
        $base_fields = array_diff($table_mapping
          ->getFieldNames($this->baseTable), array(
          $this->langcodeKey,
        ));
        $fields = array_diff($table_mapping
          ->getFieldNames($this->revisionDataTable), $base_fields);

        // Find fields that are not revisioned or entity keys. Data fields have
        // the same value regardless of entity revision.
        $data_fields = array_diff($table_mapping
          ->getFieldNames($this->dataTable), $fields, $base_fields);
        if ($data_fields) {
          $fields = array_merge($fields, $data_fields);
          $query
            ->leftJoin($this->dataTable, 'data', "(revision.{$this->idKey} = data.{$this->idKey})");
          $query
            ->fields('data', $data_fields);
        }

        // Get the revision IDs.
        $revision_ids = array();
        foreach ($values as $entity_values) {
          $revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
        }
        $query
          ->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
      }
      else {
        $fields = $table_mapping
          ->getFieldNames($this->dataTable);
      }
      $result = $query
        ->execute();
      foreach ($result as $row) {
        $id = $row[$this->idKey];

        // Field values in default language are stored with
        // LanguageInterface::LANGCODE_DEFAULT as key.
        $langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
        $translations[$id][$langcode] = TRUE;
        foreach ($fields as $field_name) {
          $columns = $table_mapping
            ->getColumnNames($field_name);

          // Do not key single-column fields by property name.
          if (count($columns) == 1) {
            $values[$id][$field_name][$langcode] = $row[reset($columns)];
          }
          else {
            foreach ($columns as $property_name => $column_name) {
              $values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
            }
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadRevisionFieldItems($revision_id) {
    $revision = NULL;

    // Build and execute the query.
    $query_result = $this
      ->buildQuery(array(), $revision_id)
      ->execute();
    $records = $query_result
      ->fetchAllAssoc($this->idKey);
    if (!empty($records)) {

      // Convert the raw records to entity objects.
      $entities = $this
        ->mapFromStorageRecords($records, TRUE);
      $revision = reset($entities) ?: NULL;
    }
    return $revision;
  }

  /**
   * {@inheritdoc}
   */
  protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
    $this->database
      ->delete($this->revisionTable)
      ->condition($this->revisionKey, $revision
      ->getRevisionId())
      ->execute();
    $this
      ->deleteRevisionFromDedicatedTables($revision);
  }

  /**
   * {@inheritdoc}
   */
  protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
    if ($this->dataTable) {

      // @todo We should not be using a condition to specify whether conditions
      //   apply to the default language. See
      //   https://www.drupal.org/node/1866330.
      // Default to the original entity language if not explicitly specified
      // otherwise.
      if (!array_key_exists($this->defaultLangcodeKey, $values)) {
        $values[$this->defaultLangcodeKey] = 1;
      }
      elseif ($values[$this->defaultLangcodeKey] === NULL) {
        unset($values[$this->defaultLangcodeKey]);
      }
    }
    parent::buildPropertyQuery($entity_query, $values);
  }

  /**
   * Builds the query to load the entity.
   *
   * This has full revision support. For entities requiring special queries,
   * the class can be extended, and the default query can be constructed by
   * calling parent::buildQuery(). This is usually necessary when the object
   * being loaded needs to be augmented with additional data from another
   * table, such as loading node type into comments or vocabulary machine name
   * into terms, however it can also support $conditions on different tables.
   * See Drupal\comment\CommentStorage::buildQuery() for an example.
   *
   * @param array|null $ids
   *   An array of entity IDs, or NULL to load all entities.
   * @param $revision_id
   *   The ID of the revision to load, or FALSE if this query is asking for the
   *   most current revision(s).
   *
   * @return \Drupal\Core\Database\Query\Select
   *   A SelectQuery object for loading the entity.
   */
  protected function buildQuery($ids, $revision_id = FALSE) {
    $query = $this->database
      ->select($this->entityType
      ->getBaseTable(), 'base');
    $query
      ->addTag($this->entityTypeId . '_load_multiple');
    if ($revision_id) {
      $query
        ->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(
        ':revisionId' => $revision_id,
      ));
    }
    elseif ($this->revisionTable) {
      $query
        ->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
    }

    // Add fields from the {entity} table.
    $table_mapping = $this
      ->getTableMapping();
    $entity_fields = $table_mapping
      ->getAllColumns($this->baseTable);
    if ($this->revisionTable) {

      // Add all fields from the {entity_revision} table.
      $entity_revision_fields = $table_mapping
        ->getAllColumns($this->revisionTable);
      $entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);

      // The ID field is provided by entity, so remove it.
      unset($entity_revision_fields[$this->idKey]);

      // Remove all fields from the base table that are also fields by the same
      // name in the revision table.
      $entity_field_keys = array_flip($entity_fields);
      foreach ($entity_revision_fields as $name) {
        if (isset($entity_field_keys[$name])) {
          unset($entity_fields[$entity_field_keys[$name]]);
        }
      }
      $query
        ->fields('revision', $entity_revision_fields);

      // Compare revision ID of the base and revision table, if equal then this
      // is the default revision.
      $query
        ->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
    }
    $query
      ->fields('base', $entity_fields);
    if ($ids) {
      $query
        ->condition("base.{$this->idKey}", $ids, 'IN');
    }
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function delete(array $entities) {
    if (!$entities) {

      // If no IDs or invalid IDs were passed, do nothing.
      return;
    }
    $transaction = $this->database
      ->startTransaction();
    try {
      parent::delete($entities);

      // Ignore replica server temporarily.
      db_ignore_replica();
    } catch (\Exception $e) {
      $transaction
        ->rollback();
      watchdog_exception($this->entityTypeId, $e);
      throw new EntityStorageException($e
        ->getMessage(), $e
        ->getCode(), $e);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doDeleteFieldItems($entities) {
    $ids = array_keys($entities);
    $this->database
      ->delete($this->entityType
      ->getBaseTable())
      ->condition($this->idKey, $ids, 'IN')
      ->execute();
    if ($this->revisionTable) {
      $this->database
        ->delete($this->revisionTable)
        ->condition($this->idKey, $ids, 'IN')
        ->execute();
    }
    if ($this->dataTable) {
      $this->database
        ->delete($this->dataTable)
        ->condition($this->idKey, $ids, 'IN')
        ->execute();
    }
    if ($this->revisionDataTable) {
      $this->database
        ->delete($this->revisionDataTable)
        ->condition($this->idKey, $ids, 'IN')
        ->execute();
    }
    foreach ($entities as $entity) {
      $this
        ->deleteFromDedicatedTables($entity);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function save(EntityInterface $entity) {
    $transaction = $this->database
      ->startTransaction();
    try {
      $return = parent::save($entity);

      // Ignore replica server temporarily.
      db_ignore_replica();
      return $return;
    } catch (\Exception $e) {
      $transaction
        ->rollback();
      watchdog_exception($this->entityTypeId, $e);
      throw new EntityStorageException($e
        ->getMessage(), $e
        ->getCode(), $e);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
    $full_save = empty($names);
    $update = !$full_save || !$entity
      ->isNew();
    if ($full_save) {
      $shared_table_fields = TRUE;
      $dedicated_table_fields = TRUE;
    }
    else {
      $table_mapping = $this
        ->getTableMapping();
      $storage_definitions = $this->entityManager
        ->getFieldStorageDefinitions($this->entityTypeId);
      $shared_table_fields = FALSE;
      $dedicated_table_fields = [];

      // Collect the name of fields to be written in dedicated tables and check
      // whether shared table records need to be updated.
      foreach ($names as $name) {
        $storage_definition = $storage_definitions[$name];
        if ($table_mapping
          ->allowsSharedTableStorage($storage_definition)) {
          $shared_table_fields = TRUE;
        }
        elseif ($table_mapping
          ->requiresDedicatedTableStorage($storage_definition)) {
          $dedicated_table_fields[] = $name;
        }
      }
    }

    // Update shared table records if necessary.
    if ($shared_table_fields) {
      $record = $this
        ->mapToStorageRecord($entity
        ->getUntranslated(), $this->baseTable);

      // Create the storage record to be saved.
      if ($update) {
        $default_revision = $entity
          ->isDefaultRevision();
        if ($default_revision) {
          $this->database
            ->update($this->baseTable)
            ->fields((array) $record)
            ->condition($this->idKey, $record->{$this->idKey})
            ->execute();
        }
        if ($this->revisionTable) {
          if ($full_save) {
            $entity->{$this->revisionKey} = $this
              ->saveRevision($entity);
          }
          else {
            $record = $this
              ->mapToStorageRecord($entity
              ->getUntranslated(), $this->revisionTable);
            $entity
              ->preSaveRevision($this, $record);
            $this->database
              ->update($this->revisionTable)
              ->fields((array) $record)
              ->condition($this->revisionKey, $record->{$this->revisionKey})
              ->execute();
          }
        }
        if ($default_revision && $this->dataTable) {
          $this
            ->saveToSharedTables($entity);
        }
        if ($this->revisionDataTable) {
          $new_revision = $full_save && $entity
            ->isNewRevision();
          $this
            ->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
        }
      }
      else {
        $insert_id = $this->database
          ->insert($this->baseTable, array(
          'return' => Database::RETURN_INSERT_ID,
        ))
          ->fields((array) $record)
          ->execute();

        // Even if this is a new entity the ID key might have been set, in which
        // case we should not override the provided ID. An ID key that is not set
        // to any value is interpreted as NULL (or DEFAULT) and thus overridden.
        if (!isset($record->{$this->idKey})) {
          $record->{$this->idKey} = $insert_id;
        }
        $entity->{$this->idKey} = (string) $record->{$this->idKey};
        if ($this->revisionTable) {
          $record->{$this->revisionKey} = $this
            ->saveRevision($entity);
        }
        if ($this->dataTable) {
          $this
            ->saveToSharedTables($entity);
        }
        if ($this->revisionDataTable) {
          $this
            ->saveToSharedTables($entity, $this->revisionDataTable);
        }
      }
    }

    // Update dedicated table records if necessary.
    if ($dedicated_table_fields) {
      $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
      $this
        ->saveToDedicatedTables($entity, $update, $names);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function has($id, EntityInterface $entity) {
    return !$entity
      ->isNew();
  }

  /**
   * Saves fields that use the shared tables.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity object.
   * @param string $table_name
   *   (optional) The table name to save to. Defaults to the data table.
   * @param bool $new_revision
   *   (optional) Whether we are dealing with a new revision. By default fetches
   *   the information from the entity object.
   */
  protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) {
    if (!isset($table_name)) {
      $table_name = $this->dataTable;
    }
    if (!isset($new_revision)) {
      $new_revision = $entity
        ->isNewRevision();
    }
    $revision = $table_name != $this->dataTable;
    if (!$revision || !$new_revision) {
      $key = $revision ? $this->revisionKey : $this->idKey;
      $value = $revision ? $entity
        ->getRevisionId() : $entity
        ->id();

      // Delete and insert to handle removed values.
      $this->database
        ->delete($table_name)
        ->condition($key, $value)
        ->execute();
    }
    $query = $this->database
      ->insert($table_name);
    foreach ($entity
      ->getTranslationLanguages() as $langcode => $language) {
      $translation = $entity
        ->getTranslation($langcode);
      $record = $this
        ->mapToDataStorageRecord($translation, $table_name);
      $values = (array) $record;
      $query
        ->fields(array_keys($values))
        ->values($values);
    }
    $query
      ->execute();
  }

  /**
   * Maps from an entity object to the storage record.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity object.
   * @param string $table_name
   *   (optional) The table name to map records to. Defaults to the base table.
   *
   * @return \stdClass
   *   The record to store.
   */
  protected function mapToStorageRecord(ContentEntityInterface $entity, $table_name = NULL) {
    if (!isset($table_name)) {
      $table_name = $this->baseTable;
    }
    $record = new \stdClass();
    $table_mapping = $this
      ->getTableMapping();
    foreach ($table_mapping
      ->getFieldNames($table_name) as $field_name) {
      if (empty($this
        ->getFieldStorageDefinitions()[$field_name])) {
        throw new EntityStorageException("Table mapping contains invalid field {$field_name}.");
      }
      $definition = $this
        ->getFieldStorageDefinitions()[$field_name];
      $columns = $table_mapping
        ->getColumnNames($field_name);
      foreach ($columns as $column_name => $schema_name) {

        // If there is no main property and only a single column, get all
        // properties from the first field item and assume that they will be
        // stored serialized.
        // @todo Give field types more control over this behavior in
        //   https://www.drupal.org/node/2232427.
        if (!$definition
          ->getMainPropertyName() && count($columns) == 1) {
          $value = ($item = $entity->{$field_name}
            ->first()) ? $item
            ->getValue() : array();
        }
        else {
          $value = isset($entity->{$field_name}->{$column_name}) ? $entity->{$field_name}->{$column_name} : NULL;
        }
        if (!empty($definition
          ->getSchema()['columns'][$column_name]['serialize'])) {
          $value = serialize($value);
        }

        // Do not set serial fields if we do not have a value. This supports all
        // SQL database drivers.
        // @see https://www.drupal.org/node/2279395
        $value = drupal_schema_get_field_value($definition
          ->getSchema()['columns'][$column_name], $value);
        if (!(empty($value) && $this
          ->isColumnSerial($table_name, $schema_name))) {
          $record->{$schema_name} = $value;
        }
      }
    }
    return $record;
  }

  /**
   * Checks whether a field column should be treated as serial.
   *
   * @param $table_name
   *   The name of the table the field column belongs to.
   * @param $schema_name
   *   The schema name of the field column.
   *
   * @return bool
   *   TRUE if the column is serial, FALSE otherwise.
   *
   * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processBaseTable()
   * @see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::processRevisionTable()
   */
  protected function isColumnSerial($table_name, $schema_name) {
    $result = FALSE;
    switch ($table_name) {
      case $this->baseTable:
        $result = $schema_name == $this->idKey;
        break;
      case $this->revisionTable:
        $result = $schema_name == $this->revisionKey;
        break;
    }
    return $result;
  }

  /**
   * Maps from an entity object to the storage record of the field data.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   * @param string $table_name
   *   (optional) The table name to map records to. Defaults to the data table.
   *
   * @return \stdClass
   *   The record to store.
   */
  protected function mapToDataStorageRecord(EntityInterface $entity, $table_name = NULL) {
    if (!isset($table_name)) {
      $table_name = $this->dataTable;
    }
    $record = $this
      ->mapToStorageRecord($entity, $table_name);
    return $record;
  }

  /**
   * Saves an entity revision.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity object.
   *
   * @return int
   *   The revision id.
   */
  protected function saveRevision(ContentEntityInterface $entity) {
    $record = $this
      ->mapToStorageRecord($entity
      ->getUntranslated(), $this->revisionTable);
    $entity
      ->preSaveRevision($this, $record);
    if ($entity
      ->isNewRevision()) {
      $insert_id = $this->database
        ->insert($this->revisionTable, array(
        'return' => Database::RETURN_INSERT_ID,
      ))
        ->fields((array) $record)
        ->execute();

      // Even if this is a new revision, the revision ID key might have been
      // set in which case we should not override the provided revision ID.
      if (!isset($record->{$this->revisionKey})) {
        $record->{$this->revisionKey} = $insert_id;
      }
      if ($entity
        ->isDefaultRevision()) {
        $this->database
          ->update($this->entityType
          ->getBaseTable())
          ->fields(array(
          $this->revisionKey => $record->{$this->revisionKey},
        ))
          ->condition($this->idKey, $record->{$this->idKey})
          ->execute();
      }
    }
    else {
      $this->database
        ->update($this->revisionTable)
        ->fields((array) $record)
        ->condition($this->revisionKey, $record->{$this->revisionKey})
        ->execute();
    }

    // Make sure to update the new revision key for the entity.
    $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
    return $record->{$this->revisionKey};
  }

  /**
   * {@inheritdoc}
   */
  protected function getQueryServiceName() {
    return 'entity.query.sql';
  }

  /**
   * Loads values of fields stored in dedicated tables for a group of entities.
   *
   * @param array &$values
   *   An array of values keyed by entity ID.
   * @param bool $load_from_revision
   *   (optional) Flag to indicate whether revisions should be loaded or not,
   *   defaults to FALSE.
   */
  protected function loadFromDedicatedTables(array &$values, $load_from_revision) {
    if (empty($values)) {
      return;
    }

    // Collect entities ids, bundles and languages.
    $bundles = array();
    $ids = array();
    $default_langcodes = array();
    foreach ($values as $key => $entity_values) {
      $bundles[$this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : $this->entityTypeId] = TRUE;
      $ids[] = !$load_from_revision ? $key : $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
      if ($this->langcodeKey && isset($entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT])) {
        $default_langcodes[$key] = $entity_values[$this->langcodeKey][LanguageInterface::LANGCODE_DEFAULT];
      }
    }

    // Collect impacted fields.
    $storage_definitions = array();
    $definitions = array();
    $table_mapping = $this
      ->getTableMapping();
    foreach ($bundles as $bundle => $v) {
      $definitions[$bundle] = $this->entityManager
        ->getFieldDefinitions($this->entityTypeId, $bundle);
      foreach ($definitions[$bundle] as $field_name => $field_definition) {
        $storage_definition = $field_definition
          ->getFieldStorageDefinition();
        if ($table_mapping
          ->requiresDedicatedTableStorage($storage_definition)) {
          $storage_definitions[$field_name] = $storage_definition;
        }
      }
    }

    // Load field data.
    $langcodes = array_keys($this->languageManager
      ->getLanguages(LanguageInterface::STATE_ALL));
    foreach ($storage_definitions as $field_name => $storage_definition) {
      $table = !$load_from_revision ? $table_mapping
        ->getDedicatedDataTableName($storage_definition) : $table_mapping
        ->getDedicatedRevisionTableName($storage_definition);

      // Ensure that only values having valid languages are retrieved. Since we
      // are loading values for multiple entities, we cannot limit the query to
      // the available translations.
      $results = $this->database
        ->select($table, 't')
        ->fields('t')
        ->condition(!$load_from_revision ? 'entity_id' : 'revision_id', $ids, 'IN')
        ->condition('deleted', 0)
        ->condition('langcode', $langcodes, 'IN')
        ->orderBy('delta')
        ->execute();
      foreach ($results as $row) {
        $bundle = $row->bundle;

        // Field values in default language are stored with
        // LanguageInterface::LANGCODE_DEFAULT as key.
        $langcode = LanguageInterface::LANGCODE_DEFAULT;
        if ($this->langcodeKey && isset($default_langcodes[$row->entity_id]) && $row->langcode != $default_langcodes[$row->entity_id]) {
          $langcode = $row->langcode;
        }
        if (!isset($values[$row->entity_id][$field_name][$langcode])) {
          $values[$row->entity_id][$field_name][$langcode] = array();
        }

        // Ensure that records for non-translatable fields having invalid
        // languages are skipped.
        if ($langcode == LanguageInterface::LANGCODE_DEFAULT || $definitions[$bundle][$field_name]
          ->isTranslatable()) {
          if ($storage_definition
            ->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($values[$row->entity_id][$field_name][$langcode]) < $storage_definition
            ->getCardinality()) {
            $item = array();

            // For each column declared by the field, populate the item from the
            // prefixed database column.
            foreach ($storage_definition
              ->getColumns() as $column => $attributes) {
              $column_name = $table_mapping
                ->getFieldColumnName($storage_definition, $column);

              // Unserialize the value if specified in the column schema.
              $item[$column] = !empty($attributes['serialize']) ? unserialize($row->{$column_name}) : $row->{$column_name};
            }

            // Add the item to the field values for the entity.
            $values[$row->entity_id][$field_name][$langcode][] = $item;
          }
        }
      }
    }
  }

  /**
   * Saves values of fields that use dedicated tables.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity.
   * @param bool $update
   *   TRUE if the entity is being updated, FALSE if it is being inserted.
   * @param string[] $names
   *   (optional) The names of the fields to be stored. Defaults to all the
   *   available fields.
   */
  protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = array()) {
    $vid = $entity
      ->getRevisionId();
    $id = $entity
      ->id();
    $bundle = $entity
      ->bundle();
    $entity_type = $entity
      ->getEntityTypeId();
    $default_langcode = $entity
      ->getUntranslated()
      ->language()
      ->getId();
    $translation_langcodes = array_keys($entity
      ->getTranslationLanguages());
    $table_mapping = $this
      ->getTableMapping();
    if (!isset($vid)) {
      $vid = $id;
    }
    $original = !empty($entity->original) ? $entity->original : NULL;

    // Determine which fields should be actually stored.
    $definitions = $this->entityManager
      ->getFieldDefinitions($entity_type, $bundle);
    if ($names) {
      $definitions = array_intersect_key($definitions, array_flip($names));
    }
    foreach ($definitions as $field_name => $field_definition) {
      $storage_definition = $field_definition
        ->getFieldStorageDefinition();
      if (!$table_mapping
        ->requiresDedicatedTableStorage($storage_definition)) {
        continue;
      }

      // When updating an existing revision, keep the existing records if the
      // field values did not change.
      if (!$entity
        ->isNewRevision() && $original && !$this
        ->hasFieldValueChanged($field_definition, $entity, $original)) {
        continue;
      }
      $table_name = $table_mapping
        ->getDedicatedDataTableName($storage_definition);
      $revision_name = $table_mapping
        ->getDedicatedRevisionTableName($storage_definition);

      // Delete and insert, rather than update, in case a value was added.
      if ($update) {

        // Only overwrite the field's base table if saving the default revision
        // of an entity.
        if ($entity
          ->isDefaultRevision()) {
          $this->database
            ->delete($table_name)
            ->condition('entity_id', $id)
            ->execute();
        }
        if ($this->entityType
          ->isRevisionable()) {
          $this->database
            ->delete($revision_name)
            ->condition('entity_id', $id)
            ->condition('revision_id', $vid)
            ->execute();
        }
      }

      // Prepare the multi-insert query.
      $do_insert = FALSE;
      $columns = array(
        'entity_id',
        'revision_id',
        'bundle',
        'delta',
        'langcode',
      );
      foreach ($storage_definition
        ->getColumns() as $column => $attributes) {
        $columns[] = $table_mapping
          ->getFieldColumnName($storage_definition, $column);
      }
      $query = $this->database
        ->insert($table_name)
        ->fields($columns);
      if ($this->entityType
        ->isRevisionable()) {
        $revision_query = $this->database
          ->insert($revision_name)
          ->fields($columns);
      }
      $langcodes = $field_definition
        ->isTranslatable() ? $translation_langcodes : array(
        $default_langcode,
      );
      foreach ($langcodes as $langcode) {
        $delta_count = 0;
        $items = $entity
          ->getTranslation($langcode)
          ->get($field_name);
        $items
          ->filterEmptyItems();
        foreach ($items as $delta => $item) {

          // We now know we have something to insert.
          $do_insert = TRUE;
          $record = array(
            'entity_id' => $id,
            'revision_id' => $vid,
            'bundle' => $bundle,
            'delta' => $delta,
            'langcode' => $langcode,
          );
          foreach ($storage_definition
            ->getColumns() as $column => $attributes) {
            $column_name = $table_mapping
              ->getFieldColumnName($storage_definition, $column);

            // Serialize the value if specified in the column schema.
            $record[$column_name] = !empty($attributes['serialize']) ? serialize($item->{$column}) : $item->{$column};
          }
          $query
            ->values($record);
          if ($this->entityType
            ->isRevisionable()) {
            $revision_query
              ->values($record);
          }
          if ($storage_definition
            ->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition
            ->getCardinality()) {
            break;
          }
        }
      }

      // Execute the query if we have values to insert.
      if ($do_insert) {

        // Only overwrite the field's base table if saving the default revision
        // of an entity.
        if ($entity
          ->isDefaultRevision()) {
          $query
            ->execute();
        }
        if ($this->entityType
          ->isRevisionable()) {
          $revision_query
            ->execute();
        }
      }
    }
  }

  /**
   * Deletes values of fields in dedicated tables for all revisions.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity.
   */
  protected function deleteFromDedicatedTables(ContentEntityInterface $entity) {
    $table_mapping = $this
      ->getTableMapping();
    foreach ($this->entityManager
      ->getFieldDefinitions($entity
      ->getEntityTypeId(), $entity
      ->bundle()) as $field_definition) {
      $storage_definition = $field_definition
        ->getFieldStorageDefinition();
      if (!$table_mapping
        ->requiresDedicatedTableStorage($storage_definition)) {
        continue;
      }
      $table_name = $table_mapping
        ->getDedicatedDataTableName($storage_definition);
      $revision_name = $table_mapping
        ->getDedicatedRevisionTableName($storage_definition);
      $this->database
        ->delete($table_name)
        ->condition('entity_id', $entity
        ->id())
        ->execute();
      if ($this->entityType
        ->isRevisionable()) {
        $this->database
          ->delete($revision_name)
          ->condition('entity_id', $entity
          ->id())
          ->execute();
      }
    }
  }

  /**
   * Deletes values of fields in dedicated tables for all revisions.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity. It must have a revision ID.
   */
  protected function deleteRevisionFromDedicatedTables(ContentEntityInterface $entity) {
    $vid = $entity
      ->getRevisionId();
    if (isset($vid)) {
      $table_mapping = $this
        ->getTableMapping();
      foreach ($this->entityManager
        ->getFieldDefinitions($entity
        ->getEntityTypeId(), $entity
        ->bundle()) as $field_definition) {
        $storage_definition = $field_definition
          ->getFieldStorageDefinition();
        if (!$table_mapping
          ->requiresDedicatedTableStorage($storage_definition)) {
          continue;
        }
        $revision_name = $table_mapping
          ->getDedicatedRevisionTableName($storage_definition);
        $this->database
          ->delete($revision_name)
          ->condition('entity_id', $entity
          ->id())
          ->condition('revision_id', $vid)
          ->execute();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
    return $this
      ->getStorageSchema()
      ->requiresEntityStorageSchemaChanges($entity_type, $original);
  }

  /**
   * {@inheritdoc}
   */
  public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
    return $this
      ->getStorageSchema()
      ->requiresFieldStorageSchemaChanges($storage_definition, $original);
  }

  /**
   * {@inheritdoc}
   */
  public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
    return $this
      ->getStorageSchema()
      ->requiresEntityDataMigration($entity_type, $original);
  }

  /**
   * {@inheritdoc}
   */
  public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
    return $this
      ->getStorageSchema()
      ->requiresFieldDataMigration($storage_definition, $original);
  }

  /**
   * {@inheritdoc}
   */
  public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
    $this
      ->wrapSchemaException(function () use ($entity_type) {
      $this
        ->getStorageSchema()
        ->onEntityTypeCreate($entity_type);
    });
  }

  /**
   * {@inheritdoc}
   */
  public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {

    // Ensure we have an updated entity type definition.
    $this->entityType = $entity_type;

    // The table layout may have changed depending on the new entity type
    // definition.
    $this
      ->initTableLayout();

    // Let the schema handler adapt to possible table layout changes.
    $this
      ->wrapSchemaException(function () use ($entity_type, $original) {
      $this
        ->getStorageSchema()
        ->onEntityTypeUpdate($entity_type, $original);
    });
  }

  /**
   * {@inheritdoc}
   */
  public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
    $this
      ->wrapSchemaException(function () use ($entity_type) {
      $this
        ->getStorageSchema()
        ->onEntityTypeDelete($entity_type);
    });
  }

  /**
   * {@inheritdoc}
   */
  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {

    // If we are adding a field stored in a shared table we need to recompute
    // the table mapping.
    // @todo This does not belong here. Remove it once we are able to generate a
    //   fresh table mapping in the schema handler. See
    //   https://www.drupal.org/node/2274017.
    if ($this
      ->getTableMapping()
      ->allowsSharedTableStorage($storage_definition)) {
      $this->tableMapping = NULL;
    }
    $this
      ->wrapSchemaException(function () use ($storage_definition) {
      $this
        ->getStorageSchema()
        ->onFieldStorageDefinitionCreate($storage_definition);
    });
  }

  /**
   * {@inheritdoc}
   */
  public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
    $this
      ->wrapSchemaException(function () use ($storage_definition, $original) {
      $this
        ->getStorageSchema()
        ->onFieldStorageDefinitionUpdate($storage_definition, $original);
    });
  }

  /**
   * {@inheritdoc}
   */
  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
    $table_mapping = $this
      ->getTableMapping($this->entityManager
      ->getLastInstalledFieldStorageDefinitions($this->entityType
      ->id()));

    // @todo Remove the FieldStorageConfigInterface check when non-configurable
    //   fields support purging: https://www.drupal.org/node/2282119.
    if ($storage_definition instanceof FieldStorageConfigInterface && $table_mapping
      ->requiresDedicatedTableStorage($storage_definition)) {

      // Mark all data associated with the field for deletion.
      $table = $table_mapping
        ->getDedicatedDataTableName($storage_definition);
      $revision_table = $table_mapping
        ->getDedicatedRevisionTableName($storage_definition);
      $this->database
        ->update($table)
        ->fields(array(
        'deleted' => 1,
      ))
        ->execute();
      if ($this->entityType
        ->isRevisionable()) {
        $this->database
          ->update($revision_table)
          ->fields(array(
          'deleted' => 1,
        ))
          ->execute();
      }
    }

    // Update the field schema.
    $this
      ->wrapSchemaException(function () use ($storage_definition) {
      $this
        ->getStorageSchema()
        ->onFieldStorageDefinitionDelete($storage_definition);
    });
  }

  /**
   * Wraps a database schema exception into an entity storage exception.
   *
   * @param callable $callback
   *   The callback to be executed.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   When a database schema exception is thrown.
   */
  protected function wrapSchemaException(callable $callback) {
    $message = 'Exception thrown while performing a schema update.';
    try {
      $callback();
    } catch (SchemaException $e) {
      $message .= ' ' . $e
        ->getMessage();
      throw new EntityStorageException($message, 0, $e);
    } catch (DatabaseExceptionWrapper $e) {
      $message .= ' ' . $e
        ->getMessage();
      throw new EntityStorageException($message, 0, $e);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definition) {
    $table_mapping = $this
      ->getTableMapping();
    $storage_definition = $field_definition
      ->getFieldStorageDefinition();

    // Mark field data as deleted.
    if ($table_mapping
      ->requiresDedicatedTableStorage($storage_definition)) {
      $table_name = $table_mapping
        ->getDedicatedDataTableName($storage_definition);
      $revision_name = $table_mapping
        ->getDedicatedRevisionTableName($storage_definition);
      $this->database
        ->update($table_name)
        ->fields(array(
        'deleted' => 1,
      ))
        ->condition('bundle', $field_definition
        ->getTargetBundle())
        ->execute();
      if ($this->entityType
        ->isRevisionable()) {
        $this->database
          ->update($revision_name)
          ->fields(array(
          'deleted' => 1,
        ))
          ->condition('bundle', $field_definition
          ->getTargetBundle())
          ->execute();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function onBundleCreate($bundle, $entity_type_id) {
  }

  /**
   * {@inheritdoc}
   */
  public function onBundleDelete($bundle, $entity_type_id) {
  }

  /**
   * {@inheritdoc}
   */
  protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {

    // Check whether the whole field storage definition is gone, or just some
    // bundle fields.
    $storage_definition = $field_definition
      ->getFieldStorageDefinition();
    $is_deleted = $this
      ->storageDefinitionIsDeleted($storage_definition);
    $table_mapping = $this
      ->getTableMapping();
    $table_name = $table_mapping
      ->getDedicatedDataTableName($storage_definition, $is_deleted);

    // Get the entities which we want to purge first.
    $entity_query = $this->database
      ->select($table_name, 't', array(
      'fetch' => \PDO::FETCH_ASSOC,
    ));
    $or = $entity_query
      ->orConditionGroup();
    foreach ($storage_definition
      ->getColumns() as $column_name => $data) {
      $or
        ->isNotNull($table_mapping
        ->getFieldColumnName($storage_definition, $column_name));
    }
    $entity_query
      ->distinct(TRUE)
      ->fields('t', array(
      'entity_id',
    ))
      ->condition('bundle', $field_definition
      ->getTargetBundle())
      ->range(0, $batch_size);

    // Create a map of field data table column names to field column names.
    $column_map = array();
    foreach ($storage_definition
      ->getColumns() as $column_name => $data) {
      $column_map[$table_mapping
        ->getFieldColumnName($storage_definition, $column_name)] = $column_name;
    }
    $entities = array();
    $items_by_entity = array();
    foreach ($entity_query
      ->execute() as $row) {
      $item_query = $this->database
        ->select($table_name, 't', array(
        'fetch' => \PDO::FETCH_ASSOC,
      ))
        ->fields('t')
        ->condition('entity_id', $row['entity_id'])
        ->orderBy('delta');
      foreach ($item_query
        ->execute() as $item_row) {
        if (!isset($entities[$item_row['revision_id']])) {

          // Create entity with the right revision id and entity id combination.
          $item_row['entity_type'] = $this->entityTypeId;

          // @todo: Replace this by an entity object created via an entity
          // factory, see https://www.drupal.org/node/1867228.
          $entities[$item_row['revision_id']] = _field_create_entity_from_ids((object) $item_row);
        }
        $item = array();
        foreach ($column_map as $db_column => $field_column) {
          $item[$field_column] = $item_row[$db_column];
        }
        $items_by_entity[$item_row['revision_id']][] = $item;
      }
    }

    // Create field item objects and return.
    foreach ($items_by_entity as $revision_id => $values) {
      $entity_adapter = $entities[$revision_id]
        ->getTypedData();
      $items_by_entity[$revision_id] = \Drupal::typedDataManager()
        ->create($field_definition, $values, $field_definition
        ->getName(), $entity_adapter);
    }
    return $items_by_entity;
  }

  /**
   * {@inheritdoc}
   */
  protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
    $storage_definition = $field_definition
      ->getFieldStorageDefinition();
    $is_deleted = $this
      ->storageDefinitionIsDeleted($storage_definition);
    $table_mapping = $this
      ->getTableMapping();
    $table_name = $table_mapping
      ->getDedicatedDataTableName($storage_definition, $is_deleted);
    $revision_name = $table_mapping
      ->getDedicatedRevisionTableName($storage_definition, $is_deleted);
    $revision_id = $this->entityType
      ->isRevisionable() ? $entity
      ->getRevisionId() : $entity
      ->id();
    $this->database
      ->delete($table_name)
      ->condition('revision_id', $revision_id)
      ->execute();
    if ($this->entityType
      ->isRevisionable()) {
      $this->database
        ->delete($revision_name)
        ->condition('revision_id', $revision_id)
        ->execute();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
    $this
      ->getStorageSchema()
      ->finalizePurge($storage_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function countFieldData($storage_definition, $as_bool = FALSE) {
    $table_mapping = $this
      ->getTableMapping();
    if ($table_mapping
      ->requiresDedicatedTableStorage($storage_definition)) {
      $is_deleted = $this
        ->storageDefinitionIsDeleted($storage_definition);
      if ($this->entityType
        ->isRevisionable()) {
        $table_name = $table_mapping
          ->getDedicatedRevisionTableName($storage_definition, $is_deleted);
      }
      else {
        $table_name = $table_mapping
          ->getDedicatedDataTableName($storage_definition, $is_deleted);
      }
      $query = $this->database
        ->select($table_name, 't');
      $or = $query
        ->orConditionGroup();
      foreach ($storage_definition
        ->getColumns() as $column_name => $data) {
        $or
          ->isNotNull($table_mapping
          ->getFieldColumnName($storage_definition, $column_name));
      }
      $query
        ->condition($or);
      if (!$as_bool) {
        $query
          ->fields('t', array(
          'entity_id',
        ))
          ->distinct(TRUE);
      }
    }
    elseif ($table_mapping
      ->allowsSharedTableStorage($storage_definition)) {

      // Ascertain the table this field is mapped too.
      $field_name = $storage_definition
        ->getName();
      try {
        $table_name = $table_mapping
          ->getFieldTableName($field_name);
      } catch (SqlContentEntityStorageException $e) {

        // This may happen when changing field storage schema, since we are not
        // able to use a table mapping matching the passed storage definition.
        // @todo Revisit this once we are able to instantiate the table mapping
        //   properly. See https://www.drupal.org/node/2274017.
        $table_name = $this->dataTable ?: $this->baseTable;
      }
      $query = $this->database
        ->select($table_name, 't');
      $or = $query
        ->orConditionGroup();
      foreach (array_keys($storage_definition
        ->getColumns()) as $property_name) {
        $or
          ->isNotNull($table_mapping
          ->getFieldColumnName($storage_definition, $property_name));
      }
      $query
        ->condition($or);
      if (!$as_bool) {
        $query
          ->fields('t', array(
          $this->idKey,
        ))
          ->distinct(TRUE);
      }
    }

    // @todo Find a way to count field data also for fields having custom
    //   storage. See https://www.drupal.org/node/2337753.
    $count = 0;
    if (isset($query)) {

      // If we are performing the query just to check if the field has data
      // limit the number of rows.
      if ($as_bool) {
        $query
          ->range(0, 1)
          ->addExpression('1');
      }
      else {

        // Otherwise count the number of rows.
        $query = $query
          ->countQuery();
      }
      $count = $query
        ->execute()
        ->fetchField();
    }
    return $as_bool ? (bool) $count : (int) $count;
  }

  /**
   * Determines whether the passed field has been already deleted.
   *
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
   *   The field storage definition.
   *
   * @return bool
   *   Whether the field has been already deleted.
   */
  protected function storageDefinitionIsDeleted(FieldStorageDefinitionInterface $storage_definition) {
    return !array_key_exists($storage_definition
      ->getName(), $this->entityManager
      ->getLastInstalledFieldStorageDefinitions($this->entityTypeId));
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ContentEntityStorageBase::$bundleKey protected property The entity bundle key.
ContentEntityStorageBase::$cacheBackend protected property Cache backend.
ContentEntityStorageBase::$entityManager protected property The entity manager.
ContentEntityStorageBase::buildCacheId protected function Builds the cache ID for the passed in entity ID.
ContentEntityStorageBase::cleanIds protected function Ensures integer entity IDs are valid.
ContentEntityStorageBase::createTranslation public function Constructs a new entity translation object, without permanently saving it. Overrides ContentEntityStorageInterface::createTranslation
ContentEntityStorageBase::deleteRevision public function Delete a specific entity revision. Overrides EntityStorageInterface::deleteRevision 1
ContentEntityStorageBase::doCreate protected function Performs storage-specific creation of entities. Overrides EntityStorageBase::doCreate
ContentEntityStorageBase::doDelete protected function Performs storage-specific entity deletion. Overrides EntityStorageBase::doDelete 1
ContentEntityStorageBase::doPostSave protected function Performs post save entity processing. Overrides EntityStorageBase::doPostSave
ContentEntityStorageBase::doPreSave protected function Performs presave entity processing. Overrides EntityStorageBase::doPreSave
ContentEntityStorageBase::doSave protected function Performs storage-specific saving of the entity. Overrides EntityStorageBase::doSave 1
ContentEntityStorageBase::getFromPersistentCache protected function Gets entities from the persistent cache backend.
ContentEntityStorageBase::hasData public function Determines if the storage contains any data. Overrides DynamicallyFieldableEntityStorageInterface::hasData 1
ContentEntityStorageBase::hasFieldValueChanged protected function Checks whether the field values changed compared to the original entity.
ContentEntityStorageBase::initFieldValues protected function Initializes field values.
ContentEntityStorageBase::invokeFieldMethod protected function Invokes a method on the Field objects within an entity.
ContentEntityStorageBase::invokeFieldPostSave protected function Invokes the post save method on the Field objects within an entity.
ContentEntityStorageBase::invokeHook protected function Invokes a hook on behalf of the entity. Overrides EntityStorageBase::invokeHook
ContentEntityStorageBase::invokeStorageLoadHook protected function Invokes hook_entity_storage_load().
ContentEntityStorageBase::invokeTranslationHooks protected function Checks translation statuses and invoke the related hooks if needed.
ContentEntityStorageBase::loadRevision public function Load a specific entity revision. Overrides EntityStorageInterface::loadRevision 1
ContentEntityStorageBase::onFieldDefinitionCreate public function Reacts to the creation of a field. Overrides FieldDefinitionListenerInterface::onFieldDefinitionCreate
ContentEntityStorageBase::onFieldDefinitionUpdate public function Reacts to the update of a field. Overrides FieldDefinitionListenerInterface::onFieldDefinitionUpdate
ContentEntityStorageBase::populateAffectedRevisionTranslations protected function Populates the affected flag for all the revision translations.
ContentEntityStorageBase::purgeFieldData public function Purges a batch of field data. Overrides DynamicallyFieldableEntityStorageInterface::purgeFieldData
ContentEntityStorageBase::resetCache public function Resets the internal, static entity cache. Overrides EntityStorageBase::resetCache 1
ContentEntityStorageBase::setPersistentCache protected function Stores entities in the persistent cache backend.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
EntityHandlerBase::$moduleHandler protected property The module handler to invoke hooks on. 3
EntityHandlerBase::moduleHandler protected function Gets the module handler. 3
EntityHandlerBase::setModuleHandler public function Sets the module handler for this handler.
EntityStorageBase::$entities protected property Static cache of entities, keyed by entity ID. 1
EntityStorageBase::$entityClass protected property Name of the entity class.
EntityStorageBase::$entityType protected property Information about the entity type.
EntityStorageBase::$entityTypeId protected property Entity type ID for this storage.
EntityStorageBase::$idKey protected property Name of the entity's ID field in the entity database table.
EntityStorageBase::$uuidKey protected property Name of entity's UUID database table field, if it supports UUIDs. 1
EntityStorageBase::$uuidService protected property The UUID service. 1
EntityStorageBase::create public function Constructs a new entity object, without permanently saving it. Overrides EntityStorageInterface::create 1
EntityStorageBase::getAggregateQuery public function Gets an aggregated query instance. Overrides EntityStorageInterface::getAggregateQuery
EntityStorageBase::getEntityType public function Gets the entity type definition. Overrides EntityStorageInterface::getEntityType
EntityStorageBase::getEntityTypeId public function Gets the entity type ID. Overrides EntityStorageInterface::getEntityTypeId
EntityStorageBase::getFromStaticCache protected function Gets entities from the static cache. 1
EntityStorageBase::getQuery public function Gets an entity query instance. Overrides EntityStorageInterface::getQuery
EntityStorageBase::load public function Loads one entity. Overrides EntityStorageInterface::load 2
EntityStorageBase::loadByProperties public function Load entities by their property values. Overrides EntityStorageInterface::loadByProperties 3
EntityStorageBase::loadMultiple public function Loads one or more entities. Overrides EntityStorageInterface::loadMultiple 2
EntityStorageBase::loadUnchanged public function Loads an unchanged entity from the database. Overrides EntityStorageInterface::loadUnchanged
EntityStorageBase::postLoad protected function Attaches data to entities upon loading.
EntityStorageBase::setStaticCache protected function Stores entities in the static entity cache. 1
EntityStorageInterface::FIELD_LOAD_CURRENT constant Load the most recent version of an entity's field data.
EntityStorageInterface::FIELD_LOAD_REVISION constant Load the version of an entity's field data specified in the entity.
SqlContentEntityStorage::$baseTable protected property The base table of the entity.
SqlContentEntityStorage::$database protected property Active database connection.
SqlContentEntityStorage::$dataTable protected property The table that stores properties, if the entity has multilingual support.
SqlContentEntityStorage::$defaultLangcodeKey protected property The default language entity key.
SqlContentEntityStorage::$langcodeKey protected property The entity langcode key. Overrides EntityStorageBase::$langcodeKey
SqlContentEntityStorage::$languageManager protected property The language manager.
SqlContentEntityStorage::$revisionDataTable protected property The table that stores revision field data if the entity supports revisions.
SqlContentEntityStorage::$revisionKey protected property Name of entity's revision database table field, if it supports revisions.
SqlContentEntityStorage::$revisionTable protected property The table that stores revisions, if the entity supports revisions.
SqlContentEntityStorage::$storageSchema protected property The entity type's storage schema object.
SqlContentEntityStorage::$tableMapping protected property The mapping of field columns to SQL tables.
SqlContentEntityStorage::buildPropertyQuery protected function Builds an entity query. Overrides EntityStorageBase::buildPropertyQuery
SqlContentEntityStorage::buildQuery protected function Builds the query to load the entity.
SqlContentEntityStorage::countFieldData public function Determines the number of entities with values for a given field. Overrides FieldableEntityStorageInterface::countFieldData
SqlContentEntityStorage::createInstance public static function Instantiates a new instance of this entity handler. Overrides ContentEntityStorageBase::createInstance 2
SqlContentEntityStorage::delete public function Deletes permanently saved entities. Overrides EntityStorageBase::delete
SqlContentEntityStorage::deleteFromDedicatedTables protected function Deletes values of fields in dedicated tables for all revisions.
SqlContentEntityStorage::deleteRevisionFromDedicatedTables protected function Deletes values of fields in dedicated tables for all revisions.
SqlContentEntityStorage::doDeleteFieldItems protected function Deletes entity field values from the storage. Overrides ContentEntityStorageBase::doDeleteFieldItems
SqlContentEntityStorage::doDeleteRevisionFieldItems protected function Deletes field values of an entity revision from the storage. Overrides ContentEntityStorageBase::doDeleteRevisionFieldItems
SqlContentEntityStorage::doLoadMultiple protected function Performs storage-specific loading of entities. Overrides EntityStorageBase::doLoadMultiple
SqlContentEntityStorage::doLoadRevisionFieldItems protected function Actually loads revision field item values from the storage. Overrides ContentEntityStorageBase::doLoadRevisionFieldItems
SqlContentEntityStorage::doSaveFieldItems protected function Writes entity field values to the storage. Overrides ContentEntityStorageBase::doSaveFieldItems 1
SqlContentEntityStorage::finalizePurge public function Performs final cleanup after all data of a field has been purged. Overrides ContentEntityStorageBase::finalizePurge
SqlContentEntityStorage::getBaseTable public function Gets the base table name.
SqlContentEntityStorage::getDataTable public function Gets the data table name.
SqlContentEntityStorage::getFieldStorageDefinitions public function Gets the base field definitions for a content entity type.
SqlContentEntityStorage::getFromStorage protected function Gets entities from the storage.
SqlContentEntityStorage::getQueryServiceName protected function Gets the name of the service for the query for this entity storage. Overrides EntityStorageBase::getQueryServiceName
SqlContentEntityStorage::getRevisionDataTable public function Gets the revision data table name.
SqlContentEntityStorage::getRevisionTable public function Gets the revision table name.
SqlContentEntityStorage::getStorageSchema protected function Gets the entity type's storage schema object.
SqlContentEntityStorage::getTableMapping public function Gets a table mapping for the entity's SQL tables. Overrides SqlEntityStorageInterface::getTableMapping
SqlContentEntityStorage::has protected function Determines if this entity already exists in storage. Overrides EntityStorageBase::has
SqlContentEntityStorage::initTableLayout protected function Initializes table name variables.
SqlContentEntityStorage::isColumnSerial protected function Checks whether a field column should be treated as serial. 1
SqlContentEntityStorage::loadFromDedicatedTables protected function Loads values of fields stored in dedicated tables for a group of entities.
SqlContentEntityStorage::loadFromSharedTables protected function Loads values for fields stored in the shared data tables.
SqlContentEntityStorage::mapFromStorageRecords protected function Maps from storage records to entity objects, and attaches fields. Overrides EntityStorageBase::mapFromStorageRecords
SqlContentEntityStorage::mapToDataStorageRecord protected function Maps from an entity object to the storage record of the field data.
SqlContentEntityStorage::mapToStorageRecord protected function Maps from an entity object to the storage record.
SqlContentEntityStorage::onBundleCreate public function Reacts to a bundle being created. Overrides EntityBundleListenerInterface::onBundleCreate
SqlContentEntityStorage::onBundleDelete public function Reacts to a bundle being deleted. Overrides EntityBundleListenerInterface::onBundleDelete
SqlContentEntityStorage::onEntityTypeCreate public function Reacts to the creation of the entity type. Overrides EntityTypeListenerInterface::onEntityTypeCreate
SqlContentEntityStorage::onEntityTypeDelete public function Reacts to the deletion of the entity type. Overrides EntityTypeListenerInterface::onEntityTypeDelete
SqlContentEntityStorage::onEntityTypeUpdate public function Reacts to the update of the entity type. Overrides EntityTypeListenerInterface::onEntityTypeUpdate
SqlContentEntityStorage::onFieldDefinitionDelete public function Reacts to the deletion of a field. Overrides ContentEntityStorageBase::onFieldDefinitionDelete
SqlContentEntityStorage::onFieldStorageDefinitionCreate public function Reacts to the creation of a field storage definition. Overrides ContentEntityStorageBase::onFieldStorageDefinitionCreate
SqlContentEntityStorage::onFieldStorageDefinitionDelete public function Reacts to the deletion of a field storage definition. Overrides ContentEntityStorageBase::onFieldStorageDefinitionDelete
SqlContentEntityStorage::onFieldStorageDefinitionUpdate public function Reacts to the update of a field storage definition. Overrides ContentEntityStorageBase::onFieldStorageDefinitionUpdate
SqlContentEntityStorage::purgeFieldItems protected function Removes field items from storage per entity during purge. Overrides ContentEntityStorageBase::purgeFieldItems
SqlContentEntityStorage::readFieldItemsToPurge protected function Reads values to be purged for a single field. Overrides ContentEntityStorageBase::readFieldItemsToPurge
SqlContentEntityStorage::requiresEntityDataMigration public function Checks if existing data would be lost if the schema changes were applied. Overrides EntityStorageSchemaInterface::requiresEntityDataMigration
SqlContentEntityStorage::requiresEntityStorageSchemaChanges public function Checks if the changes to the entity type requires storage schema changes. Overrides EntityStorageSchemaInterface::requiresEntityStorageSchemaChanges
SqlContentEntityStorage::requiresFieldDataMigration public function Checks if existing data would be lost if the schema changes were applied. Overrides DynamicallyFieldableEntityStorageSchemaInterface::requiresFieldDataMigration
SqlContentEntityStorage::requiresFieldStorageSchemaChanges public function Checks if the changes to the storage definition requires schema changes. Overrides DynamicallyFieldableEntityStorageSchemaInterface::requiresFieldStorageSchemaChanges
SqlContentEntityStorage::save public function Saves the entity permanently. Overrides EntityStorageBase::save
SqlContentEntityStorage::saveRevision protected function Saves an entity revision.
SqlContentEntityStorage::saveToDedicatedTables protected function Saves values of fields that use dedicated tables.
SqlContentEntityStorage::saveToSharedTables protected function Saves fields that use the shared tables.
SqlContentEntityStorage::setEntityType public function Updates the wrapped entity type definition.
SqlContentEntityStorage::storageDefinitionIsDeleted protected function Determines whether the passed field has been already deleted.
SqlContentEntityStorage::wrapSchemaException protected function Wraps a database schema exception into an entity storage exception.
SqlContentEntityStorage::__construct public function Constructs a SqlContentEntityStorage object. Overrides ContentEntityStorageBase::__construct 2
StringTranslationTrait::$stringTranslation protected property The string translation service.
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.