Newer
Older
catch
committed
<?php
/**
* @file
catch
committed
* Contains \Drupal\Core\Entity\Sql\SqlContentEntityStorage.
catch
committed
*/
catch
committed
namespace Drupal\Core\Entity\Sql;
catch
committed
use Drupal\Component\Utility\SafeMarkup;
catch
committed
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
catch
committed
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
catch
committed
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
Angie Byron
committed
use Drupal\Core\Entity\EntityBundleListenerInterface;
catch
committed
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
catch
committed
use Drupal\Core\Entity\Query\QueryInterface;
Angie Byron
committed
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
Alex Pott
committed
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\Core\Language\LanguageManagerInterface;
catch
committed
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
Alex Pott
committed
* A content entity database storage implementation.
catch
committed
*
Alex Pott
committed
* This class can be used as-is by most content entity types. Entity types
catch
committed
* requiring special handling can extend the class.
*
catch
committed
* 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.
*
* @ingroup entity_api
catch
committed
*/
Angie Byron
committed
class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, DynamicallyFieldableEntityStorageSchemaInterface, EntityBundleListenerInterface {
/**
* The mapping of field columns to SQL tables.
*
* @var \Drupal\Core\Entity\Sql\TableMappingInterface
*/
protected $tableMapping;
catch
committed
/**
* 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;
Alex Pott
committed
/**
* The default language entity key.
*
* @var string
*/
protected $defaultLangcodeKey = FALSE;
/**
* The base table of the entity.
*
* @var string
*/
protected $baseTable;
catch
committed
/**
* 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;
/**
Alex Pott
committed
* The entity manager.
catch
committed
*
Alex Pott
committed
* @var \Drupal\Core\Entity\EntityManagerInterface
catch
committed
*/
Alex Pott
committed
protected $entityManager;
catch
committed
/**
* The entity type's storage schema object.
*
Alex Pott
committed
* @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
*/
protected $storageSchema;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
catch
committed
/**
* {@inheritdoc}
*/
Alex Pott
committed
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
catch
committed
return new static(
Alex Pott
committed
$entity_type,
catch
committed
$container->get('database'),
$container->get('entity.manager'),
$container->get('cache.entity'),
$container->get('language_manager')
catch
committed
);
}
Angie Byron
committed
/**
* 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);
}
catch
committed
/**
catch
committed
* Constructs a SqlContentEntityStorage object.
catch
committed
*
Alex Pott
committed
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
catch
committed
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
Alex Pott
committed
* @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.
catch
committed
*/
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
Alex Pott
committed
parent::__construct($entity_type);
catch
committed
$this->database = $database;
Alex Pott
committed
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
$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';
}
Alex Pott
committed
$translatable = $this->entityType->isTranslatable();
if ($translatable) {
$this->dataTable = $this->entityType->getDataTable() ?: $this->entityTypeId . '_field_data';
$this->langcodeKey = $this->entityType->getKey('langcode');
Alex Pott
committed
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
if ($revisionable && $translatable) {
$this->revisionDataTable = $this->entityType->getRevisionDataTable() ?: $this->entityTypeId . '_field_revision';
}
}
/**
Alex Pott
committed
* Gets the base table name.
*
* @return string
* The table name.
*/
public function getBaseTable() {
return $this->baseTable;
}
/**
Alex Pott
committed
* Gets the revision table name.
*
* @return string|false
* The table name or FALSE if it is not available.
*/
public function getRevisionTable() {
return $this->revisionTable;
}
/**
Alex Pott
committed
* Gets the data table name.
*
* @return string|false
* The table name or FALSE if it is not available.
*/
public function getDataTable() {
return $this->dataTable;
}
/**
Alex Pott
committed
* 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;
}
catch
committed
/**
Alex Pott
committed
* Gets the entity type's storage schema object.
*
catch
committed
* @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
*/
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);
catch
committed
}
return $this->storageSchema;
}
catch
committed
/**
* Updates the wrapped entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The update entity type.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* See 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(SafeMarkup::format('Unsupported entity type @id', array('@id' => $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();
Alex Pott
committed
$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.
->setFieldNames($this->baseTable, $key_fields)
Alex Pott
committed
->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.
Alex Pott
committed
$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));
Alex Pott
committed
$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));
Alex Pott
committed
$table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
catch
committed
}
// 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;
}
catch
committed
}
return $table_mapping;
catch
committed
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
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.
$entities_from_storage = $this->getFromStorage($ids);
$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);
// Call hook_entity_storage_load().
foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
$function = $module . '_entity_storage_load';
$function($entities, $this->entityTypeId);
}
// Call hook_TYPE_storage_load().
foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
$function = $module . '_' . $this->entityTypeId . '_storage_load';
$function($entities);
}
}
}
return $entities;
}
/**
* Ensures integer entity IDs are valid.
*
* The identifier sanitization provided by this method has been introduced
* as Drupal used to rely on the database to facilitate this, which worked
* correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
*
* @param array $ids
* The entity IDs to verify.
* @return array
* The sanitized list of entity IDs.
*/
protected function cleanIds(array $ids) {
$definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
$id_definition = $definitions[$this->entityType->getKey('id')];
if ($id_definition->getType() == 'integer') {
$ids = array_filter($ids, function ($id) {
return is_numeric($id) && $id == (int) $id;
});
$ids = array_map('intval', $ids);
}
return $ids;
}
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
/**
* Gets entities from the persistent cache backend.
*
* @param array|null &$ids
* If not empty, return entities that match these IDs. IDs that were found
* will be removed from the list.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* Array of entities from the persistent cache.
*/
protected function getFromPersistentCache(array &$ids = NULL) {
if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
return array();
}
$entities = array();
// Build the list of cache entries to retrieve.
$cid_map = array();
foreach ($ids as $id) {
$cid_map[$id] = $this->buildCacheId($id);
}
$cids = array_values($cid_map);
if ($cache = $this->cacheBackend->getMultiple($cids)) {
// Get the entities that were found in the cache.
foreach ($ids as $index => $id) {
$cid = $cid_map[$id];
if (isset($cache[$cid])) {
$entities[$id] = $cache[$cid]->data;
unset($ids[$index]);
}
}
}
return $entities;
}
/**
* Stores entities in the persistent cache backend.
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* Entities to store in the cache.
*/
protected function setPersistentCache($entities) {
if (!$this->entityType->isPersistentlyCacheable()) {
return;
}
$cache_tags = array(
$this->entityTypeId . '_values',
'entity_field_info',
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
);
foreach ($entities as $id => $entity) {
$this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
}
}
/**
* Invokes hook_entity_load_uncached().
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* List of entities, keyed on the entity ID.
*/
protected function invokeLoadUncachedHook(array &$entities) {
if (!empty($entities)) {
// Call hook_entity_load_uncached().
foreach ($this->moduleHandler()->getImplementations('entity_load_uncached') as $module) {
$function = $module . '_entity_load_uncached';
$function($entities, $this->entityTypeId);
}
// Call hook_TYPE_load_uncached().
foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load_uncached') as $module) {
$function = $module . '_' . $this->entityTypeId . '_load_uncached';
$function($entities);
}
}
}
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
if ($ids) {
$cids = array();
foreach ($ids as $id) {
unset($this->entities[$id]);
$cids[] = $this->buildCacheId($id);
}
if ($this->entityType->isPersistentlyCacheable()) {
$this->cacheBackend->deleteMultiple($cids);
}
}
else {
$this->entities = array();
if ($this->entityType->isPersistentlyCacheable()) {
catch
committed
Cache::invalidateTags(array($this->entityTypeId . '_values'));
}
}
}
catch
committed
Alex Pott
committed
* Builds the cache ID for the passed in entity ID.
*
* @param int $id
* Entity ID for which the cache ID should be built.
*
* @return string
* Cache ID that can be passed to the cache backend.
*/
protected function buildCacheId($id) {
return "values:{$this->entityTypeId}:$id";
catch
committed
}
/**
* Maps from storage records to entity objects, and attaches fields.
Alex Pott
committed
*
catch
committed
* @param array $records
* Associative array of query results, keyed on the entity ID.
Alex Pott
committed
* @param bool $load_from_revision
* Flag to indicate whether revisions should be loaded or not.
catch
committed
*
* @return array
* An array of entity objects implementing the EntityInterface.
*/
Alex Pott
committed
protected function mapFromStorageRecords(array $records, $load_from_revision = FALSE) {
Alex Pott
committed
if (!$records) {
return array();
}
Alex Pott
committed
$values = array();
catch
committed
foreach ($records as $id => $record) {
Alex Pott
committed
$values[$id] = array();
catch
committed
// 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.
catch
committed
foreach ($record as $name => $value) {
catch
committed
// 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);
Alex Pott
committed
$values[$id][$field_name][LanguageInterface::LANGCODE_DEFAULT][$property_name] = $value;
catch
committed
}
else {
// Handle columns named directly after the field (e.g if the field
// type only stores one property).
Alex Pott
committed
$values[$id][$name][LanguageInterface::LANGCODE_DEFAULT] = $value;
catch
committed
}
catch
committed
}
}
Alex Pott
committed
Alex Pott
committed
// 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]));
}
Alex Pott
committed
catch
committed
return $entities;
}
/**
Alex Pott
committed
* Loads values for fields stored in the shared data tables.
catch
committed
*
Alex Pott
committed
* @param array &$values
* Associative array of entities values, keyed on the entity ID.
* @param array &$translations
* List of translations, keyed on the entity ID.
catch
committed
*/
Alex Pott
committed
protected function loadFromSharedTables(array &$values, array &$translations) {
catch
committed
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;
Alex Pott
committed
$alias = $this->revisionDataTable ? 'revision' : 'data';
$query = $this->database->select($table, $alias, array('fetch' => \PDO::FETCH_ASSOC))
->fields($alias)
Alex Pott
committed
->condition($alias . '.' . $this->idKey, array_keys($values), 'IN')
Alex Pott
committed
->orderBy($alias . '.' . $this->idKey);
catch
committed
Alex Pott
committed
$table_mapping = $this->getTableMapping();
catch
committed
if ($this->revisionDataTable) {
Alex Pott
committed
// 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);
Alex Pott
committed
// Find fields that are not revisioned or entity keys. Data fields have
// the same value regardless of entity revision.
Alex Pott
committed
$data_fields = array_diff($table_mapping->getFieldNames($this->dataTable), $fields, $base_fields);
Alex Pott
committed
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);
}
Alex Pott
committed
// Get the revision IDs.
$revision_ids = array();
Alex Pott
committed
foreach ($values as $entity_values) {
$revision_ids[] = $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT];
catch
committed
}
Alex Pott
committed
$query->condition('revision.' . $this->revisionKey, $revision_ids, 'IN');
catch
committed
}
else {
Alex Pott
committed
$fields = $table_mapping->getFieldNames($this->dataTable);
catch
committed
}
Alex Pott
committed
$result = $query->execute();
foreach ($result as $row) {
$id = $row[$this->idKey];
catch
committed
// Field values in default language are stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
Alex Pott
committed
$langcode = empty($row[$this->defaultLangcodeKey]) ? $row[$this->langcodeKey] : LanguageInterface::LANGCODE_DEFAULT;
catch
committed
$translations[$id][$langcode] = TRUE;
Alex Pott
committed
foreach ($fields as $field_name) {
$columns = $table_mapping->getColumnNames($field_name);
// Do not key single-column fields by property name.
if (count($columns) == 1) {
Alex Pott
committed
$values[$id][$field_name][$langcode] = $row[reset($columns)];
catch
committed
}
else {
foreach ($columns as $property_name => $column_name) {
Alex Pott
committed
$values[$id][$field_name][$langcode][$property_name] = $row[$column_name];
catch
committed
}
catch
committed
}
}
}
}
}
/**
* {@inheritdoc}
catch
committed
*/
public function loadRevision($revision_id) {
// Build and execute the query.
$query_result = $this->buildQuery(array(), $revision_id)->execute();
Alex Pott
committed
$records = $query_result->fetchAllAssoc($this->idKey);
catch
committed
Alex Pott
committed
if (!empty($records)) {
// Convert the raw records to entity objects.
Alex Pott
committed
$entities = $this->mapFromStorageRecords($records, TRUE);
Alex Pott
committed
$this->postLoad($entities);
$entity = reset($entities);
if ($entity) {
return $entity;
}
catch
committed
}
}
/**
catch
committed
* Implements \Drupal\Core\Entity\EntityStorageInterface::deleteRevision().
catch
committed
*/
public function deleteRevision($revision_id) {
if ($revision = $this->loadRevision($revision_id)) {
// Prevent deletion if this is the default revision.
if ($revision->isDefaultRevision()) {
throw new EntityStorageException('Default revision can not be deleted');
}
$this->database->delete($this->revisionTable)
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
$this->invokeFieldMethod('deleteRevision', $revision);
Alex Pott
committed
$this->deleteRevisionFromDedicatedTables($revision);
catch
committed
$this->invokeHook('revision_delete', $revision);
}
}
/**
Angie Byron
committed
* {@inheritdoc}
catch
committed
*/
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.
catch
committed
// Default to the original entity language if not explicitly specified
// otherwise.
Alex Pott
committed
if (!array_key_exists($this->defaultLangcodeKey, $values)) {
$values[$this->defaultLangcodeKey] = 1;
catch
committed
}
// If the 'default_langcode' flag is explicitly not set, we do not care
// whether the queried values are in the original entity language or not.
Alex Pott
committed
elseif ($values[$this->defaultLangcodeKey] === NULL) {
unset($values[$this->defaultLangcodeKey]);
catch
committed
}
}
Angie Byron
committed
parent::buildPropertyQuery($entity_query, $values);
catch
committed
}
/**
* 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.
catch
committed
* See Drupal\comment\CommentStorage::buildQuery() for an example.
catch
committed
*
* @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).
*
Alex Pott
committed
* @return \Drupal\Core\Database\Query\Select
catch
committed
* A SelectQuery object for loading the entity.
*/
protected function buildQuery($ids, $revision_id = FALSE) {
$query = $this->database->select($this->entityType->getBaseTable(), 'base');
catch
committed
$query->addTag($this->entityTypeId . '_load_multiple');
catch
committed
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);
catch
committed
if ($this->revisionTable) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = $table_mapping->getAllColumns($this->revisionTable);
catch
committed
$entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
catch
committed
// 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.
Alex Pott
committed
$query->addExpression('CASE base.' . $this->revisionKey . ' WHEN revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', 'isDefaultRevision');
catch
committed
}
$query->fields('base', $entity_fields);
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
return $query;
}
/**
catch
committed
* Implements \Drupal\Core\Entity\EntityStorageInterface::delete().
catch
committed
*/
public function delete(array $entities) {
if (!$entities) {
// If no IDs or invalid IDs were passed, do nothing.
return;
}
$transaction = $this->database->startTransaction();
try {
Alex Pott
committed
parent::delete($entities);
catch
committed
// Ignore replica server temporarily.
db_ignore_replica();
catch
committed
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityTypeId, $e);
catch
committed
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
Alex Pott
committed
protected function doDelete($entities) {
$ids = array_keys($entities);
catch
committed
Alex Pott
committed
$this->database->delete($this->entityType->getBaseTable())
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
catch
committed
Alex Pott
committed
if ($this->revisionTable) {
$this->database->delete($this->revisionTable)
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
}
catch
committed
Alex Pott
committed
if ($this->dataTable) {
$this->database->delete($this->dataTable)
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
}
catch
committed
Alex Pott
committed
if ($this->revisionDataTable) {
$this->database->delete($this->revisionDataTable)
->condition($this->idKey, $ids, 'IN')
Alex Pott
committed
->execute();
}
catch
committed
Alex Pott
committed
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
Alex Pott
committed
$this->deleteFromDedicatedTables($entity);
Alex Pott
committed
}
}
/**
* {@inheritdoc}
*/
public function save(EntityInterface $entity) {
$transaction = $this->database->startTransaction();
try {
// Sync the changes made in the fields array to the internal values array.
$entity->updateOriginalValues();
$return = parent::save($entity);
catch
committed
// Ignore replica server temporarily.
db_ignore_replica();
catch
committed
return $return;
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityTypeId, $e);
catch
committed
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
protected function doSave($id, EntityInterface $entity) {
// Create the storage record to be saved.
$record = $this->mapToStorageRecord($entity);
$is_new = $entity->isNew();
if (!$is_new) {
if ($entity->isDefaultRevision()) {
$this->database
->update($this->baseTable)
->fields((array) $record)
->condition($this->idKey, $record->{$this->idKey})
->execute();
$return = SAVED_UPDATED;
Alex Pott
committed
}
else {
// @todo, should a different value be returned when saving an entity
// with $isDefaultRevision = FALSE?
$return = FALSE;
}
if ($this->revisionTable) {
$entity->{$this->revisionKey}->value = $this->saveRevision($entity);
Alex Pott
committed
}
if ($this->dataTable) {
Alex Pott
committed
$this->saveToSharedTables($entity);
Alex Pott
committed
}
if ($this->revisionDataTable) {
Alex Pott
committed
$this->saveToSharedTables($entity, $this->revisionDataTable);
Alex Pott
committed
}
}
else {
// Ensure the entity is still seen as new after assigning it an id,
// while storing its data.
$entity->enforceIsNew();
$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
Dries Buytaert
committed
// 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;
}
$return = SAVED_NEW;
Alex Pott
committed
$entity->{$this->idKey}->value = (string) $record->{$this->idKey};
if ($this->revisionTable) {
$entity->setNewRevision();
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
if ($this->dataTable) {
Alex Pott
committed
$this->saveToSharedTables($entity);
Alex Pott
committed
}
if ($this->revisionDataTable) {
Alex Pott
committed
$this->saveToSharedTables($entity, $this->revisionDataTable);
Alex Pott
committed
}
}
$this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity);
Alex Pott
committed
$this->saveToDedicatedTables($entity, !$is_new);
Alex Pott
committed
if (!$is_new && $this->dataTable) {
$this->invokeTranslationHooks($entity);
}
$entity->enforceIsNew(FALSE);
if ($this->revisionTable) {