Newer
Older
catch
committed
<?php
/**
* @file
* Contains \Drupal\Core\Entity\DatabaseStorageController.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Language\Language;
use Drupal\field\FieldInfo;
Angie Byron
committed
use Drupal\field\FieldConfigUpdateForbiddenException;
use Drupal\field\FieldConfigInterface;
use Drupal\field\FieldInstanceConfigInterface;
use Drupal\field\Entity\FieldConfig;
catch
committed
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a base entity controller class.
*
* Default implementation of Drupal\Core\Entity\EntityStorageControllerInterface.
*
* This class can be used as-is by most simple entity types. Entity types
* requiring special handling can extend the class.
*/
class FieldableDatabaseStorageController extends FieldableEntityStorageControllerBase {
/**
* 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 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;
/**
* Whether this entity type should use the static cache.
*
* @var boolean
*/
protected $cache;
/**
* Active database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The field info object.
*
* @var \Drupal\field\FieldInfo
*/
protected $fieldInfo;
/**
* {@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('field.info')
);
}
/**
* Constructs a DatabaseStorageController object.
*
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.
* @param \Drupal\field\FieldInfo $field_info
* The field info service.
*/
Alex Pott
committed
public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info) {
parent::__construct($entity_type);
catch
committed
$this->database = $database;
$this->fieldInfo = $field_info;
// Check if the entity type supports IDs.
if ($this->entityType->hasKey('id')) {
$this->idKey = $this->entityType->getKey('id');
catch
committed
}
// Check if the entity type supports UUIDs.
$this->uuidKey = $this->entityType->getKey('uuid');
catch
committed
// Check if the entity type supports revisions.
if ($this->entityType->hasKey('revision')) {
$this->revisionKey = $this->entityType->getKey('revision');
$this->revisionTable = $this->entityType->getRevisionTable();
catch
committed
}
// Check if the entity type has a dedicated table for fields.
if ($data_table = $this->entityType->getDataTable()) {
Alex Pott
committed
$this->dataTable = $data_table;
catch
committed
// Entity types having both revision and translation support should always
// define a revision data table.
if ($this->revisionTable && $revision_data_table = $this->entityType->getRevisionDataTable()) {
Alex Pott
committed
$this->revisionDataTable = $revision_data_table;
catch
committed
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
}
}
}
/**
* {@inheritdoc}
*/
public function loadMultiple(array $ids = NULL) {
$entities = array();
// Create a new variable which is either a prepared version of the $ids
// array for later comparison with the entity cache, or FALSE if no $ids
// were passed. The $ids array is reduced as items are loaded from cache,
// and we need to know if it's empty for this reason to avoid querying the
// database when all requested entities are loaded from cache.
$passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
// Try to load entities from the static cache, if the entity type supports
// static caching.
if ($this->cache && $ids) {
$entities += $this->cacheGet($ids);
// If any entities were loaded, remove them from the ids still to load.
if ($passed_ids) {
$ids = array_keys(array_diff_key($passed_ids, $entities));
}
}
// Load any remaining entities from the database. This is the case if $ids
// is set to NULL (so we load all entities) or if there are any ids left to
// load.
if ($ids === NULL || $ids) {
// Build and execute the query.
$query_result = $this->buildQuery($ids)->execute();
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
}
Alex Pott
committed
// Pass all entities loaded from the database through $this->postLoad(),
catch
committed
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
Alex Pott
committed
$this->postLoad($queried_entities);
catch
committed
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
$entities += $queried_entities;
}
if ($this->cache) {
// Add entities to the cache.
if (!empty($queried_entities)) {
$this->cacheSet($queried_entities);
}
}
// Ensure that the returned array is ordered the same as the original
// $ids array if this was passed in and remove any invalid ids.
if ($passed_ids) {
// Remove any invalid ids from the array.
$passed_ids = array_intersect_key($passed_ids, $entities);
foreach ($entities as $entity) {
$passed_ids[$entity->id()] = $entity;
}
$entities = $passed_ids;
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function load($id) {
$entities = $this->loadMultiple(array($id));
return isset($entities[$id]) ? $entities[$id] : NULL;
}
/**
* Maps from storage records to entity objects.
*
* @param array $records
* Associative array of query results, keyed on the entity ID.
*
* @return array
* An array of entity objects implementing the EntityInterface.
*/
Alex Pott
committed
protected function mapFromStorageRecords(array $records) {
catch
committed
$entities = array();
foreach ($records as $id => $record) {
$entities[$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);
$entities[$id][$field_name][Language::LANGCODE_DEFAULT][$property_name] = $value;
}
else {
// Handle columns named directly after the field (e.g if the field
// type only stores one property).
$entities[$id][$name][Language::LANGCODE_DEFAULT] = $value;
}
catch
committed
}
// If we have no multilingual values we can instantiate entity objecs
// right now, otherwise we need to collect all the field values first.
if (!$this->dataTable) {
$bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE;
// Turn the record into an entity class.
$entities[$id] = new $this->entityClass($entities[$id], $this->entityTypeId, $bundle);
catch
committed
}
}
Alex Pott
committed
$this->attachPropertyData($entities);
catch
committed
return $entities;
}
/**
* Attaches property data in all languages for translatable properties.
*
* @param array &$entities
* Associative array of entities, keyed on the entity ID.
*/
Alex Pott
committed
protected function attachPropertyData(array &$entities) {
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;
$query = $this->database->select($table, 'data', array('fetch' => \PDO::FETCH_ASSOC))
->fields('data')
->condition($this->idKey, array_keys($entities))
->orderBy('data.' . $this->idKey);
if ($this->revisionDataTable) {
Alex Pott
committed
// Get the revision IDs.
$revision_ids = array();
foreach ($entities as $values) {
$revision_ids[] = is_object($values) ? $values->getRevisionId() : $values[$this->revisionKey][Language::LANGCODE_DEFAULT];
catch
committed
}
Alex Pott
committed
$query->condition($this->revisionKey, $revision_ids);
catch
committed
}
$data = $query->execute();
$field_definitions = \Drupal::entityManager()->getBaseFieldDefinitions($this->entityTypeId);
catch
committed
$translations = array();
if ($this->revisionDataTable) {
$data_column_names = array_flip(array_diff(drupal_schema_fields_sql($this->entityType->getRevisionDataTable()), drupal_schema_fields_sql($this->entityType->getBaseTable())));
catch
committed
}
else {
$data_column_names = array_flip(drupal_schema_fields_sql($this->entityType->getDataTable()));
catch
committed
}
foreach ($data as $values) {
$id = $values[$this->idKey];
// Field values in default language are stored with
// Language::LANGCODE_DEFAULT as key.
$langcode = empty($values['default_langcode']) ? $values['langcode'] : Language::LANGCODE_DEFAULT;
$translations[$id][$langcode] = TRUE;
catch
committed
foreach (array_keys($field_definitions) as $field_name) {
// Handle columns named directly after the field.
if (isset($data_column_names[$field_name])) {
$entities[$id][$field_name][$langcode] = $values[$field_name];
}
else {
// @todo Change this logic to be based on a mapping of field
// definition properties (translatability, revisionability) in
// https://drupal.org/node/2144631.
foreach ($data_column_names as $data_column_name) {
// Handle columns named [field_name]__[column_name], for which we
// need to look through all column names from the table that start
// with the name of the field.
if (($data_field_name = strstr($data_column_name, '__', TRUE)) && $data_field_name === $field_name) {
$property_name = substr($data_column_name, strpos($data_column_name, '__') + 2);
$entities[$id][$field_name][$langcode][$property_name] = $values[$data_column_name];
}
}
catch
committed
}
}
}
foreach ($entities as $id => $values) {
$bundle = $this->bundleKey ? $values[$this->bundleKey][Language::LANGCODE_DEFAULT] : FALSE;
// Turn the record into an entity class.
$entities[$id] = new $this->entityClass($values, $this->entityTypeId, $bundle, array_keys($translations[$id]));
catch
committed
}
}
}
/**
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::loadRevision().
*/
public function loadRevision($revision_id) {
// Build and execute the query.
$query_result = $this->buildQuery(array(), $revision_id)->execute();
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
Alex Pott
committed
// Pass the loaded entities from the database through $this->postLoad(),
catch
committed
// which attaches fields (if supported by the entity type) and calls the
// entity type specific load callback, for example hook_node_load().
if (!empty($queried_entities)) {
Alex Pott
committed
$this->postLoad($queried_entities);
catch
committed
}
return reset($queried_entities);
}
/**
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::deleteRevision().
*/
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);
$this->deleteFieldItemsRevision($revision);
$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 http://drupal.org/node/1866330.
// Default to the original entity language if not explicitly specified
// otherwise.
if (!array_key_exists('default_langcode', $values)) {
$values['default_langcode'] = 1;
}
// 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.
elseif ($values['default_langcode'] === NULL) {
unset($values['default_langcode']);
}
}
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.
* See Drupal\comment\CommentStorageController::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 SelectQuery
* 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.
$entity_fields = drupal_schema_fields_sql($this->entityType->getBaseTable());
catch
committed
if ($this->revisionTable) {
// Add all fields from the {entity_revision} table.
catch
committed
$entity_revision_fields = drupal_schema_fields_sql($this->entityType->getRevisionTable());
$entity_revision_fields = array_combine($entity_revision_fields, $entity_revision_fields);
catch
committed
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
// 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('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision');
}
$query->fields('base', $entity_fields);
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
return $query;
}
/**
* Attaches data to entities upon loading.
*
* This will attach fields, if the entity is fieldable. It calls
* hook_entity_load() for modules which need to add data to all entities.
* It also calls hook_TYPE_load() on the loaded entities. For example
* hook_node_load() or hook_user_load(). If your hook_TYPE_load()
* expects special parameters apart from the queried entities, you can set
* $this->hookLoadArguments prior to calling the method.
* See Drupal\node\NodeStorageController::attachLoad() for an example.
*
* @param $queried_entities
* Associative array of query results, keyed on the entity ID.
*/
Alex Pott
committed
protected function postLoad(array &$queried_entities) {
catch
committed
// Map the loaded records into entity objects and according fields.
Alex Pott
committed
$queried_entities = $this->mapFromStorageRecords($queried_entities);
catch
committed
// Attach field values.
if ($this->entityType->isFieldable()) {
Alex Pott
committed
$this->loadFieldItems($queried_entities);
catch
committed
}
Alex Pott
committed
parent::postLoad($queried_entities);
catch
committed
}
/**
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::delete().
*/
public function delete(array $entities) {
if (!$entities) {
// If no IDs or invalid IDs were passed, do nothing.
return;
}
$transaction = $this->database->startTransaction();
try {
$entity_class = $this->entityClass;
$entity_class::preDelete($this, $entities);
foreach ($entities as $entity) {
$this->invokeHook('predelete', $entity);
}
$ids = array_keys($entities);
$this->database->delete($this->entityType->getBaseTable())
catch
committed
->condition($this->idKey, $ids)
->execute();
if ($this->revisionTable) {
$this->database->delete($this->revisionTable)
->condition($this->idKey, $ids)
->execute();
}
if ($this->dataTable) {
$this->database->delete($this->dataTable)
->condition($this->idKey, $ids)
->execute();
}
if ($this->revisionDataTable) {
$this->database->delete($this->revisionDataTable)
->condition($this->idKey, $ids)
->execute();
}
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->deleteFieldItems($entity);
}
catch
committed
// Reset the cache as soon as the changes have been applied.
$this->resetCache($ids);
$entity_class::postDelete($this, $entities);
foreach ($entities as $entity) {
$this->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
db_ignore_slave();
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityTypeId, $e);
catch
committed
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@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();
// Load the stored entity, if any.
if (!$entity->isNew() && !isset($entity->original)) {
Angie Byron
committed
$id = $entity->id();
if ($entity->getOriginalId() !== NULL) {
$id = $entity->getOriginalId();
}
$entity->original = $this->loadUnchanged($id);
catch
committed
}
$entity->preSave($this);
$this->invokeFieldMethod('preSave', $entity);
$this->invokeHook('presave', $entity);
// Create the storage record to be saved.
$record = $this->mapToStorageRecord($entity);
if (!$entity->isNew()) {
if ($entity->isDefaultRevision()) {
$return = drupal_write_record($this->entityType->getBaseTable(), $record, $this->idKey);
catch
committed
}
else {
// @todo, should a different value be returned when saving an entity
// with $isDefaultRevision = FALSE?
$return = FALSE;
}
if ($this->revisionTable) {
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
if ($this->dataTable) {
$this->savePropertyData($entity);
}
if ($this->revisionDataTable) {
$this->savePropertyData($entity, 'revision_data_table');
}
$entity->setNewRevision(FALSE);
$this->invokeFieldMethod('update', $entity);
$this->saveFieldItems($entity, TRUE);
$this->resetCache(array($entity->id()));
$entity->postSave($this, TRUE);
catch
committed
$this->invokeHook('update', $entity);
if ($this->dataTable) {
$this->invokeTranslationHooks($entity);
}
}
else {
// Ensure the entity is still seen as new after assigning it an id,
// while storing its data.
$entity->enforceIsNew();
$return = drupal_write_record($this->entityType->getBaseTable(), $record);
catch
committed
$entity->{$this->idKey}->value = (string) $record->{$this->idKey};
if ($this->revisionTable) {
$entity->setNewRevision();
$record->{$this->revisionKey} = $this->saveRevision($entity);
}
if ($this->dataTable) {
$this->savePropertyData($entity);
}
if ($this->revisionDataTable) {
$this->savePropertyData($entity, 'revision_data_table');
}
$entity->enforceIsNew(FALSE);
$this->invokeFieldMethod('insert', $entity);
$this->saveFieldItems($entity, FALSE);
// Reset general caches, but keep caches specific to certain entities.
$this->resetCache(array());
$entity->postSave($this, FALSE);
catch
committed
$this->invokeHook('insert', $entity);
}
// Ignore slave server temporarily.
db_ignore_slave();
unset($entity->original);
return $return;
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityTypeId, $e);
catch
committed
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Stores the entity property language-aware data.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param string $table_key
* (optional) The entity key identifying the target table. Defaults to
* 'data_table'.
*/
protected function savePropertyData(EntityInterface $entity, $table_key = 'data_table') {
$table_name = $this->entityType->get($table_key);
catch
committed
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
$revision = $table_key != 'data_table';
if (!$revision || !$entity->isNewRevision()) {
$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_key);
$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\EntityInterface $entity
* The entity object.
* @param string $table_key
* (optional) The entity key identifying the target table. Defaults to
* 'base_table'.
*
* @return \stdClass
* The record to store.
*/
protected function mapToStorageRecord(EntityInterface $entity, $table_key = 'base_table') {
$record = new \stdClass();
catch
committed
$values = array();
$definitions = $entity->getFieldDefinitions();
$schema = drupal_get_schema($this->entityType->get($table_key));
catch
committed
$is_new = $entity->isNew();
catch
committed
$multi_column_fields = array();
foreach (drupal_schema_fields_sql($this->entityType->get($table_key)) as $name) {
catch
committed
// Check for fields which store data in multiple columns and process them
// separately.
if ($field = strstr($name, '__', TRUE)) {
$multi_column_fields[$field] = TRUE;
continue;
}
$values[$name] = isset($definitions[$name]) && isset($entity->$name->value) ? $entity->$name->value : NULL;
}
// Handle fields that store multiple properties and match each property name
// to its schema column name.
foreach (array_keys($multi_column_fields) as $field_name) {
$field_items = $entity->get($field_name);
$field_value = $field_items->getValue();
foreach (array_keys($field_items->getFieldDefinition()->getColumns()) as $field_schema_column) {
if (isset($schema['fields'][$field_name . '__' . $field_schema_column])) {
$values[$field_name . '__' . $field_schema_column] = isset($field_value[0][$field_schema_column]) ? $field_value[0][$field_schema_column] : NULL;
catch
committed
}
}
}
foreach ($values as $field_name => $value) {
catch
committed
// If we are creating a new entity, we must not populate the record with
// NULL values otherwise defaults would not be applied.
if (isset($value) || !$is_new) {
catch
committed
$record->$field_name = drupal_schema_get_field_value($schema['fields'][$field_name], $value);
catch
committed
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
}
}
return $record;
}
/**
* 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_key
* (optional) The entity key identifying the target table. Defaults to
* 'data_table'.
*
* @return \stdClass
* The record to store.
*/
protected function mapToDataStorageRecord(EntityInterface $entity, $table_key = 'data_table') {
$record = $this->mapToStorageRecord($entity, $table_key);
$record->langcode = $entity->language()->id;
$record->default_langcode = intval($record->langcode == $entity->getUntranslated()->language()->id);
return $record;
}
/**
* Saves an entity revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return int
* The revision id.
*/
protected function saveRevision(EntityInterface $entity) {
$record = $this->mapToStorageRecord($entity, 'revision_table');
// When saving a new revision, set any existing revision ID to NULL so as to
// ensure that a new revision will actually be created.
if ($entity->isNewRevision() && isset($record->{$this->revisionKey})) {
$record->{$this->revisionKey} = NULL;
}
$entity->preSaveRevision($this, $record);
if ($entity->isNewRevision()) {
drupal_write_record($this->revisionTable, $record);
if ($entity->isDefaultRevision()) {
$this->database->update($this->entityType->getBaseTable())
catch
committed
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
->fields(array($this->revisionKey => $record->{$this->revisionKey}))
->condition($this->idKey, $record->{$this->idKey})
->execute();
}
}
else {
drupal_write_record($this->revisionTable, $record, $this->revisionKey);
}
// Make sure to update the new revision key for the entity.
$entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
return $record->{$this->revisionKey};
}
/**
* {@inheritdoc}
*/
public function getQueryServiceName() {
return 'entity.query.sql';
}
/**
* {@inheritdoc}
*/
protected function doLoadFieldItems($entities, $age) {
$load_current = $age == static::FIELD_LOAD_CURRENT;
// Collect entities ids, bundles and languages.
catch
committed
$bundles = array();
$ids = array();
$default_langcodes = array();
catch
committed
foreach ($entities as $key => $entity) {
$bundles[$entity->bundle()] = TRUE;
$ids[] = $load_current ? $key : $entity->getRevisionId();
$default_langcodes[$key] = $entity->getUntranslated()->language()->id;
catch
committed
}
// Collect impacted fields.
$fields = array();
foreach ($bundles as $bundle => $v) {
foreach ($this->fieldInfo->getBundleInstances($this->entityTypeId, $bundle) as $field_name => $instance) {
catch
committed
$fields[$field_name] = $instance->getField();
}
}
// Load field data.
$langcodes = array_keys(language_list(Language::STATE_ALL));
catch
committed
foreach ($fields as $field_name => $field) {
$table = $load_current ? static::_fieldTableName($field) : static::_fieldRevisionTableName($field);
// 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.
catch
committed
$results = $this->database->select($table, 't')
->fields('t')
->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
->condition('deleted', 0)
->condition('langcode', $langcodes, 'IN')
->orderBy('delta')
->execute();
$delta_count = array();
foreach ($results as $row) {
Alex Pott
committed
// Ensure that records for non-translatable fields having invalid
// languages are skipped.
Alex Pott
committed
if ($row->langcode == $default_langcodes[$row->entity_id] || $field->isTranslatable()) {
if (!isset($delta_count[$row->entity_id][$row->langcode])) {
$delta_count[$row->entity_id][$row->langcode] = 0;
catch
committed
}
if ($field->getCardinality() == FieldConfigInterface::CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field->getCardinality()) {
$item = array();
// For each column declared by the field, populate the item from the
// prefixed database column.
foreach ($field->getColumns() as $column => $attributes) {
$column_name = static::_fieldColumnName($field, $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.
$entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}[$delta_count[$row->entity_id][$row->langcode]] = $item;
$delta_count[$row->entity_id][$row->langcode]++;
}
catch
committed
}
}
}
}
/**
* {@inheritdoc}
*/
protected function doSaveFieldItems(EntityInterface $entity, $update) {
$vid = $entity->getRevisionId();
$id = $entity->id();
$bundle = $entity->bundle();
$entity_type = $entity->getEntityTypeId();
$default_langcode = $entity->getUntranslated()->language()->id;
$translation_langcodes = array_keys($entity->getTranslationLanguages());
catch
committed
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
if (!isset($vid)) {
$vid = $id;
}
foreach ($this->fieldInfo->getBundleInstances($entity_type, $bundle) as $field_name => $instance) {
$field = $instance->getField();
$table_name = static::_fieldTableName($field);
$revision_name = static::_fieldRevisionTableName($field);
// 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();
}
$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 ($field->getColumns() as $column => $attributes) {
$columns[] = static::_fieldColumnName($field, $column);
}
$query = $this->database->insert($table_name)->fields($columns);
$revision_query = $this->database->insert($revision_name)->fields($columns);
Alex Pott
committed
$langcodes = $field->isTranslatable() ? $translation_langcodes : array($default_langcode);
catch
committed
foreach ($langcodes as $langcode) {
$delta_count = 0;
$items = $entity->getTranslation($langcode)->get($field_name);
$items->filterEmptyItems();
catch
committed
foreach ($items as $delta => $item) {
// We now know we have someting to insert.
$do_insert = TRUE;
$record = array(
'entity_id' => $id,
'revision_id' => $vid,
'bundle' => $bundle,
'delta' => $delta,
'langcode' => $langcode,
);
foreach ($field->getColumns() as $column => $attributes) {
$column_name = static::_fieldColumnName($field, $column);
// Serialize the value if specified in the column schema.
$record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column;
}
$query->values($record);
$revision_query->values($record);
if ($field->getCardinality() != FieldConfigInterface::CARDINALITY_UNLIMITED && ++$delta_count == $field->getCardinality()) {
catch
committed
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();
}
$revision_query->execute();
}
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteFieldItems(EntityInterface $entity) {
foreach ($this->fieldInfo->getBundleInstances($entity->getEntityTypeId(), $entity->bundle()) as $instance) {
catch
committed
$field = $instance->getField();
$table_name = static::_fieldTableName($field);
$revision_name = static::_fieldRevisionTableName($field);
$this->database->delete($table_name)
->condition('entity_id', $entity->id())
->execute();
$this->database->delete($revision_name)
->condition('entity_id', $entity->id())
->execute();
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteFieldItemsRevision(EntityInterface $entity) {
$vid = $entity->getRevisionId();
if (isset($vid)) {
foreach ($this->fieldInfo->getBundleInstances($entity->getEntityTypeId(), $entity->bundle()) as $instance) {
catch
committed
$revision_name = static::_fieldRevisionTableName($instance->getField());
$this->database->delete($revision_name)
->condition('entity_id', $entity->id())
->condition('revision_id', $vid)
->execute();
}
}
}
/**
* {@inheritdoc}
*/
public function onFieldCreate(FieldConfigInterface $field) {
catch
committed
$schema = $this->_fieldSqlSchema($field);
foreach ($schema as $name => $table) {
$this->database->schema()->createTable($name, $table);
}
}
/**
* {@inheritdoc}
*/
public function onFieldUpdate(FieldConfigInterface $field) {
catch
committed
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
$original = $field->original;
if (!$field->hasData()) {
// There is no data. Re-create the tables completely.
if ($this->database->supportsTransactionalDDL()) {
// If the database supports transactional DDL, we can go ahead and rely
// on it. If not, we will have to rollback manually if something fails.
$transaction = $this->database->startTransaction();
}
try {
$original_schema = $this->_fieldSqlSchema($original);
foreach ($original_schema as $name => $table) {
$this->database->schema()->dropTable($name, $table);
}
$schema = $this->_fieldSqlSchema($field);
foreach ($schema as $name => $table) {
$this->database->schema()->createTable($name, $table);
}
}
catch (\Exception $e) {
if ($this->database->supportsTransactionalDDL()) {
$transaction->rollback();
}