diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 9716ccc..b1e2d7a 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -156,7 +156,14 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans foreach ($this->getEntityType()->getKeys() as $key => $field_name) { if (isset($this->values[$field_name])) { if (is_array($this->values[$field_name]) && isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) { - $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT]; + if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) { + if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) { + $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value']; + } + } + else { + $this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT]; + } } } } @@ -561,6 +568,11 @@ public function language() { $language = $this->languages[$this->activeLangcode]; } else { + // @todo Avoid this check by getting the language from the language + // manager directly. + if (!isset($this->languages[$this->defaultLangcode])) { + $this->languages += $this->languageManager()->getLanguages(Language::STATE_ALL); + } $language = $this->languages[$this->defaultLangcode]; } return $language; diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index cf7930d..27605c9 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Entity; use Drupal\Component\Utility\String; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; use Drupal\Core\Entity\Query\QueryInterface; @@ -119,13 +120,21 @@ class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements S protected $schemaHandler; /** + * Cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, $container->get('database'), - $container->get('entity.manager') + $container->get('entity.manager'), + $container->get('cache.entity') ); } @@ -138,12 +147,15 @@ public static function createInstance(ContainerInterface $container, EntityTypeI * The database connection to be used. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend to be used. */ - public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager) { + public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) { parent::__construct($entity_type); $this->database = $database; $this->entityManager = $entity_manager; + $this->cacheBackend = $cache; $this->fieldStorageDefinitions = $entity_manager->getBaseFieldDefinitions($entity_type->id()); // @todo Remove table names from the entity type definition in @@ -339,13 +351,158 @@ public function getTableMapping() { * {@inheritdoc} */ protected function doLoadMultiple(array $ids = NULL) { - // Build and execute the query. - $records = $this - ->buildQuery($ids) - ->execute() - ->fetchAllAssoc($this->idKey); + // Attempt to load entities from the persistent cache. This will remove IDs + // that were loaded from $ids. + $entities_from_cache = $this->getFromPersistentCache($ids); - return $this->mapFromStorageRecords($records); + // Load any remaining entities from the database. + $entities_from_storage = $this->getFromStorage($ids); + $this->setPersistentCache($entities_from_storage); + + return $entities_from_cache + $entities_from_storage; + } + + /** + * Gets entities from the storage. + * + * @param array|null $ids + * If not empty, return entities that match these IDs. Return all entities + * when NULL. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of entities from the storage. + */ + protected function getFromStorage(array $ids = NULL) { + $entities = array(); + + if ($ids === NULL || $ids) { + // Build and execute the query. + $query_result = $this->buildQuery($ids)->execute(); + $records = $query_result->fetchAllAssoc($this->idKey); + + // Map the loaded records into entity objects and according fields. + if ($records) { + $entities = $this->mapFromStorageRecords($records); + + // Call hook_entity_storage_load(). + foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) { + $function = $module . '_entity_storage_load'; + $function($entities, $this->entityTypeId); + } + // Call hook_TYPE_storage_load(). + foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) { + $function = $module . '_' . $this->entityTypeId . '_storage_load'; + $function($entities); + } + } + } + + return $entities; + } + + /** + * Gets entities from the persistent cache backend. + * + * @param array|null &$ids + * If not empty, return entities that match these IDs. IDs that were found + * will be removed from the list. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of entities from the persistent cache. + */ + protected function getFromPersistentCache(array &$ids = NULL) { + if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) { + return array(); + } + $entities = array(); + // Build the list of cache entries to retrieve. + $cids = array(); + foreach ($ids as $id) { + $cids[] = $this->buildCacheId($id); + } + if ($cache = $this->cacheBackend->getMultiple($cids)) { + // Get the entities that were found in the cache. + foreach ($ids as $index => $id) { + $cid = $this->buildCacheId($id); + if (isset($cache[$cid])) { + $entities[$id] = $cache[$cid]->data; + unset($ids[$index]); + } + } + } + return $entities; + } + + /** + * Stores entities in the persistent cache backend. + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * Entities to store in the cache. + */ + protected function setPersistentCache($entities) { + if (!$this->entityType->isPersistentlyCacheable()) { + return; + } + + foreach ($entities as $id => $entity) { + $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, array($this->entityTypeId . '_values' => TRUE, 'entity_field_info' => TRUE)); + } + } + + /** + * Invokes hook_entity_load_uncached(). + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * List of entities, keyed on the entity ID. + */ + protected function invokeLoadUncachedHook(array &$entities) { + if (!empty($entities)) { + // Call hook_entity_load_uncached(). + foreach ($this->moduleHandler()->getImplementations('entity_load_uncached') as $module) { + $function = $module . '_entity_load_uncached'; + $function($entities, $this->entityTypeId); + } + // Call hook_TYPE_load_uncached(). + foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load_uncached') as $module) { + $function = $module . '_' . $this->entityTypeId . '_load_uncached'; + $function($entities); + } + } + } + + /** + * {@inheritdoc} + */ + public function resetCache(array $ids = NULL) { + if ($ids) { + $cids = array(); + foreach ($ids as $id) { + unset($this->entities[$id]); + $cids[] = $this->buildCacheId($id); + } + if ($this->entityType->isPersistentlyCacheable()) { + $this->cacheBackend->deleteMultiple($cids); + } + } + else { + $this->entities = array(); + if ($this->entityType->isPersistentlyCacheable()) { + $this->cacheBackend->deleteTags(array($this->entityTypeId . '_values' => TRUE)); + } + } + } + + /** + * 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"; } /** @@ -936,9 +1093,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. @@ -1007,9 +1181,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(); @@ -1095,9 +1274,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->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $instance) { if (!($instance instanceof FieldInstanceConfigInterface)) { continue; @@ -1115,9 +1297,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->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $instance) { diff --git a/core/lib/Drupal/Core/Entity/ContentEntityInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php index 654fc5e..22fec88 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; diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 29c1de9..ee8bd9c 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -75,196 +75,6 @@ protected function doCreate(array $values) { } /** - * 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('entity')->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 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->getFieldDefinition() instanceof FieldInstanceConfigInterface && !$items->isEmpty()) { - $data[$langcode][$field_name] = $items->getValue(); - } - } - } - $cid = "field:{$this->entityTypeId}:$id"; - \Drupal::cache('entity')->set($cid, $data, Cache::PERMANENT, array('entity_field_info' => TRUE)); - } - } - } - } - - /** - * 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('entity')->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('entity')->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 - * EntityStorageInterface::FIELD_LOAD_CURRENT to load the most - * recent revision for all fields, or - * EntityStorageInterface::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/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 4382665..1c2a3b0 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -38,7 +38,7 @@ class EntityType implements EntityTypeInterface { * * @var bool */ - protected $field_cache; + protected $persistent_cache; /** * An array of entity keys. @@ -249,8 +249,8 @@ public function isRenderCacheable() { /** * {@inheritdoc} */ - public function isFieldDataCacheable() { - return isset($this->field_cache) ? $this->field_cache: TRUE; + public function isPersistentlyCacheable() { + return isset($this->persistent_cache) ? $this->persistent_cache: TRUE; } /** diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index a6e39aa..d92e00b 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -153,7 +153,7 @@ public function isRenderCacheable(); * * @return bool */ - public function isFieldDataCacheable(); + public function isPersistentlyCacheable(); /** * Sets the name of the entity type class. diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 1c44234..9ade502 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -786,11 +786,11 @@ function comment_form_field_ui_field_edit_form_alter(&$form, $form_state) { } /** - * Implements hook_entity_load(). + * Implements hook_entity_storage_load(). * * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions() */ -function comment_entity_load($entities, $entity_type) { +function comment_entity_storage_load($entities, $entity_type) { // Comments can only be attached to content entities, so skip others. if (!\Drupal::entityManager()->getDefinition($entity_type)->isSubclassOf('Drupal\Core\Entity\ContentEntityInterface')) { return; diff --git a/core/modules/comment/src/CommentStatistics.php b/core/modules/comment/src/CommentStatistics.php index fbbdff4..1d298c3 100644 --- a/core/modules/comment/src/CommentStatistics.php +++ b/core/modules/comment/src/CommentStatistics.php @@ -244,6 +244,10 @@ public function update(CommentInterface $comment) { ->condition('field_name', $comment->getFieldName()) ->execute(); } + + // Reset the cache of the commented entity so that when the entity is loaded + // the next time, the statistics will be loaded again. + $this->entityManager->getStorage($comment->getCommentedEntityTypeId())->resetCache(array($comment->getCommentedEntityId())); } } diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php index 2cffb5e..b00dae6 100644 --- a/core/modules/comment/src/CommentStorage.php +++ b/core/modules/comment/src/CommentStorage.php @@ -7,6 +7,7 @@ namespace Drupal\comment; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; @@ -38,11 +39,13 @@ class CommentStorage extends ContentEntityDatabaseStorage implements CommentStor * The database connection to be used. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. * @param \Drupal\comment\CommentStatisticsInterface $comment_statistics * The comment statistics service. */ - public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, CommentStatisticsInterface $comment_statistics) { - parent::__construct($entity_info, $database, $entity_manager); + public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, CommentStatisticsInterface $comment_statistics) { + parent::__construct($entity_info, $database, $entity_manager, $cache); $this->statistics = $comment_statistics; } @@ -54,6 +57,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $entity_info, $container->get('database'), $container->get('entity.manager'), + $container->get('cache.entity'), $container->get('comment.statistics') ); } diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 1023c17..629be6c 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -500,9 +500,9 @@ function content_translation_language_fallback_candidates_entity_view_alter(&$ca } /** - * Implements hook_entity_load(). + * Implements hook_entity_storage_load(). */ -function content_translation_entity_load(array $entities, $entity_type) { +function content_translation_entity_storage_load(array $entities, $entity_type) { $enabled_entities = array(); if (content_translation_enabled($entity_type)) { diff --git a/core/modules/field/src/Tests/FieldAttachOtherTest.php b/core/modules/field/src/Tests/FieldAttachOtherTest.php index 73a3b1e..2a23b49 100644 --- a/core/modules/field/src/Tests/FieldAttachOtherTest.php +++ b/core/modules/field/src/Tests/FieldAttachOtherTest.php @@ -177,17 +177,18 @@ function testEntityDisplayViewMultiple() { } /** - * Test field cache. + * Test entity cache. + * + * @todo Remove this when there is equal unit test coverage. */ - function testFieldAttachCache() { + function testEntityCache() { // Initialize random values and a test entity. $entity_init = entity_create('entity_test', array('type' => $this->instance->bundle)); - $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED; $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('entity')->get($cid), 'Non-cached: no initial cache entry'); @@ -196,7 +197,7 @@ function testFieldAttachCache() { $entity = clone($entity_init); $entity->{$this->field_name}->setValue($values); $entity = $this->entitySaveReload($entity); - $cid = "field:$entity_type:" . $entity->id(); + $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. @@ -208,22 +209,22 @@ function testFieldAttachCache() { )); // Check that no initial cache entry is present. - $cid = "field:$entity_type:" . $entity->id(); + $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('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')->getStorage($entity->getEntityTypeId()); $controller->resetCache(); - $controller->load($entity->id()); + $cached_entity = $controller->load($entity->id()); $cache = \Drupal::cache('entity')->get($cid); - $this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load'); + $this->assertEqual($cache->data, $cached_entity, '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()); @@ -233,9 +234,9 @@ function testFieldAttachCache() { // Load, and check that a cache entry is present with the expected values. $controller->resetCache(); - $controller->load($entity->id()); + $cached_entity = $controller->load($entity->id()); $cache = \Drupal::cache('entity')->get($cid); - $this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load'); + $this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load'); // Create a new revision, and check that the cache entry is wiped. $values = $this->_generateTestFieldValues($this->field_2->getCardinality()); @@ -246,9 +247,9 @@ function testFieldAttachCache() { // Load, and check that a cache entry is present with the expected values. $controller->resetCache(); - $controller->load($entity->id()); + $cached_entity = $controller->load($entity->id()); $cache = \Drupal::cache('entity')->get($cid); - $this->assertEqual($cache->data[$langcode][$this->field_name_2], $values, 'Cached: correct cache entry on load'); + $this->assertEqual($cache->data, $cached_entity, 'Cached: correct cache entry on load'); // Delete, and check that the cache entry is wiped. $entity->delete(); diff --git a/core/modules/file/file.module b/core/modules/file/file.module index e1f4b28..14e7322 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -1890,15 +1890,14 @@ function file_get_file_references(File $file, FieldDefinitionInterface $field = $file_fields[$entity_type_id][$bundle] = array(); // This contains the possible field names. foreach ($entity->getFieldDefinitions() as $field_name => $field_definition) { - $field_type = $field_definition->getType(); // If this is the first time this field type is seen, check // whether it references files. - if (!isset($field_columns[$field_type])) { - $field_columns[$field_type] = file_field_find_file_reference_column($field_definition); + if (!isset($field_columns[$field_definition->getType()])) { + $field_columns[$field_definition->getType()] = file_field_find_file_reference_column($field_definition); } // If the field type does reference files then record it. - if ($field_columns[$field_type]) { - $file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_type]; + if ($field_columns[$field_definition->getType()]) { + $file_fields[$entity_type_id][$bundle][$field_name] = $field_columns[$field_definition->getType()]; } } } diff --git a/core/modules/file/src/Tests/FilePrivateTest.php b/core/modules/file/src/Tests/FilePrivateTest.php index 3dce48e..cb6fec3 100644 --- a/core/modules/file/src/Tests/FilePrivateTest.php +++ b/core/modules/file/src/Tests/FilePrivateTest.php @@ -29,6 +29,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); + node_access_test_add_field(entity_load('node_type', 'article')); node_access_rebuild(); \Drupal::state()->set('node_access_test.private', TRUE); } diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index af041fc..5d0856c 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -329,9 +329,9 @@ function forum_node_predelete(EntityInterface $node) { } /** - * Implements hook_node_load(). + * Implements hook_node_storage_load(). */ -function forum_node_load($nodes) { +function forum_node_storage_load($nodes) { $node_vids = array(); foreach ($nodes as $node) { if (\Drupal::service('forum_manager')->checkNodeType($node)) { diff --git a/core/modules/forum/src/Tests/ForumNodeAccessTest.php b/core/modules/forum/src/Tests/ForumNodeAccessTest.php index 99f8b25..5e7ff26 100644 --- a/core/modules/forum/src/Tests/ForumNodeAccessTest.php +++ b/core/modules/forum/src/Tests/ForumNodeAccessTest.php @@ -32,6 +32,7 @@ public static function getInfo() { function setUp() { parent::setUp(); node_access_rebuild(); + node_access_test_add_field(entity_load('node_type', 'forum')); \Drupal::state()->set('node_access_test.private', TRUE); } @@ -54,7 +55,7 @@ function testForumNodeAccess() { $edit = array( 'title[0][value]' => $private_node_title, 'body[0][value]' => $this->randomName(200), - 'private' => TRUE, + 'private[0][value]' => TRUE, ); $this->drupalPostForm('node/add/forum', $edit, t('Save'), array('query' => array('forum_id' => 1))); $private_node = $this->drupalGetNodeByTitle($private_node_title); diff --git a/core/modules/node/src/Tests/NodeAccessBaseTableTest.php b/core/modules/node/src/Tests/NodeAccessBaseTableTest.php index a590f28..0fd562b 100644 --- a/core/modules/node/src/Tests/NodeAccessBaseTableTest.php +++ b/core/modules/node/src/Tests/NodeAccessBaseTableTest.php @@ -39,6 +39,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); + node_access_test_add_field(entity_load('node_type', 'article')); + node_access_rebuild(); \Drupal::state()->set('node_access_test.private', TRUE); } @@ -75,7 +77,7 @@ function testNodeAccessBasic() { 'title[0][value]' => t('@private_public Article created by @user', array('@private_public' => $type, '@user' => $this->webUser->getUsername())), ); if ($is_private) { - $edit['private'] = TRUE; + $edit['private[0][value]'] = TRUE; $edit['body[0][value]'] = 'private node'; $edit['field_tags'] = 'private'; } @@ -85,14 +87,13 @@ function testNodeAccessBasic() { } $this->drupalPostForm('node/add/article', $edit, t('Save')); - $nid = db_query('SELECT nid FROM {node_field_data} WHERE title = :title', array(':title' => $edit['title[0][value]']))->fetchField(); - $private_status = db_query('SELECT private FROM {node_access_test} where nid = :nid', array(':nid' => $nid))->fetchField(); - $this->assertTrue($is_private == $private_status, 'The private status of the node was properly set in the node_access_test table.'); + $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); + $this->assertEqual($is_private, (int)$node->private->value, 'The private status of the node was properly set in the node_access_test table.'); if ($is_private) { - $private_nodes[] = $nid; + $private_nodes[] = $node->id(); } - $titles[$nid] = $edit['title[0][value]']; - $this->nodesByUser[$this->webUser->id()][$nid] = $is_private; + $titles[$node->id()] = $edit['title[0][value]']; + $this->nodesByUser[$this->webUser->id()][$node->id()] = $is_private; } } $this->publicTid = db_query('SELECT tid FROM {taxonomy_term_data} WHERE name = :name', array(':name' => 'public'))->fetchField(); diff --git a/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php b/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php index 7af2816..0e0ac0e 100644 --- a/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php +++ b/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php @@ -53,6 +53,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); + node_access_test_add_field(entity_load('node_type', 'page')); + // Create the 'private' field, which allows the node to be marked as private // (restricted access) in a given translation. $field_private = entity_create('field_config', array( diff --git a/core/modules/node/src/Tests/NodeAccessLanguageTest.php b/core/modules/node/src/Tests/NodeAccessLanguageTest.php index 619fcac..f6f8294 100644 --- a/core/modules/node/src/Tests/NodeAccessLanguageTest.php +++ b/core/modules/node/src/Tests/NodeAccessLanguageTest.php @@ -33,6 +33,8 @@ public static function getInfo() { function setUp() { parent::setUp(); + node_access_test_add_field(entity_load('node_type', 'page')); + // After enabling a node access module, the access table has to be rebuild. node_access_rebuild(); diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.install b/core/modules/node/tests/modules/node_access_test/node_access_test.install deleted file mode 100644 index 6b3ef5d..0000000 --- a/core/modules/node/tests/modules/node_access_test/node_access_test.install +++ /dev/null @@ -1,42 +0,0 @@ - 'The base table for node_access_test.', - 'fields' => array( - 'nid' => array( - 'description' => 'The {node}.nid this record affects.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'private' => array( - 'description' => 'Boolean indicating whether the node is private (visible to administrator) or not (visible to non-administrators).', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - ), - 'indexes' => array( - 'nid' => array('nid'), - ), - 'primary key' => array('nid'), - 'foreign keys' => array( - 'versioned_node' => array( - 'table' => 'node', - 'columns' => array('nid' => 'nid'), - ), - ), - ); - - return $schema; -} \ No newline at end of file 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 13c2ec8..6f2fd26 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 @@ -19,8 +19,9 @@ * @see \Drupal\node\Tests\NodeAccessBaseTableTest */ -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\FieldDefinition; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldInstanceConfig; +use Drupal\node\NodeTypeInterface; use Drupal\node\NodeInterface; /** @@ -127,73 +128,32 @@ function node_access_test_permission() { } /** - * Implements hook_entity_base_field_info(). + * Adds the private field to a node type. + * + * @param \Drupal\node\NodeTypeInterface $type + * A node type entity. */ -function node_access_test_entity_base_field_info(EntityTypeInterface $entity_type) { - if ($entity_type->id() === 'node') { - $fields['private'] = FieldDefinition::create('boolean') - ->setLabel(t('Private')) - ->setComputed(TRUE); +function node_access_test_add_field(NodeTypeInterface $type) { + $field = FieldConfig::create(array( + 'name' => 'private', + 'entity_type' => 'node', + 'type' => 'integer', + )); + $field->save(); + $instance = FieldInstanceConfig::create(array( + 'field_name' => 'private', + 'entity_type' => 'node', + 'bundle' => $type->id(), + 'label' => 'Private', + )); + $instance->save(); - return $fields; - } -} - -/** - * Implements hook_form_BASE_FORM_ID_alter(). - */ -function node_access_test_form_node_form_alter(&$form, $form_state) { - // Only show this checkbox for NodeAccessBaseTableTestCase. - if (\Drupal::state()->get('node_access_test.private')) { - $node = $form_state['controller']->getEntity($form_state); - $form['private'] = array( - '#type' => 'checkbox', - '#title' => t('Private'), - '#description' => t('Check here if this content should be set private and only shown to privileged users.'), - '#default_value' => $node->private->value, - ); - } -} - -/** - * Implements hook_node_load(). - */ -function node_access_test_node_load($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; - } -} - -/** - * Implements hook_node_predelete(). - */ -function node_access_test_node_predelete(NodeInterface $node) { - db_delete('node_access_test')->condition('nid', $node->id())->execute(); -} - -/** - * Implements hook_node_insert(). - */ -function node_access_test_node_insert(NodeInterface $node) { - _node_access_test_node_write($node); -} - -/** - * Implements hook_nodeapi_update(). - */ -function node_access_test_node_update(NodeInterface $node) { - _node_access_test_node_write($node); -} - -/** - * Saves the private status of the node in the database. - */ -function _node_access_test_node_write(NodeInterface $node) { - db_merge('node_access_test') - ->key('nid', $node->id()) - ->fields(array('private' => (int) $node->private->value)) - ->execute(); + // Assign widget settings for the 'default' form mode. + entity_get_form_display('node', $type->id(), 'default') + ->setComponent('private', array( + 'type' => 'number', + )) + ->save(); } /** diff --git a/core/modules/rdf/rdf.module b/core/modules/rdf/rdf.module index 81335c1..a1e0e68 100644 --- a/core/modules/rdf/rdf.module +++ b/core/modules/rdf/rdf.module @@ -225,9 +225,9 @@ function rdf_entity_prepare_view($entity_type, array $entities, array $displays) } /** - * Implements hook_comment_load(). + * Implements hook_comment_storage_load(). */ -function rdf_comment_load($comments) { +function rdf_comment_storage_load($comments) { foreach ($comments as $comment) { // Pages with many comments can show poor performance. This information // isn't needed until rdf_preprocess_comment() is called, but set it here diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php index ddc302a..6d62c68 100644 --- a/core/modules/system/entity.api.php +++ b/core/modules/system/entity.api.php @@ -269,18 +269,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 + * hook_entity_storage_load() should be used to load additional data for + * content entities. + * + * @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 content entities when loaded from the storage. + * + * The results of this hook will 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() + */ +function hook_entity_storage_load(array $entities, $entity_type) { + foreach ($entities as $entity) { + $entity->foo = mymodule_add_something_uncached($entity); + } +} + +/** + * Act on content entities of a given type when loaded from the storage. + * + * The results of this hook will be cached if the entity type supports it. + * + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * The entities keyed by entity ID. + * + * @see hook_entity_storage_load() + */ +function hook_ENTITY_TYPE_storage_load(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/src/Tests/Update/UpdateScriptTest.php b/core/modules/system/src/Tests/Update/UpdateScriptTest.php index cd92191..b533d7c 100644 --- a/core/modules/system/src/Tests/Update/UpdateScriptTest.php +++ b/core/modules/system/src/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/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php index 01e6872..7b7efaf 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -32,7 +32,7 @@ * }, * base_table = "entity_test", * fieldable = TRUE, - * field_cache = FALSE, + * persistent_cache = FALSE, * entity_keys = { * "id" = "id", * "uuid" = "uuid", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php index 07d0e75..ab5fdab 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestCache.php @@ -22,7 +22,6 @@ * }, * base_table = "entity_test", * fieldable = TRUE, - * field_cache = TRUE, * entity_keys = { * "id" = "id", * "uuid" = "uuid", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php index e881b9a..e15d389 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestLabelCallback.php @@ -13,7 +13,7 @@ * @ContentEntityType( * id = "entity_test_label_callback", * label = @Translation("Entity test label callback"), - * field_cache = FALSE, + * persistent_cache = FALSE, * base_table = "entity_test", * label_callback = "entity_test_label_callback", * fieldable = TRUE, diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php index 6dc35d4..5ad3ba7 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoLabel.php @@ -13,7 +13,7 @@ * @ContentEntityType( * id = "entity_test_no_label", * label = @Translation("Entity Test without label"), - * field_cache = FALSE, + * persistent_cache = FALSE, * base_table = "entity_test", * entity_keys = { * "id" = "id", diff --git a/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php b/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php index c50eed2..ed5f58e 100644 --- a/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php +++ b/core/modules/tracker/src/Tests/TrackerNodeAccessTest.php @@ -34,6 +34,7 @@ public function setUp() { parent::setUp(); node_access_rebuild(); $this->drupalCreateContentType(array('type' => 'page')); + node_access_test_add_field(entity_load('node_type', 'page')); $this->container->get('comment.manager')->addDefaultField('node', 'page', 'comment', CommentItemInterface::OPEN); \Drupal::state()->set('node_access_test.private', TRUE); } diff --git a/core/modules/user/src/UserStorage.php b/core/modules/user/src/UserStorage.php index 44d323f..3a9d9f5 100644 --- a/core/modules/user/src/UserStorage.php +++ b/core/modules/user/src/UserStorage.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\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -32,13 +33,6 @@ class UserStorage extends ContentEntityDatabaseStorage implements UserStorageInt protected $password; /** - * Provides the user data service object. - * - * @var \Drupal\user\UserDataInterface - */ - protected $userData; - - /** * Constructs a new UserStorage object. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type @@ -47,16 +41,15 @@ class UserStorage extends ContentEntityDatabaseStorage implements UserStorageInt * The database connection to be used. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. + * @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, EntityManagerInterface $entity_manager, PasswordInterface $password, UserDataInterface $user_data) { - parent::__construct($entity_type, $database, $entity_manager); + public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, PasswordInterface $password) { + parent::__construct($entity_type, $database, $entity_manager, $cache); $this->password = $password; - $this->userData = $user_data; } /** @@ -67,8 +60,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $entity_type, $container->get('database'), $container->get('entity.manager'), - $container->get('password'), - $container->get('user.data') + $container->get('cache.entity'), + $container->get('password') ); } @@ -149,6 +142,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())); } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php index 0b8f6e2..a20cbc1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityDatabaseStorageTest.php @@ -7,13 +7,14 @@ namespace Drupal\Tests\Core\Entity; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\ContentEntityDatabaseStorage; use Drupal\Core\Field\FieldDefinition; use Drupal\Tests\UnitTestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; /** - * Tests the fieldable database storage. + * Tests the content entity database storage. * * @coversDefaultClass \Drupal\Core\Entity\ContentEntityDatabaseStorage * @@ -51,12 +52,47 @@ class ContentEntityDatabaseStorageTest extends UnitTestCase { protected $entityManager; /** + * The entity type ID. + * + * @var string + */ + protected $entityTypeId = 'entity_test'; + + /** + * The dependency injection container. + * + * @var \Symfony\Component\DependencyInjection\ContainerBuilder + */ + protected $container; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleHandler; + + /** + * The cache backend to use. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $cache; + + /** + * The database connection to use. + * + * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject + */ + protected $connection; + + /** * {@inheritdoc} */ public static function getInfo() { return array( - 'name' => 'Fieldable database storage', - 'description' => 'Tests the fieldable database storage enhancer for entities.', + 'name' => 'Content entity database storage', + 'description' => 'Tests the content entity database storage.', 'group' => 'Entity' ); } @@ -68,9 +104,17 @@ public function setUp() { $this->entityType = $this->getMock('Drupal\Core\Entity\ContentEntityTypeInterface'); $this->entityType->expects($this->any()) ->method('id') - ->will($this->returnValue('entity_test')); + ->will($this->returnValue($this->entityTypeId)); + + $this->container = new ContainerBuilder(); + \Drupal::setContainer($this->container); $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); } /** @@ -920,10 +964,8 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent public function testFieldSqlSchemaForEntityWithStringIdentifier() { $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); - $container = new ContainerBuilder(); - $container->set('plugin.manager.field.field_type', $field_type_manager); - $container->set('entity.manager', $this->entityManager); - \Drupal::setContainer($container); + $this->container->set('plugin.manager.field.field_type', $field_type_manager); + $this->container->set('entity.manager', $this->entityManager); $this->entityType->expects($this->any()) ->method('getKey') @@ -952,7 +994,7 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->method('getDefinition') ->with('test_entity') ->will($this->returnValue($this->entityType)); - $this->entityManager->expects($this->once()) + $this->entityManager->expects($this->any()) ->method('getBaseFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); @@ -994,13 +1036,10 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { */ public function testCreate() { $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); - $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - $container = new ContainerBuilder(); - $container->set('language_manager', $language_manager); - $container->set('entity.manager', $this->entityManager); - $container->set('module_handler', $module_handler); - \Drupal::setContainer($container); + $this->container->set('language_manager', $language_manager); + $this->container->set('entity.manager', $this->entityManager); + $this->container->set('module_handler', $this->moduleHandler); $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') ->disableOriginalConstructor() @@ -1008,6 +1047,9 @@ public function testCreate() { ->getMockForAbstractClass(); $this->entityType->expects($this->atLeastOnce()) + ->method('id') + ->will($this->returnValue($this->entityTypeId)); + $this->entityType->expects($this->atLeastOnce()) ->method('getClass') ->will($this->returnValue(get_class($entity))); $this->entityType->expects($this->atLeastOnce()) @@ -1044,15 +1086,161 @@ public function testCreate() { * Sets up the content entity database storage. */ protected function setUpEntityStorage() { - $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') - ->disableOriginalConstructor() - ->getMock(); $this->entityManager->expects($this->once()) ->method('getBaseFieldDefinitions') ->will($this->returnValue($this->fieldDefinitions)); - $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $connection, $this->entityManager); + $this->entityStorage = new ContentEntityDatabaseStorage($this->entityType, $this->connection, $this->entityManager, $this->cache); + } + + /** + * @covers ::doLoadMultiple + * @covers ::buildCacheId + * @covers ::getFromPersistentCache + */ + public function testLoadMultiplePersistentCached() { + $this->setUpModuleHandlerNoImplementations(); + + $key = 'values:' . $this->entityTypeId . ':1'; + $id = 1; + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + $this->entityType->expects($this->atLeastOnce()) + ->method('isPersistentlyCacheable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->atLeastOnce()) + ->method('id') + ->will($this->returnValue($this->entityTypeId)); + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($entity))); + + $this->cache->expects($this->once()) + ->method('getMultiple') + ->with(array($key)) + ->will($this->returnValue(array($key => (object) array( + 'data' => $entity, + )))); + $this->cache->expects($this->never()) + ->method('set'); + + $this->setUpEntityStorage(); + $entities = $this->entityStorage->loadMultiple(array($id)); + $this->assertEquals($entity, $entities[$id]); + } + + /** + * @covers ::doLoadMultiple + * @covers ::buildCacheId + * @covers ::getFromPersistentCache + * @covers ::setPersistentCache + */ + public function testLoadMultipleNoPersistentCache() { + $this->setUpModuleHandlerNoImplementations(); + + $id = 1; + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + $this->entityType->expects($this->any()) + ->method('isPersistentlyCacheable') + ->will($this->returnValue(FALSE)); + $this->entityType->expects($this->atLeastOnce()) + ->method('id') + ->will($this->returnValue($this->entityTypeId)); + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($entity))); + + // There should be no calls to the cache backend for an entity type without + // persistent caching. + $this->cache->expects($this->never()) + ->method('getMultiple'); + $this->cache->expects($this->never()) + ->method('set'); + + $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') + ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache)) + ->setMethods(array('getFromStorage')) + ->getMock(); + $entity_storage->expects($this->once()) + ->method('getFromStorage') + ->with(array($id)) + ->will($this->returnValue(array($id => $entity))); + + $entities = $entity_storage->loadMultiple(array($id)); + $this->assertEquals($entity, $entities[$id]); + + } + + /** + * @covers ::doLoadMultiple + * @covers ::buildCacheId + * @covers ::getFromPersistentCache + * @covers ::setPersistentCache + */ + public function testLoadMultiplePersistentCacheMiss() { + $this->setUpModuleHandlerNoImplementations(); + + $id = 1; + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->any()) + ->method('id') + ->will($this->returnValue($id)); + + $this->entityType->expects($this->any()) + ->method('isPersistentlyCacheable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->atLeastOnce()) + ->method('id') + ->will($this->returnValue($this->entityTypeId)); + $this->entityType->expects($this->atLeastOnce()) + ->method('getClass') + ->will($this->returnValue(get_class($entity))); + + // In case of a cache miss, the entity is loaded from the storage and then + // set in the cache. + $key = 'values:' . $this->entityTypeId . ':1'; + $this->cache->expects($this->once()) + ->method('getMultiple') + ->with(array($key)) + ->will($this->returnValue(array())); + $this->cache->expects($this->once()) + ->method('set') + ->with($key, $entity, CacheBackendInterface::CACHE_PERMANENT, array($this->entityTypeId . '_values' => TRUE, 'entity_field_info' => TRUE)); + + $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') + ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache)) + ->setMethods(array('getFromStorage')) + ->getMock(); + $entity_storage->expects($this->once()) + ->method('getFromStorage') + ->with(array($id)) + ->will($this->returnValue(array($id => $entity))); + + $entities = $entity_storage->loadMultiple(array($id)); + $this->assertEquals($entity, $entities[$id]); + + } + + /** + * Sets up the module handler with no implementations. + */ + protected function setUpModuleHandlerNoImplementations() { + $this->moduleHandler->expects($this->any()) + ->method('getImplementations') + ->will($this->returnValueMap(array( + array('entity_load', array()), + array($this->entityTypeId . '_load', array()) + ))); + + $this->container->set('module_handler', $this->moduleHandler); } }