Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Contains \Drupal\Core\Entity\DatabaseStorageController.
Dries Buytaert
committed
*/
namespace Drupal\Core\Entity;
Dries Buytaert
committed
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Language\Language;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Uuid\Uuid;
use Drupal\field\FieldInfo;
use Drupal\field\FieldUpdateForbiddenException;
use Drupal\field\FieldInterface;
use Drupal\field\FieldInstanceInterface;
use Drupal\field\Entity\Field;
use Symfony\Component\DependencyInjection\ContainerInterface;
Dries Buytaert
committed
/**
* Defines a base entity controller class.
*
* Default implementation of Drupal\Core\Entity\EntityStorageControllerInterface.
Dries Buytaert
committed
*
* This class can be used as-is by most simple entity types. Entity types
* requiring special handling can extend the class.
*/
class DatabaseStorageController extends FieldableEntityStorageControllerBase {
Dries Buytaert
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;
/**
* The table that stores revisions, if the entity supports revisions.
*
* @var string
*/
protected $revisionTable;
/**
* Whether this entity type should use the static cache.
*
* Set by entity info.
*
* @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}
*/
public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) {
return new static(
$entity_type,
$entity_info,
$container->get('database'),
$container->get('field.info')
);
}
Dries Buytaert
committed
Dries Buytaert
committed
/**
Dries Buytaert
committed
* Constructs a DatabaseStorageController object.
*
* @param string $entity_type
Dries Buytaert
committed
* The entity type for which the instance is created.
* @param array $entity_info
* An array of entity info for the entity type.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
* @param \Drupal\field\FieldInfo $field_info
* The field info service.
Dries Buytaert
committed
*/
public function __construct($entity_type, array $entity_info, Connection $database, FieldInfo $field_info) {
Alex Pott
committed
parent::__construct($entity_type, $entity_info);
$this->database = $database;
$this->fieldInfo = $field_info;
Dries Buytaert
committed
// Check if the entity type supports IDs.
if (isset($this->entityInfo['entity_keys']['id'])) {
$this->idKey = $this->entityInfo['entity_keys']['id'];
}
else {
$this->idKey = FALSE;
}
Dries Buytaert
committed
Dries Buytaert
committed
// Check if the entity type supports UUIDs.
Angie Byron
committed
if (!empty($this->entityInfo['entity_keys']['uuid'])) {
$this->uuidKey = $this->entityInfo['entity_keys']['uuid'];
Dries Buytaert
committed
}
else {
$this->uuidKey = FALSE;
}
Dries Buytaert
committed
// Check if the entity type supports revisions.
Angie Byron
committed
if (!empty($this->entityInfo['entity_keys']['revision'])) {
$this->revisionKey = $this->entityInfo['entity_keys']['revision'];
$this->revisionTable = $this->entityInfo['revision_table'];
Dries Buytaert
committed
}
else {
$this->revisionKey = FALSE;
}
}
/**
Dries Buytaert
committed
* {@inheritdoc}
Dries Buytaert
committed
*/
Dries Buytaert
committed
public function loadMultiple(array $ids = NULL) {
Dries Buytaert
committed
$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);
Dries Buytaert
committed
// 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();
Angie Byron
committed
if (!empty($this->entityInfo['class'])) {
// We provide the necessary arguments for PDO to create objects of the
// specified entity class.
// @see Drupal\Core\Entity\EntityInterface::__construct()
$query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityInfo['class'], array(array(), $this->entityType));
}
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
Dries Buytaert
committed
}
// Pass all entities loaded from the database through $this->attachLoad(),
// 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)) {
$this->attachLoad($queried_entities);
Dries Buytaert
committed
$entities += $queried_entities;
}
if ($this->cache) {
// Add entities to the cache.
if (!empty($queried_entities)) {
Dries Buytaert
committed
$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) {
Dries Buytaert
committed
$passed_ids[$entity->id()] = $entity;
Dries Buytaert
committed
}
$entities = $passed_ids;
}
return $entities;
}
Dries Buytaert
committed
/**
* {@inheritdoc}
*/
public function load($id) {
$entities = $this->loadMultiple(array($id));
return isset($entities[$id]) ? $entities[$id] : NULL;
}
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::loadRevision().
*/
public function loadRevision($revision_id) {
// Build and execute the query.
$query_result = $this->buildQuery(array(), $revision_id)->execute();
Angie Byron
committed
if (!empty($this->entityInfo['class'])) {
// We provide the necessary arguments for PDO to create objects of the
// specified entity class.
// @see Drupal\Core\Entity\EntityInterface::__construct()
$query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityInfo['class'], array(array(), $this->entityType));
}
$queried_entities = $query_result->fetchAllAssoc($this->idKey);
// Pass the loaded entities from the database through $this->attachLoad(),
// 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)) {
$this->attachLoad($queried_entities, $revision_id);
}
return reset($queried_entities);
}
Angie Byron
committed
/**
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::deleteRevision().
Angie Byron
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)
Angie Byron
committed
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
$this->invokeFieldMethod('deleteRevision', $revision);
$this->deleteFieldItemsRevision($revision);
Angie Byron
committed
$this->invokeHook('revision_delete', $revision);
}
}
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::loadByProperties().
*/
public function loadByProperties(array $values = array()) {
// Build a query to fetch the entity IDs.
Alex Pott
committed
$entity_query = \Drupal::entityQuery($this->entityType);
$this->buildPropertyQuery($entity_query, $values);
$result = $entity_query->execute();
Dries Buytaert
committed
return $result ? $this->loadMultiple($result) : array();
}
/**
* Builds an entity query.
*
* @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
* EntityQuery instance.
* @param array $values
* An associative array of properties of the entity, where the keys are the
* property names and the values are the values those properties must have.
*/
protected function buildPropertyQuery(QueryInterface $entity_query, array $values) {
foreach ($values as $name => $value) {
$entity_query->condition($name, $value);
}
}
Dries Buytaert
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.
Dries Buytaert
committed
*
* @param array|null $ids
* An array of entity IDs, or NULL to load all entities.
Dries Buytaert
committed
* @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->entityInfo['base_table'], 'base');
Dries Buytaert
committed
$query->addTag($this->entityType . '_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->revisionKey) {
$query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
}
// Add fields from the {entity} table.
$entity_fields = drupal_schema_fields_sql($this->entityInfo['base_table']);
Dries Buytaert
committed
if ($this->revisionKey) {
// Add all fields from the {entity_revision} table.
$entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->entityInfo['revision_table']));
Dries Buytaert
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) {
Dries Buytaert
committed
if (isset($entity_field_keys[$name])) {
unset($entity_fields[$entity_field_keys[$name]]);
}
}
$query->fields('revision', $entity_revision_fields);
Dries Buytaert
committed
// Compare revision id of the base and revision table, if equal then this
Angie Byron
committed
// is the default revision.
$query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision');
Dries Buytaert
committed
}
$query->fields('base', $entity_fields);
if ($ids) {
$query->condition("base.{$this->idKey}", $ids, 'IN');
}
Dries Buytaert
committed
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.
Angie Byron
committed
* See Drupal\node\NodeStorageController::attachLoad() for an example.
Dries Buytaert
committed
*
* @param $queried_entities
* Associative array of query results, keyed on the entity ID.
* @param $load_revision
* (optional) TRUE if the revision should be loaded, defaults to FALSE.
Dries Buytaert
committed
*/
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
// Attach field values.
Dries Buytaert
committed
if ($this->entityInfo['fieldable']) {
$this->loadFieldItems($queried_entities, $load_revision ? static::FIELD_LOAD_REVISION : static::FIELD_LOAD_CURRENT);
Dries Buytaert
committed
}
// Call hook_entity_load().
catch
committed
foreach (\Drupal::moduleHandler()->getImplementations('entity_load') as $module) {
Dries Buytaert
committed
$function = $module . '_entity_load';
$function($queried_entities, $this->entityType);
}
// Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
// always the queried entities, followed by additional arguments set in
// $this->hookLoadArguments.
$args = array_merge(array($queried_entities), $this->hookLoadArguments);
catch
committed
foreach (\Drupal::moduleHandler()->getImplementations($this->entityType . '_load') as $module) {
Angie Byron
committed
call_user_func_array($module . '_' . $this->entityType . '_load', $args);
Dries Buytaert
committed
}
}
/**
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::create().
*/
public function create(array $values) {
$entity_class = $this->entityInfo['class'];
$entity_class::preCreate($this, $values);
Dries Buytaert
committed
$entity = new $entity_class($values, $this->entityType);
Dries Buytaert
committed
// Assign a new UUID if there is none yet.
if ($this->uuidKey && !isset($entity->{$this->uuidKey})) {
$uuid = new Uuid();
$entity->{$this->uuidKey} = $uuid->generate();
}
$entity->postCreate($this);
Dries Buytaert
committed
Angie Byron
committed
// Modules might need to add or change the data initially held by the new
// entity object, for instance to fill-in default values.
$this->invokeHook('create', $entity);
Dries Buytaert
committed
return $entity;
}
/**
* 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->entityInfo['class'];
$entity_class::preDelete($this, $entities);
foreach ($entities as $entity) {
$this->invokeHook('predelete', $entity);
}
$ids = array_keys($entities);
$this->database->delete($this->entityInfo['base_table'])
->condition($this->idKey, $ids, 'IN')
->execute();
Angie Byron
committed
if ($this->revisionKey) {
$this->database->delete($this->revisionTable)
Angie Byron
committed
->condition($this->idKey, $ids, 'IN')
->execute();
}
// Reset the cache as soon as the changes have been applied.
$this->resetCache($ids);
$entity_class::postDelete($this, $entities);
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->deleteFieldItems($entity);
$this->invokeHook('delete', $entity);
}
// Ignore slave server temporarily.
db_ignore_slave();
}
Angie Byron
committed
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityType, $e);
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Implements \Drupal\Core\Entity\EntityStorageControllerInterface::save().
*/
public function save(EntityInterface $entity) {
$transaction = $this->database->startTransaction();
try {
// Load the stored entity, if any.
if (!$entity->isNew() && !isset($entity->original)) {
$entity->original = entity_load_unchanged($this->entityType, $entity->id());
}
$entity->preSave($this);
$this->invokeFieldMethod('preSave', $entity);
$this->invokeHook('presave', $entity);
if (!$entity->isNew()) {
Angie Byron
committed
if ($entity->isDefaultRevision()) {
Angie Byron
committed
$return = drupal_write_record($this->entityInfo['base_table'], $entity, $this->idKey);
Angie Byron
committed
}
else {
// @todo, should a different value be returned when saving an entity
// with $isDefaultRevision = FALSE?
$return = FALSE;
}
if ($this->revisionKey) {
$this->saveRevision($entity);
}
Dries Buytaert
committed
$this->resetCache(array($entity->id()));
$entity->postSave($this, TRUE);
$this->invokeFieldMethod('update', $entity);
$this->saveFieldItems($entity, TRUE);
$this->invokeHook('update', $entity);
}
else {
Angie Byron
committed
$return = drupal_write_record($this->entityInfo['base_table'], $entity);
Angie Byron
committed
if ($this->revisionKey) {
$this->saveRevision($entity);
}
// Reset general caches, but keep caches specific to certain entities.
$this->resetCache(array());
$entity->enforceIsNew(FALSE);
$entity->postSave($this, FALSE);
$this->invokeFieldMethod('insert', $entity);
$this->saveFieldItems($entity, FALSE);
$this->invokeHook('insert', $entity);
}
// Ignore slave server temporarily.
db_ignore_slave();
unset($entity->original);
return $return;
}
Angie Byron
committed
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityType, $e);
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
Angie Byron
committed
/**
* Saves an entity revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
Angie Byron
committed
* The entity object.
*/
protected function saveRevision(EntityInterface $entity) {
// Convert the entity into an array as it might not have the same properties
// as the entity, it is just a raw structure.
$record = (array) $entity;
// When saving a new revision, set any existing revision ID to NULL so as to
// ensure that a new revision will actually be created.
Angie Byron
committed
if ($entity->isNewRevision() && $record[$this->revisionKey]) {
$record[$this->revisionKey] = NULL;
}
// Cast to object as preSaveRevision() expects one to be compatible with the
// upcoming NG storage controller.
$record = (object) $record;
$entity->preSaveRevision($this, $record);
$record = (array) $record;
Angie Byron
committed
if ($entity->isNewRevision()) {
drupal_write_record($this->revisionTable, $record);
if ($entity->isDefaultRevision()) {
$this->database->update($this->entityInfo['base_table'])
Angie Byron
committed
->fields(array($this->revisionKey => $record[$this->revisionKey]))
->condition($this->idKey, $entity->id())
->execute();
}
$entity->setNewRevision(FALSE);
}
else {
drupal_write_record($this->revisionTable, $record, $this->revisionKey);
}
// Make sure to update the new revision key for the entity.
$entity->{$this->revisionKey} = $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 and bundles.
$bundles = array();
$ids = array();
foreach ($entities as $key => $entity) {
$bundles[$entity->bundle()] = TRUE;
$ids[] = $load_current ? $key : $entity->getRevisionId();
}
// Collect impacted fields.
$fields = array();
foreach ($bundles as $bundle => $v) {
foreach ($this->fieldInfo->getBundleInstances($this->entityType, $bundle) as $field_name => $instance) {
$fields[$field_name] = $instance->getField();
}
}
// Load field data.
$all_langcodes = array_keys(language_list());
foreach ($fields as $field_name => $field) {
$table = $load_current ? static::_fieldTableName($field) : static::_fieldRevisionTableName($field);
// If the field is translatable 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.
$langcodes = $field['translatable'] ? $all_langcodes : array(Language::LANGCODE_NOT_SPECIFIED);
$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) {
if (!isset($delta_count[$row->entity_id][$row->langcode])) {
$delta_count[$row->entity_id][$row->langcode] = 0;
}
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->langcode] < $field['cardinality']) {
$item = array();
// For each column declared by the field, populate the item from the
// prefixed database column.
foreach ($field['columns'] 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]++;
}
}
}
}
/**
* {@inheritdoc}
*/
protected function doSaveFieldItems(EntityInterface $entity, $update) {
$vid = $entity->getRevisionId();
$id = $entity->id();
$bundle = $entity->bundle();
$entity_type = $entity->entityType();
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['columns'] 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);
$langcodes = $field['translatable'] ? array_keys($entity->getTranslationLanguages()) : array(Language::LANGCODE_NOT_SPECIFIED);
foreach ($langcodes as $langcode) {
$items = $entity->getTranslation($langcode)->{$field_name}->getValue();
if (!isset($items)) {
continue;
}
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
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
752
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
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
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
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
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
$delta_count = 0;
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['columns'] as $column => $attributes) {
$column_name = static::_fieldColumnName($field, $column);
$value = isset($item[$column]) ? $item[$column] : NULL;
// Serialize the value if specified in the column schema.
$record[$column_name] = (!empty($attributes['serialize'])) ? serialize($value) : $value;
}
$query->values($record);
$revision_query->values($record);
if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
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->entityType(), $entity->bundle()) as $instance) {
$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->entityType(), $entity->bundle()) as $instance) {
$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(FieldInterface $field) {
$schema = $this->_fieldSqlSchema($field);
foreach ($schema as $name => $table) {
$this->database->schema()->createTable($name, $table);
}
}
/**
* {@inheritdoc}
*/
public function onFieldUpdate(FieldInterface $field) {
$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();
}
else {
// Recreate tables.
$original_schema = $this->_fieldSqlSchema($original);
foreach ($original_schema as $name => $table) {
if (!$this->database->schema()->tableExists($name)) {
$this->database->schema()->createTable($name, $table);
}
}
}
throw $e;
}
}
else {
if ($field['columns'] != $original['columns']) {
throw new FieldUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
}
// There is data, so there are no column changes. Drop all the prior
// indexes and create all the new ones, except for all the priors that
// exist unchanged.
$table = static::_fieldTableName($original);
$revision_table = static::_fieldRevisionTableName($original);
$schema = $field->getSchema();
$original_schema = $original->getSchema();
foreach ($original_schema['indexes'] as $name => $columns) {
if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
$real_name = static::_fieldIndexName($field, $name);
$this->database->schema()->dropIndex($table, $real_name);
$this->database->schema()->dropIndex($revision_table, $real_name);
}
}
$table = static::_fieldTableName($field);
$revision_table = static::_fieldRevisionTableName($field);
foreach ($schema['indexes'] as $name => $columns) {
if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
$real_name = static::_fieldIndexName($field, $name);
$real_columns = array();
foreach ($columns as $column_name) {
// Indexes can be specified as either a column name or an array with
// column name and length. Allow for either case.
if (is_array($column_name)) {
$real_columns[] = array(
static::_fieldColumnName($field, $column_name[0]),
$column_name[1],
);
}
else {
$real_columns[] = static::_fieldColumnName($field, $column_name);
}
}
$this->database->schema()->addIndex($table, $real_name, $real_columns);
$this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
}
}
}
}
/**
* {@inheritdoc}
*/
public function onFieldDelete(FieldInterface $field) {
// Mark all data associated with the field for deletion.
$field['deleted'] = FALSE;
$table = static::_fieldTableName($field);
$revision_table = static::_fieldRevisionTableName($field);
$this->database->update($table)
->fields(array('deleted' => 1))
->execute();
// Move the table to a unique name while the table contents are being
// deleted.
$field['deleted'] = TRUE;
$new_table = static::_fieldTableName($field);
$revision_new_table = static::_fieldRevisionTableName($field);
$this->database->schema()->renameTable($table, $new_table);
$this->database->schema()->renameTable($revision_table, $revision_new_table);
}
/**
* {@inheritdoc}
*/
public function onInstanceDelete(FieldInstanceInterface $instance) {
$field = $instance->getField();
$table_name = static::_fieldTableName($field);
$revision_name = static::_fieldRevisionTableName($field);
$this->database->update($table_name)
->fields(array('deleted' => 1))
->condition('bundle', $instance['bundle'])
->execute();
$this->database->update($revision_name)
->fields(array('deleted' => 1))
->condition('bundle', $instance['bundle'])
->execute();
}
/**
* {@inheritdoc}
*/
public function onBundleRename($bundle, $bundle_new) {
// We need to account for deleted or inactive fields and instances.
$instances = field_read_instances(array('entity_type' => $this->entityType, 'bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
foreach ($instances as $instance) {
$field = $instance->getField();
if ($field['storage']['type'] == 'field_sql_storage') {
$table_name = static::_fieldTableName($field);
$revision_name = static::_fieldRevisionTableName($field);
$this->database->update($table_name)
->fields(array('bundle' => $bundle_new))
->condition('bundle', $bundle)
->execute();
$this->database->update($revision_name)
->fields(array('bundle' => $bundle_new))
->condition('bundle', $bundle)
->execute();
}
}
}
/**
* {@inheritdoc}
*/
protected function readFieldItemsToPurge(EntityInterface $entity, FieldInstanceInterface $instance) {
$field = $instance->getField();
$table_name = static::_fieldTableName($field);
$query = $this->database->select($table_name, 't', array('fetch' => \PDO::FETCH_ASSOC))
->condition('entity_id', $entity->id())
->orderBy('delta');
foreach ($field->getColumns() as $column_name => $data) {
$query->addField('t', static::_fieldColumnName($field, $column_name), $column_name);
}
return $query->execute()->fetchAll();
}
/**
* {@inheritdoc}
*/
public function purgeFieldItems(EntityInterface $entity, FieldInstanceInterface $instance) {
$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}
*/
public function onFieldPurge(FieldInterface $field) {
$table_name = static::_fieldTableName($field);
$revision_name = static::_fieldRevisionTableName($field);
$this->database->schema()->dropTable($table_name);
$this->database->schema()->dropTable($revision_name);
}
/**
* Gets the SQL table schema.
*
* @private Calling this function circumvents the entity system and is
* strongly discouraged. This function is not considered part of the public
* API and modules relying on it might break even in minor releases.
*
* @param \Drupal\field\FieldInterface $field
* The field object
* @param array $schema
* The field schema array. Mandatory for upgrades, omit otherwise.
*
* @return array
* The same as a hook_schema() implementation for the data and the
* revision tables.
*
* @see hook_schema()
*/
public static function _fieldSqlSchema(FieldInterface $field, array $schema = NULL) {
if ($field['deleted']) {
$description_current = "Data storage for deleted field {$field['id']} ({$field['entity_type']}, {$field['field_name']}).";
$description_revision = "Revision archive storage for deleted field {$field['id']} ({$field['entity_type']}, {$field['field_name']}).";
}
else {
$description_current = "Data storage for {$field['entity_type']} field {$field['field_name']}.";
$description_revision = "Revision archive storage for {$field['entity_type']} field {$field['field_name']}.";
}
$current = array(
'description' => $description_current,
'fields' => array(
'bundle' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
),
'deleted' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
'description' => 'A boolean indicating whether this data item has been deleted'
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this data is attached to',
),
'revision_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => FALSE,
'description' => 'The entity revision id this data is attached to, or NULL if the entity type is not versioned',
),
'langcode' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The language code for this data item.',
),
'delta' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The sequence number for this data item, used for multi-value fields',
),
),
'primary key' => array('entity_id', 'deleted', 'delta', 'langcode'),