class SqlContentEntityStorage in Zircon Profile 8
Same name and namespace in other branches
- 8.0 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
- class \Drupal\Core\Entity\EntityHandlerBase uses DependencySerializationTrait, StringTranslationTrait
- class \Drupal\Core\Entity\EntityStorageBase implements EntityHandlerInterface, EntityStorageInterface
- class \Drupal\Core\Entity\ContentEntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface
- class \Drupal\Core\Entity\Sql\SqlContentEntityStorage implements EntityBundleListenerInterface, DynamicallyFieldableEntityStorageSchemaInterface, SqlEntityStorageInterface
- class \Drupal\Core\Entity\ContentEntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface
- class \Drupal\Core\Entity\EntityStorageBase implements EntityHandlerInterface, EntityStorageInterface
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.
File
- core/
lib/ Drupal/ Core/ Entity/ Sql/ SqlContentEntityStorage.php, line 44 - Contains \Drupal\Core\Entity\Sql\SqlContentEntityStorage.
Namespace
Drupal\Core\Entity\SqlView 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
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
ContentEntityStorageBase:: |
protected | property | The entity bundle key. | |
ContentEntityStorageBase:: |
protected | property | Cache backend. | |
ContentEntityStorageBase:: |
protected | property | The entity manager. | |
ContentEntityStorageBase:: |
protected | function | Builds the cache ID for the passed in entity ID. | |
ContentEntityStorageBase:: |
protected | function | Ensures integer entity IDs are valid. | |
ContentEntityStorageBase:: |
public | function |
Constructs a new entity translation object, without permanently saving it. Overrides ContentEntityStorageInterface:: |
|
ContentEntityStorageBase:: |
public | function |
Delete a specific entity revision. Overrides EntityStorageInterface:: |
1 |
ContentEntityStorageBase:: |
protected | function |
Performs storage-specific creation of entities. Overrides EntityStorageBase:: |
|
ContentEntityStorageBase:: |
protected | function |
Performs storage-specific entity deletion. Overrides EntityStorageBase:: |
1 |
ContentEntityStorageBase:: |
protected | function |
Performs post save entity processing. Overrides EntityStorageBase:: |
|
ContentEntityStorageBase:: |
protected | function |
Performs presave entity processing. Overrides EntityStorageBase:: |
|
ContentEntityStorageBase:: |
protected | function |
Performs storage-specific saving of the entity. Overrides EntityStorageBase:: |
1 |
ContentEntityStorageBase:: |
protected | function | Gets entities from the persistent cache backend. | |
ContentEntityStorageBase:: |
public | function |
Determines if the storage contains any data. Overrides DynamicallyFieldableEntityStorageInterface:: |
1 |
ContentEntityStorageBase:: |
protected | function | Checks whether the field values changed compared to the original entity. | |
ContentEntityStorageBase:: |
protected | function | Initializes field values. | |
ContentEntityStorageBase:: |
protected | function | Invokes a method on the Field objects within an entity. | |
ContentEntityStorageBase:: |
protected | function | Invokes the post save method on the Field objects within an entity. | |
ContentEntityStorageBase:: |
protected | function |
Invokes a hook on behalf of the entity. Overrides EntityStorageBase:: |
|
ContentEntityStorageBase:: |
protected | function | Invokes hook_entity_storage_load(). | |
ContentEntityStorageBase:: |
protected | function | Checks translation statuses and invoke the related hooks if needed. | |
ContentEntityStorageBase:: |
public | function |
Load a specific entity revision. Overrides EntityStorageInterface:: |
1 |
ContentEntityStorageBase:: |
public | function |
Reacts to the creation of a field. Overrides FieldDefinitionListenerInterface:: |
|
ContentEntityStorageBase:: |
public | function |
Reacts to the update of a field. Overrides FieldDefinitionListenerInterface:: |
|
ContentEntityStorageBase:: |
protected | function | Populates the affected flag for all the revision translations. | |
ContentEntityStorageBase:: |
public | function |
Purges a batch of field data. Overrides DynamicallyFieldableEntityStorageInterface:: |
|
ContentEntityStorageBase:: |
public | function |
Resets the internal, static entity cache. Overrides EntityStorageBase:: |
1 |
ContentEntityStorageBase:: |
protected | function | Stores entities in the persistent cache backend. | |
DependencySerializationTrait:: |
protected | property | An array of service IDs keyed by property name used for serialization. | |
DependencySerializationTrait:: |
public | function | 1 | |
DependencySerializationTrait:: |
public | function | 2 | |
EntityHandlerBase:: |
protected | property | The module handler to invoke hooks on. | 3 |
EntityHandlerBase:: |
protected | function | Gets the module handler. | 3 |
EntityHandlerBase:: |
public | function | Sets the module handler for this handler. | |
EntityStorageBase:: |
protected | property | Static cache of entities, keyed by entity ID. | 1 |
EntityStorageBase:: |
protected | property | Name of the entity class. | |
EntityStorageBase:: |
protected | property | Information about the entity type. | |
EntityStorageBase:: |
protected | property | Entity type ID for this storage. | |
EntityStorageBase:: |
protected | property | Name of the entity's ID field in the entity database table. | |
EntityStorageBase:: |
protected | property | Name of entity's UUID database table field, if it supports UUIDs. | 1 |
EntityStorageBase:: |
protected | property | The UUID service. | 1 |
EntityStorageBase:: |
public | function |
Constructs a new entity object, without permanently saving it. Overrides EntityStorageInterface:: |
1 |
EntityStorageBase:: |
public | function |
Gets an aggregated query instance. Overrides EntityStorageInterface:: |
|
EntityStorageBase:: |
public | function |
Gets the entity type definition. Overrides EntityStorageInterface:: |
|
EntityStorageBase:: |
public | function |
Gets the entity type ID. Overrides EntityStorageInterface:: |
|
EntityStorageBase:: |
protected | function | Gets entities from the static cache. | 1 |
EntityStorageBase:: |
public | function |
Gets an entity query instance. Overrides EntityStorageInterface:: |
|
EntityStorageBase:: |
public | function |
Loads one entity. Overrides EntityStorageInterface:: |
2 |
EntityStorageBase:: |
public | function |
Load entities by their property values. Overrides EntityStorageInterface:: |
3 |
EntityStorageBase:: |
public | function |
Loads one or more entities. Overrides EntityStorageInterface:: |
2 |
EntityStorageBase:: |
public | function |
Loads an unchanged entity from the database. Overrides EntityStorageInterface:: |
|
EntityStorageBase:: |
protected | function | Attaches data to entities upon loading. | |
EntityStorageBase:: |
protected | function | Stores entities in the static entity cache. | 1 |
EntityStorageInterface:: |
constant | Load the most recent version of an entity's field data. | ||
EntityStorageInterface:: |
constant | Load the version of an entity's field data specified in the entity. | ||
SqlContentEntityStorage:: |
protected | property | The base table of the entity. | |
SqlContentEntityStorage:: |
protected | property | Active database connection. | |
SqlContentEntityStorage:: |
protected | property | The table that stores properties, if the entity has multilingual support. | |
SqlContentEntityStorage:: |
protected | property | The default language entity key. | |
SqlContentEntityStorage:: |
protected | property |
The entity langcode key. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | property | The language manager. | |
SqlContentEntityStorage:: |
protected | property | The table that stores revision field data if the entity supports revisions. | |
SqlContentEntityStorage:: |
protected | property | Name of entity's revision database table field, if it supports revisions. | |
SqlContentEntityStorage:: |
protected | property | The table that stores revisions, if the entity supports revisions. | |
SqlContentEntityStorage:: |
protected | property | The entity type's storage schema object. | |
SqlContentEntityStorage:: |
protected | property | The mapping of field columns to SQL tables. | |
SqlContentEntityStorage:: |
protected | function |
Builds an entity query. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function | Builds the query to load the entity. | |
SqlContentEntityStorage:: |
public | function |
Determines the number of entities with values for a given field. Overrides FieldableEntityStorageInterface:: |
|
SqlContentEntityStorage:: |
public static | function |
Instantiates a new instance of this entity handler. Overrides ContentEntityStorageBase:: |
2 |
SqlContentEntityStorage:: |
public | function |
Deletes permanently saved entities. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function | Deletes values of fields in dedicated tables for all revisions. | |
SqlContentEntityStorage:: |
protected | function | Deletes values of fields in dedicated tables for all revisions. | |
SqlContentEntityStorage:: |
protected | function |
Deletes entity field values from the storage. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function |
Deletes field values of an entity revision from the storage. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function |
Performs storage-specific loading of entities. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function |
Actually loads revision field item values from the storage. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function |
Writes entity field values to the storage. Overrides ContentEntityStorageBase:: |
1 |
SqlContentEntityStorage:: |
public | function |
Performs final cleanup after all data of a field has been purged. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
public | function | Gets the base table name. | |
SqlContentEntityStorage:: |
public | function | Gets the data table name. | |
SqlContentEntityStorage:: |
public | function | Gets the base field definitions for a content entity type. | |
SqlContentEntityStorage:: |
protected | function | Gets entities from the storage. | |
SqlContentEntityStorage:: |
protected | function |
Gets the name of the service for the query for this entity storage. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
public | function | Gets the revision data table name. | |
SqlContentEntityStorage:: |
public | function | Gets the revision table name. | |
SqlContentEntityStorage:: |
protected | function | Gets the entity type's storage schema object. | |
SqlContentEntityStorage:: |
public | function |
Gets a table mapping for the entity's SQL tables. Overrides SqlEntityStorageInterface:: |
|
SqlContentEntityStorage:: |
protected | function |
Determines if this entity already exists in storage. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function | Initializes table name variables. | |
SqlContentEntityStorage:: |
protected | function | Checks whether a field column should be treated as serial. | 1 |
SqlContentEntityStorage:: |
protected | function | Loads values of fields stored in dedicated tables for a group of entities. | |
SqlContentEntityStorage:: |
protected | function | Loads values for fields stored in the shared data tables. | |
SqlContentEntityStorage:: |
protected | function |
Maps from storage records to entity objects, and attaches fields. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function | Maps from an entity object to the storage record of the field data. | |
SqlContentEntityStorage:: |
protected | function | Maps from an entity object to the storage record. | |
SqlContentEntityStorage:: |
public | function |
Reacts to a bundle being created. Overrides EntityBundleListenerInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to a bundle being deleted. Overrides EntityBundleListenerInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the creation of the entity type. Overrides EntityTypeListenerInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the deletion of the entity type. Overrides EntityTypeListenerInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the update of the entity type. Overrides EntityTypeListenerInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the deletion of a field. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the creation of a field storage definition. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the deletion of a field storage definition. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
public | function |
Reacts to the update of a field storage definition. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function |
Removes field items from storage per entity during purge. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function |
Reads values to be purged for a single field. Overrides ContentEntityStorageBase:: |
|
SqlContentEntityStorage:: |
public | function |
Checks if existing data would be lost if the schema changes were applied. Overrides EntityStorageSchemaInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Checks if the changes to the entity type requires storage schema changes. Overrides EntityStorageSchemaInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Checks if existing data would be lost if the schema changes were applied. Overrides DynamicallyFieldableEntityStorageSchemaInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Checks if the changes to the storage definition requires schema changes. Overrides DynamicallyFieldableEntityStorageSchemaInterface:: |
|
SqlContentEntityStorage:: |
public | function |
Saves the entity permanently. Overrides EntityStorageBase:: |
|
SqlContentEntityStorage:: |
protected | function | Saves an entity revision. | |
SqlContentEntityStorage:: |
protected | function | Saves values of fields that use dedicated tables. | |
SqlContentEntityStorage:: |
protected | function | Saves fields that use the shared tables. | |
SqlContentEntityStorage:: |
public | function | Updates the wrapped entity type definition. | |
SqlContentEntityStorage:: |
protected | function | Determines whether the passed field has been already deleted. | |
SqlContentEntityStorage:: |
protected | function | Wraps a database schema exception into an entity storage exception. | |
SqlContentEntityStorage:: |
public | function |
Constructs a SqlContentEntityStorage object. Overrides ContentEntityStorageBase:: |
2 |
StringTranslationTrait:: |
protected | property | The string translation service. | |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |