diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index a70ad36..268bc8b 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -136,18 +136,7 @@ public function loadMultiple(array $ids = NULL) { $entities += $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->{$this->idKey}] = $entity; - } - $entities = $passed_ids; - } - - return $entities; + return $this->ensureOrder($passed_ids, $entities); } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index f0855ef..61c1e9c 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Entity\Plugin\DataType\EntityReference; +use Drupal\Core\Field\PrepareCacheInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Language\Language; use Drupal\Core\Session\AccountInterface; @@ -987,4 +988,38 @@ public function referencedEntities() { return $referenced_entities; } + /** + * {@inheritdoc} + */ + public function getCacheData() { + $data = $this->values; + $default_langcode = $this->getUntranslated()->language()->id; + foreach ($this->getTranslationLanguages() as $langcode => $language) { + $translation = $this->getTranslation($langcode); + // Make sure the default language is valid. + if ($default_langcode == $langcode) { + $langcode = Language::LANGCODE_DEFAULT; + } + foreach ($translation as $field_name => $items) { + if (!$items->isEmpty()) { + foreach ($items as $delta => $item) { + if (isset($data[$field_name][$langcode]) && !is_array($data[$field_name][$langcode])) { + $data[$field_name][$langcode] = array($data[$field_name][$langcode]); + } + // If the field item needs to be prepare the cache data, call + // the corresponding method, otherwise use the values as cache + // data. + if ($item instanceof PrepareCacheInterface) { + $data[$field_name][$langcode][$delta] = $item->getCacheData(); + } + else { + $data[$field_name][$langcode][$delta] = $item->getValue(); + } + } + } + } + } + return $data; + } + } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php index 59f596c..b298757 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Field\PrepareCacheInterface; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\TranslatableInterface; @@ -26,7 +27,7 @@ * @see \Drupal\Core\TypedData\TypedDataManager * @see \Drupal\Core\Field\FieldItemListInterface */ -interface ContentEntityInterface extends EntityInterface, RevisionableInterface, TranslatableInterface, ComplexDataInterface { +interface ContentEntityInterface extends EntityInterface, RevisionableInterface, TranslatableInterface, ComplexDataInterface, PrepareCacheInterface { /** * Marks the translation identified by the given language code as existing. diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index 3cdf898..a820dcd 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -143,18 +143,7 @@ public function loadMultiple(array $ids = NULL) { } } - // 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; + return $this->ensureOrder($passed_ids, $entities); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php index 5416935..2fa2236 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php @@ -160,17 +160,19 @@ protected function invokeHook($hook, EntityInterface $entity) { * Associative array of query results, keyed on the entity ID. */ protected function postLoad(array &$queried_entities) { - $entity_class = $this->entityType->getClass(); - $entity_class::postLoad($this, $queried_entities); - // Call hook_entity_load(). - foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) { - $function = $module . '_entity_load'; - $function($queried_entities, $this->entityTypeId); - } - // Call hook_TYPE_load(). - foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) { - $function = $module . '_' . $this->entityTypeId . '_load'; - $function($queried_entities); + if (!empty($queried_entities)) { + $entity_class = $this->entityType->getClass(); + $entity_class::postLoad($this, $queried_entities); + // Call hook_entity_load(). + foreach ($this->moduleHandler()->getImplementations('entity_load') as $module) { + $function = $module . '_entity_load'; + $function($queried_entities, $this->entityTypeId); + } + // Call hook_TYPE_load(). + foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load') as $module) { + $function = $module . '_' . $this->entityTypeId . '_load'; + $function($queried_entities); + } } } @@ -200,4 +202,29 @@ public function loadByProperties(array $values = array()) { return $result ? $this->loadMultiple($result) : array(); } + /** + * Ensures that entities are returned in the requested order. + * + * @param array|false $ordered_ids + * Array with the originally requested IDs or FALSE. + * @param \Drupal\Core\Entity\EntityInterface $unordered_entities + * List of entities for the requested IDs in unknown order. + * + * @return \Drupal\Core\Entity\EntityInterface + * Array with the correctly ordered entities. + */ + protected function ensureOrder($ordered_ids, $unordered_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 ($ordered_ids) { + // Remove any invalid ids from the array. + $ordered_entities = array_intersect_key($ordered_ids, $unordered_entities); + foreach ($unordered_entities as $entity) { + $ordered_entities[$entity->id()] = $entity; + } + return $ordered_entities; + } + return $unordered_entities; + } + } diff --git a/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php index 222145f..5e532c2 100644 --- a/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/FieldableDatabaseStorageController.php @@ -7,12 +7,11 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Field\PrepareCacheInterface; 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\FieldConfigInterface; @@ -61,11 +60,11 @@ class FieldableDatabaseStorageController extends FieldableEntityStorageControlle protected $revisionDataTable; /** - * Whether this entity type should use the static cache. + * Holds statically cached entities. * - * @var boolean + * @var array */ - protected $cache; + protected $entities = array(); /** * Active database connection. @@ -82,13 +81,35 @@ class FieldableDatabaseStorageController extends FieldableEntityStorageControlle protected $fieldInfo; /** + * Cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** + * The entity bundle key. + * + * @var string|bool + */ + protected $bundleKey = FALSE; + + /** + * Name of the entity class. + * + * @var string + */ + protected $entityClass; + + /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, $container->get('database'), - $container->get('field.info') + $container->get('field.info'), + $container->get('cache.entity') ); } @@ -101,12 +122,15 @@ public static function createInstance(ContainerInterface $container, EntityTypeI * The database connection to be used. * @param \Drupal\field\FieldInfo $field_info * The field info service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. */ - public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info) { + public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info, CacheBackendInterface $cache) { parent::__construct($entity_type); $this->database = $database; $this->fieldInfo = $field_info; + $this->cacheBackend = $cache; // Check if the entity type supports IDs. if ($this->entityType->hasKey('id')) { @@ -137,60 +161,241 @@ public function __construct(EntityTypeInterface $entity_type, Connection $databa * {@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)); - } + // static caching. This will remove ID's that were loaded from $ids. + $entities_from_static_cache = $this->getFromStaticCache($ids); + + // Load remaining entities either from the persistent cache or storage. + $entities = $this->doLoadMultiple($ids); + $this->invokeLoadUncachedHook($entities); + $this->setStaticCache($entities); + + $entities += $entities_from_static_cache; + return $this->ensureOrder($passed_ids, $entities); + } + + /** + * Loads entities from persistent cache or storage. + * + * @param array $ids + * List of entity IDs to load or NULL to load all. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of loaded entities. + */ + protected function doLoadMultiple(array $ids = NULL) { + // There is nothing to do if $ids is an empty array. + if ($ids === array()) { + return array(); } - // 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. + // Attempt to load entities from the persistent cache. This will remove ID's + // 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 entity cache. + */ + protected function getFromStorage(array $ids = NULL) { + $entities = array(); + if ($ids === NULL || $ids) { // Build and execute the query. $query_result = $this->buildQuery($ids)->execute(); - $queried_entities = $query_result->fetchAllAssoc($this->idKey); + $records = $query_result->fetchAllAssoc($this->idKey); + + // Map the loaded records into entity objects and according fields. + if ($records) { + $entities = $this->mapFromStorageRecords($records); + + // Attach field values. + if ($this->entityType->isFieldable()) { + $this->loadFieldItems($entities); + } + } } // Pass all entities loaded from the database through $this->postLoad(), - // 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->postLoad($queried_entities); - $entities += $queried_entities; + // to invoke the load hooks and postLoad() method on the entity class. + $this->postLoad($entities); + + return $entities; + } + + /** + * Gets entities from the static cache. + * + * @param array &$ids + * If not empty, return entities that match these IDs. ID's that were found + * will be removed from the list. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of entities from the entity cache. + */ + protected function getFromStaticCache(&$ids) { + $entities = array(); + + if ($this->entityType->isStaticallyCacheable() && $ids) { + $entities = array(); + // Load any available entities from the internal cache. + if (!empty($this->entities)) { + foreach ($ids as $index => $id) { + if (isset($this->entities[$id])) { + $entities[$id] = $this->entities[$id]; + // Remove the ID from the list. + unset($ids[$index]); + } + } + } + } + return $entities; + } + + /** + * Stores entities in the static entity cache. + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * Entities to store in the cache. + */ + protected function setStaticCache(array $entities) { + if ($this->entityType->isStaticallyCacheable()) { + $this->entities += $entities; + } + } + + /** + * Gets entities from the persistent cache. + * + * @param array &$ids + * If not empty, return entities that match these IDs. ID's that were found + * will be removed from the list. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of entities from the entity cache. + */ + protected function getFromPersistentCache(&$ids) { + if (!$this->entityType->isFieldDataCacheable() || empty($ids)) { + return array(); } + $entities = array(); + // Build the list of cache entries to retrieve. + $cids = array(); + foreach ($ids as $id) { + $cids[] = $this->buildCacheId($id); + } + if ($cache = $this->cacheBackend->getMultiple($cids)) { + // Create entity objects based on the loaded values from the cache. + foreach ($ids as $index => $id) { + $cid = $this->buildCacheId($id); + if (isset($cache[$cid])) { + $values = $cache[$cid]->data['values']; + $translations = $cache[$cid]->data['translations']; + $bundle = $this->bundleKey ? $cache[$cid]->data['bundle'] : FALSE; + $entities[$id] = new $this->entityClass($values, $this->entityTypeId, $bundle, $translations); + unset($ids[$index]); + } + } + } + return $entities; + } + + /** + * Stores entities in the persistent entity cache. + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * Entities to store in the cache. + */ + protected function setPersistentCache($entities) { + if (!$this->entityType->isFieldDataCacheable()) { + return; + } + + foreach ($entities as $id => $entity) { + $data = array( + 'id' => $entity->id(), + 'bundle' => $entity->bundle(), + 'translations' => array_keys($entity->getTranslationLanguages()), + 'values' => $entity->getCacheData(), + ); + $this->cacheBackend->set($this->buildCacheId($id), $data, CacheBackendInterface::CACHE_PERMANENT, array($this->entityTypeId . '_values' => TRUE)); + } + } - if ($this->cache) { - // Add entities to the cache. - if (!empty($queried_entities)) { - $this->cacheSet($queried_entities); + /** + * Invokes hook_entity_load_uncached(). + * + * @param \Drupal\Core\Entity\EntityInterface[] $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); } } + } - // 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; + /** + * {@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->isFieldDataCacheable()) { + $this->cacheBackend->deleteMultiple($cids); + } + } + else { + $this->entities = array(); + if ($this->entityType->isFieldDataCacheable()) { + $this->cacheBackend->deleteTags(array($this->entityTypeId . '_values' => TRUE)); } - $entities = $passed_ids; } + } - return $entities; + /** + * Returns 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"; } /** @@ -321,15 +526,20 @@ protected function attachPropertyData(array &$entities) { 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); + if ($records = $query_result->fetchAllAssoc($this->idKey)) { + // Map the record to an entity. + $entities = $this->mapFromStorageRecords($records); - // Pass the loaded entities from the database through $this->postLoad(), - // 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->postLoad($queried_entities); + // Attach field values. + if ($this->entityType->isFieldable()) { + $this->loadFieldItems($entities); + } + + // Pass all entities loaded from the database through $this->postLoad(), + // to invoke the load hooks and postLoad() method on the entity class. + $this->postLoad($entities); + return reset($entities); } - return reset($queried_entities); } /** @@ -440,32 +650,6 @@ protected function buildQuery($ids, $revision_id = FALSE) { } /** - * 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. - */ - protected function postLoad(array &$queried_entities) { - // Map the loaded records into entity objects and according fields. - $queried_entities = $this->mapFromStorageRecords($queried_entities); - - // Attach field values. - if ($this->entityType->isFieldable()) { - $this->loadFieldItems($queried_entities); - } - - parent::postLoad($queried_entities); - } - - /** * Implements \Drupal\Core\Entity\EntityStorageControllerInterface::delete(). */ public function delete(array $entities) { @@ -772,9 +956,26 @@ public function getQueryServiceName() { } /** - * {@inheritdoc} + * Loads values of configurable fields for a group of entities. + * + * Loads all fields for each entity object in a group of a single entity type. + * The loaded field values are added directly to the entity objects. + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * An array of entities keyed by entity ID. */ - protected function doLoadFieldItems($entities, $age) { + protected function loadFieldItems(array $entities) { + if (empty($entities) || !$this->entityType->isFieldable()) { + return; + } + + $age = static::FIELD_LOAD_CURRENT; + foreach ($entities as $entity) { + if (!$entity->isDefaultRevision()) { + $age = static::FIELD_LOAD_REVISION; + break; + } + } $load_current = $age == static::FIELD_LOAD_CURRENT; // Collect entities ids, bundles and languages. @@ -841,9 +1042,14 @@ protected function doLoadFieldItems($entities, $age) { } /** - * {@inheritdoc} + * Saves values of configurable fields for an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param bool $update + * TRUE if the entity is being updated, FALSE if it is being inserted. */ - protected function doSaveFieldItems(EntityInterface $entity, $update) { + protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { $vid = $entity->getRevisionId(); $id = $entity->id(); $bundle = $entity->bundle(); @@ -926,9 +1132,12 @@ protected function doSaveFieldItems(EntityInterface $entity, $update) { } /** - * {@inheritdoc} + * Deletes values of configurable fields for all revisions of an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. */ - protected function doDeleteFieldItems(EntityInterface $entity) { + protected function deleteFieldItems(EntityInterface $entity) { foreach ($this->fieldInfo->getBundleInstances($entity->getEntityTypeId(), $entity->bundle()) as $instance) { $field = $instance->getField(); $table_name = static::_fieldTableName($field); @@ -943,9 +1152,12 @@ protected function doDeleteFieldItems(EntityInterface $entity) { } /** - * {@inheritdoc} + * Deletes values of configurable fields for a single revision of an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. It must have a revision ID. */ - protected function doDeleteFieldItemsRevision(EntityInterface $entity) { + protected function deleteFieldItemsRevision(EntityInterface $entity) { $vid = $entity->getRevisionId(); if (isset($vid)) { foreach ($this->fieldInfo->getBundleInstances($entity->getEntityTypeId(), $entity->bundle()) as $instance) { diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php index ec468b7..7580e32 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php @@ -8,10 +8,8 @@ namespace Drupal\Core\Entity; use Drupal\Component\Utility\String; -use Drupal\Core\Field\PrepareCacheInterface; use Drupal\field\FieldConfigInterface; use Drupal\field\FieldInstanceConfigInterface; -use Drupal\Core\Field\ConfigFieldItemListInterface; use Symfony\Component\DependencyInjection\ContainerInterface; abstract class FieldableEntityStorageControllerBase extends EntityStorageControllerBase implements FieldableEntityStorageControllerInterface { @@ -93,206 +91,6 @@ public function create(array $values = array()) { } /** - * Loads values of configurable fields for a group of entities. - * - * Loads all fields for each entity object in a group of a single entity type. - * The loaded field values are added directly to the entity objects. - * - * This method is a wrapper that handles the field data cache. Subclasses - * need to implement the doLoadFieldItems() method with the actual storage - * logic. - * - * @param array $entities - * An array of entities keyed by entity ID. - */ - protected function loadFieldItems(array $entities) { - if (empty($entities)) { - return; - } - - $age = static::FIELD_LOAD_CURRENT; - foreach ($entities as $entity) { - if (!$entity->isDefaultRevision()) { - $age = static::FIELD_LOAD_REVISION; - break; - } - } - - // Only the most current revision of non-deleted fields for cacheable entity - // types can be cached. - $load_current = $age == static::FIELD_LOAD_CURRENT; - $use_cache = $load_current && $this->entityType->isFieldDataCacheable(); - - // Assume all entities will need to be queried. Entities found in the cache - // will be removed from the list. - $queried_entities = $entities; - - // Fetch available entities from cache, if applicable. - if ($use_cache) { - // Build the list of cache entries to retrieve. - $cids = array(); - foreach ($entities as $id => $entity) { - $cids[] = "field:{$this->entityTypeId}:$id"; - } - $cache = \Drupal::cache('field')->getMultiple($cids); - // Put the cached field values back into the entities and remove them from - // the list of entities to query. - foreach ($entities as $id => $entity) { - $cid = "field:{$this->entityTypeId}:$id"; - if (isset($cache[$cid])) { - unset($queried_entities[$id]); - foreach ($cache[$cid]->data as $langcode => $values) { - $translation = $entity->getTranslation($langcode); - // We do not need to worry about field translatability here, the - // translation object will manage that automatically. - foreach ($values as $field_name => $items) { - $translation->$field_name = $items; - } - } - } - } - } - - // Fetch other entities from their storage location. - if ($queried_entities) { - // Let the storage controller actually load the values. - $this->doLoadFieldItems($queried_entities, $age); - - // Build cache data. - // @todo: Improve this logic to avoid instantiating field objects once - // the field logic is improved to not do that anyway. - if ($use_cache) { - foreach ($queried_entities as $id => $entity) { - $data = array(); - foreach ($entity->getTranslationLanguages() as $langcode => $language) { - $translation = $entity->getTranslation($langcode); - foreach ($translation as $field_name => $items) { - if ($items instanceof ConfigFieldItemListInterface && !$items->isEmpty()) { - foreach ($items as $delta => $item) { - // If the field item needs to prepare the cache data, call the - // corresponding method, otherwise use the values as cache - // data. - if ($item instanceof PrepareCacheInterface) { - $data[$langcode][$field_name][$delta] = $item->getCacheData(); - } - else { - $data[$langcode][$field_name][$delta] = $item->getValue(); - } - } - } - } - } - $cid = "field:{$this->entityTypeId}:$id"; - \Drupal::cache('field')->set($cid, $data); - } - } - } - } - - /** - * Saves values of configurable fields for an entity. - * - * This method is a wrapper that handles the field data cache. Subclasses - * need to implement the doSaveFieldItems() method with the actual storage - * logic. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param bool $update - * TRUE if the entity is being updated, FALSE if it is being inserted. - */ - protected function saveFieldItems(EntityInterface $entity, $update = TRUE) { - $this->doSaveFieldItems($entity, $update); - - if ($update) { - $entity_type = $entity->getEntityType(); - if ($entity_type->isFieldDataCacheable()) { - \Drupal::cache('field')->delete('field:' . $entity->getEntityTypeId() . ':' . $entity->id()); - } - } - } - - /** - * Deletes values of configurable fields for all revisions of an entity. - * - * This method is a wrapper that handles the field data cache. Subclasses - * need to implement the doDeleteFieldItems() method with the actual storage - * logic. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - */ - protected function deleteFieldItems(EntityInterface $entity) { - $this->doDeleteFieldItems($entity); - - $entity_type = $entity->getEntityType(); - if ($entity_type->isFieldDataCacheable()) { - \Drupal::cache('field')->delete('field:' . $entity->getEntityTypeId() . ':' . $entity->id()); - } - } - - /** - * Deletes values of configurable fields for a single revision of an entity. - * - * This method is a wrapper that handles the field data cache. Subclasses - * need to implement the doDeleteFieldItemsRevision() method with the actual - * storage logic. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. It must have a revision ID attribute. - */ - protected function deleteFieldItemsRevision(EntityInterface $entity) { - $this->doDeleteFieldItemsRevision($entity); - } - - /** - * Loads values of configurable fields for a group of entities. - * - * This is the method that holds the actual storage logic. - * - * @param array $entities - * An array of entities keyed by entity ID. - * @param int $age - * EntityStorageControllerInterface::FIELD_LOAD_CURRENT to load the most - * recent revision for all fields, or - * EntityStorageControllerInterface::FIELD_LOAD_REVISION to load the version - * indicated by each entity. - */ - abstract protected function doLoadFieldItems($entities, $age); - - /** - * Saves values of configurable fields for an entity. - * - * This is the method that holds the actual storage logic. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param bool $update - * TRUE if the entity is being updated, FALSE if it is being inserted. - */ - abstract protected function doSaveFieldItems(EntityInterface $entity, $update); - - /** - * Deletes values of configurable fields for all revisions of an entity. - * - * This is the method that holds the actual storage logic. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - */ - abstract protected function doDeleteFieldItems(EntityInterface $entity); - - /** - * Deletes values of configurable fields for a single revision of an entity. - * - * This is the method that holds the actual storage logic. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - */ - abstract protected function doDeleteFieldItemsRevision(EntityInterface $entity); - - /** * {@inheritdoc} */ public function onFieldCreate(FieldConfigInterface $field) { } diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 317a885..c122ef9 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -421,9 +421,9 @@ function book_children($book_link) { } /** - * Implements hook_node_load(). + * Implements hook_node_load_uncached(). */ -function book_node_load($nodes) { +function book_node_load_uncached($nodes) { $result = db_query("SELECT * FROM {book} WHERE nid IN (:nids)", array(':nids' => array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC)); foreach ($result as $record) { $nodes[$record['nid']]->book = $record; diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 0070378..26a56e4 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -868,11 +868,11 @@ function comment_translation_configuration_element_submit($form, &$form_state) { } /** - * Implements hook_entity_load(). + * Implements hook_entity_load_uncached(). * * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions() */ -function comment_entity_load($entities, $entity_type) { +function comment_entity_load_uncached($entities, $entity_type) { if (!\Drupal::service('comment.manager')->getFields($entity_type)) { // Do not query database when entity has no comment fields. return; diff --git a/core/modules/comment/lib/Drupal/comment/CommentStorageController.php b/core/modules/comment/lib/Drupal/comment/CommentStorageController.php index 4434b6e..c9d634c 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentStorageController.php +++ b/core/modules/comment/lib/Drupal/comment/CommentStorageController.php @@ -35,12 +35,12 @@ protected function buildQuery($ids, $revision_id = FALSE) { /** * {@inheritdoc} */ - protected function postLoad(array &$queried_entities) { + protected function mapFromStorageRecords(array $records) { // Prepare standard comment fields. - foreach ($queried_entities as &$record) { + foreach ($records as $record) { $record->name = $record->uid ? $record->registered_name : $record->name; } - parent::postLoad($queried_entities); + return parent::mapFromStorageRecords($records); } /** diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php index 9428dc0..338ed3e 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachOtherTest.php @@ -179,22 +179,22 @@ function testEntityDisplayViewMultiple() { function testFieldAttachCache() { // Initialize random values and a test entity. $entity_init = entity_create('entity_test', array('type' => $this->instance->bundle)); - $langcode = Language::LANGCODE_NOT_SPECIFIED; + $langcode = Language::LANGCODE_DEFAULT; $values = $this->_generateTestFieldValues($this->field->getCardinality()); // Non-cacheable entity type. $entity_type = 'entity_test'; - $cid = "field:$entity_type:" . $entity_init->id(); + $cid = "values:$entity_type:" . $entity_init->id(); // Check that no initial cache entry is present. - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Non-cached: no initial cache entry'); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Non-cached: no initial cache entry'); // Save, and check that no cache entry is present. $entity = clone($entity_init); $entity->{$this->field_name}->setValue($values); $entity = $this->entitySaveReload($entity); - $cid = "field:$entity_type:" . $entity->id(); - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Non-cached: no cache entry on insert and load'); + $cid = "values:$entity_type:" . $entity->id(); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Non-cached: no cache entry on insert and load'); // Cacheable entity type. $entity_type = 'entity_test_cache'; @@ -206,22 +206,22 @@ function testFieldAttachCache() { )); // Check that no initial cache entry is present. - $cid = "field:$entity_type:" . $entity->id(); - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Cached: no initial cache entry'); + $cid = "values:$entity_type:" . $entity->id(); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no initial cache entry'); // Save, and check that no cache entry is present. $entity = clone($entity_init); $entity->{$this->field_name_2} = $values; $entity->save(); - $cid = "field:$entity_type:" . $entity->id(); + $cid = "values:$entity_type:" . $entity->id(); - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Cached: no cache entry on insert'); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on insert'); // Load, and check that a cache entry is present with the expected values. $controller = $this->container->get('entity.manager')->getStorageController($entity->getEntityTypeId()); $controller->resetCache(); $controller->load($entity->id()); - $cache = \Drupal::cache('field')->get($cid); - $this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load'); + $cache = \Drupal::cache('entity')->get($cid); + $this->assertEqual($cache->data['values'][$this->field_name_2][$langcode], $values, 'Cached: correct cache entry on load'); // Update with different values, and check that the cache entry is wiped. $values = $this->_generateTestFieldValues($this->field_2->getCardinality()); @@ -231,13 +231,13 @@ function testFieldAttachCache() { )); $entity->{$this->field_name_2} = $values; $entity->save(); - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Cached: no cache entry on update'); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on update'); // Load, and check that a cache entry is present with the expected values. $controller->resetCache(); $controller->load($entity->id()); - $cache = \Drupal::cache('field')->get($cid); - $this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load'); + $cache = \Drupal::cache('entity')->get($cid); + $this->assertEqual($cache->data['values'][$this->field_name_2][$langcode], $values, 'Cached: correct cache entry on load'); // Create a new revision, and check that the cache entry is wiped. $entity = entity_create($entity_type, array( @@ -248,17 +248,17 @@ function testFieldAttachCache() { $entity->{$this->field_name_2} = $values; $entity->setNewRevision(); $entity->save(); - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Cached: no cache entry on new revision creation'); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry on new revision creation'); // Load, and check that a cache entry is present with the expected values. $controller->resetCache(); $controller->load($entity->id()); - $cache = \Drupal::cache('field')->get($cid); - $this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load'); + $cache = \Drupal::cache('entity')->get($cid); + $this->assertEqual($cache->data['values'][$this->field_name_2][$langcode], $values, 'Cached: correct cache entry on load'); // Delete, and check that the cache entry is wiped. $entity->delete(); - $this->assertFalse(\Drupal::cache('field')->get($cid), 'Cached: no cache entry after delete'); + $this->assertFalse(\Drupal::cache('entity')->get($cid), 'Cached: no cache entry after delete'); } /** diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php index 6321c1b..bc137a5 100644 --- a/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php @@ -83,6 +83,7 @@ function testDisableFilterModule() { 'filters[filter_test_replace][status]' => 1, ); $this->drupalPostForm('admin/config/content/formats/manage/' . $format_id, $edit, t('Save configuration')); + \Drupal::entityManager()->getStorageController('node')->resetCache(array($node->id())); // Verify that filter_test_replace filter replaced the content. $this->drupalGet('node/' . $node->id()); diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module index 128b31a..81259f3 100644 --- a/core/modules/node/tests/modules/node_access_test/node_access_test.module +++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module @@ -107,9 +107,9 @@ function node_access_test_form_node_form_alter(&$form, $form_state) { } /** - * Implements hook_node_load(). + * Implements hook_ENTITY_TYPE_load_uncached(). */ -function node_access_test_node_load($nodes) { +function node_access_test_node_load_uncached($nodes) { $result = db_query('SELECT nid, private FROM {node_access_test} WHERE nid IN(:nids)', array(':nids' => array_keys($nodes))); foreach ($result as $record) { $nodes[$record->nid]->private = $record->private; diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php index cdd5946..b22979a 100644 --- a/core/modules/system/entity.api.php +++ b/core/modules/system/entity.api.php @@ -266,18 +266,55 @@ function hook_entity_create(\Drupal\Core\Entity\EntityInterface $entity) { * This is a generic load hook called for all entity types loaded via the * entity API. * - * @param array $entities + * The hook will not be invoked for entities loaded from the cache. Use + * hook_entity_load_uncached() if necessary. + * + * @param \Drupal\Core\Entity\EntityInterface[] $entities * The entities keyed by entity ID. * @param string $entity_type_id * The type of entities being loaded (i.e. node, user, comment). */ -function hook_entity_load($entities, $entity_type_id) { +function hook_entity_load(array $entities, $entity_type_id) { foreach ($entities as $entity) { $entity->foo = mymodule_add_something($entity); } } /** + * Act on entities when loaded from the storage or cache. + * + * Only use this hook if the result must not be cached. + * + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * The entities keyed by entity ID. + * @param string $entity_type + * The type of entities being loaded (i.e. node, user, comment). + * + * @see hook_entity_load_uncached() + */ +function hook_entity_load_uncached(array $entities, $entity_type) { + foreach ($entities as $entity) { + $entity->foo = mymodule_add_something_uncached($entity); + } +} + +/** + * Act on entities of the given type when loaded from the storage or cache. + * + * Only use this hook if the result must not be cached. + * + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * The entities keyed by entity ID. + * + * @see hook_entity_load_uncached() + */ +function hook_ENTITY_TYPE_load_uncached(array $entities) { + foreach ($entities as $entity) { + $entity->foo = mymodule_add_something_uncached($entity); + } +} + +/** * Act on an entity before it is about to be created or updated. * * @param \Drupal\Core\Entity\EntityInterface $entity diff --git a/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php index 463b486..44f69b0 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php @@ -83,11 +83,7 @@ function testUpdateAccess() { $this->assertResponse(200); // Access the update page as user 1. - $user1 = user_load(1); - $user1->pass_raw = user_password(); - $user1->pass = $this->container->get('password')->hash(trim($user1->pass_raw)); - db_query("UPDATE {users} SET pass = :pass WHERE uid = :uid", array(':pass' => $user1->getPassword(), ':uid' => $user1->id())); - $this->drupalLogin($user1); + $this->drupalLogin($this->root_user); $this->drupalGet($this->update_url, array('external' => TRUE)); $this->assertResponse(200); } diff --git a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php index f325b85..6d50f34 100644 --- a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php +++ b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php @@ -126,38 +126,36 @@ function testProcessedCache() { // Load the entity and check that the field cache contains the expected // data. $entity = entity_load($entity_type, $entity->id()); - $cache = \Drupal::cache('field')->get("field:$entity_type:" . $entity->id()); - $this->assertEqual($cache->data, array( - Language::LANGCODE_NOT_SPECIFIED => array( - 'summary_field' => array( - 0 => array( - 'value' => $value, - 'summary' => $summary, - 'format' => 'plain_text', - 'processed' => $value, - 'summary_processed' => $summary, - ), + $cache = \Drupal::cache('entity')->get("values:$entity_type:" . $entity->id()); + $this->assertEqual($cache->data['values']['summary_field'], array( + Language::LANGCODE_DEFAULT => array( + 0 => array( + 'value' => $value, + 'summary' => $summary, + 'format' => 'plain_text', + 'processed' => $value, + 'summary_processed' => $summary, ), ), )); // Inject fake processed values into the cache to make sure that these are // used as-is and not re-calculated when the entity is loaded. - $data = array( - Language::LANGCODE_NOT_SPECIFIED => array( - 'summary_field' => array( - 0 => array( - 'value' => $value, - 'summary' => $summary, - 'format' => 'plain_text', - 'processed' => 'Cached processed value', - 'summary_processed' => 'Cached summary processed value', - ), + $data = $cache->data; + $data['values']['summary_field'] = array( + Language::LANGCODE_DEFAULT => array( + 0 => array( + 'value' => $value, + 'summary' => $summary, + 'format' => 'plain_text', + 'processed' => 'Cached processed value', + 'summary_processed' => 'Cached summary processed value', ), ), ); - \Drupal::cache('field')->set("field:$entity_type:" . $entity->id(), $data); - $entity = entity_load($entity_type, $entity->id(), TRUE); + \Drupal::entityManager()->getStorageController($entity_type)->resetCache(); + \Drupal::cache('entity')->set("values:$entity_type:" . $entity->id(), $data); + $entity = entity_load($entity_type, $entity->id()); $this->assertEqual($entity->summary_field->processed, 'Cached processed value'); $this->assertEqual($entity->summary_field->summary_processed, 'Cached summary processed value'); diff --git a/core/modules/user/lib/Drupal/user/UserStorageController.php b/core/modules/user/lib/Drupal/user/UserStorageController.php index 0694019..6b90784 100644 --- a/core/modules/user/lib/Drupal/user/UserStorageController.php +++ b/core/modules/user/lib/Drupal/user/UserStorageController.php @@ -8,6 +8,7 @@ namespace Drupal\user; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Password\PasswordInterface; @@ -48,15 +49,15 @@ class UserStorageController extends FieldableDatabaseStorageController implement * The database connection to be used. * @param \Drupal\field\FieldInfo $field_info * The field info service. - * @param \Drupal\Component\Uuid\UuidInterface $uuid_service - * The UUID Service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. * @param \Drupal\Core\Password\PasswordInterface $password * The password hashing service. * @param \Drupal\user\UserDataInterface $user_data * The user data service. */ - public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info, UuidInterface $uuid_service, PasswordInterface $password, UserDataInterface $user_data) { - parent::__construct($entity_type, $database, $field_info, $uuid_service); + public function __construct(EntityTypeInterface $entity_type, Connection $database, FieldInfo $field_info, CacheBackendInterface $cache, PasswordInterface $password, UserDataInterface $user_data) { + parent::__construct($entity_type, $database, $field_info, $cache); $this->password = $password; $this->userData = $user_data; @@ -70,7 +71,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $entity_type, $container->get('database'), $container->get('field.info'), - $container->get('uuid'), + $container->get('cache.entity'), $container->get('password'), $container->get('user.data') ); @@ -79,23 +80,20 @@ public static function createInstance(ContainerInterface $container, EntityTypeI /** * {@inheritdoc} */ - function postLoad(array &$queried_users) { - foreach ($queried_users as $key => $record) { - $queried_users[$key]->roles = array(); + function mapFromStorageRecords(array $records) { + foreach ($records as $key => $record) { + $records[$key]->roles = array(); if ($record->uid) { - $queried_users[$record->uid]->roles[] = DRUPAL_AUTHENTICATED_RID; + $records[$record->uid]->roles[] = DRUPAL_AUTHENTICATED_RID; } else { - $queried_users[$record->uid]->roles[] = DRUPAL_ANONYMOUS_RID; + $records[$record->uid]->roles[] = DRUPAL_ANONYMOUS_RID; } } // Add any additional roles from the database. - $this->addRoles($queried_users); - - // Call the default postLoad() method. This will add fields and call - // hook_user_load(). - parent::postLoad($queried_users); + $this->addRoles($records); + return parent::mapFromStorageRecords($records); } /** @@ -152,6 +150,8 @@ public function updateLastLoginTimestamp(UserInterface $account) { ->fields(array('login' => $account->getLastLoginTime())) ->condition('uid', $account->id()) ->execute(); + // Ensure that the entity cache is cleared. + $this->resetCache(array($account->id())); } }