diff -u b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php --- b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -8,6 +8,8 @@ namespace Drupal\Core\Entity; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -22,15 +24,34 @@ protected $bundleKey = FALSE; /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * Cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** * Constructs a ContentEntityStorageBase object. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type definition. + * @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) { + public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) { parent::__construct($entity_type); - $this->bundleKey = $this->entityType->getKey('bundle'); + $this->entityManager = $entity_manager; + $this->cacheBackend = $cache; } /** @@ -38,7 +59,9 @@ */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( - $entity_type + $entity_type, + $container->get('entity.manager'), + $container->get('cache.entity') ); } @@ -287,6 +310,48 @@ } /** + * Invokes hook_entity_storage_load(). + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * List of entities, keyed on the entity ID. + */ + protected function invokeStorageLoadHook(array &$entities) { + if (!empty($entities)) { + // 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); + } + } + } + + /** + * 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} */ protected function invokeHook($hook, EntityInterface $entity) { @@ -391,2 +456,115 @@ + /** + * Ensures integer entity IDs are valid. + * + * The identifier sanitization provided by this method has been introduced + * as Drupal used to rely on the database to facilitate this, which worked + * correctly with MySQL but led to errors with other DBMS such as PostgreSQL. + * + * @param array $ids + * The entity IDs to verify. + * @return array + * The sanitized list of entity IDs. + */ + protected function cleanIds(array $ids) { + $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId); + $id_definition = $definitions[$this->entityType->getKey('id')]; + if ($id_definition->getType() == 'integer') { + $ids = array_filter($ids, function ($id) { + return is_numeric($id) && $id == (int) $id; + }); + $ids = array_map('intval', $ids); + } + return $ids; + } + + /** + * Gets entities from the persistent cache backend. + * + * @param array|null &$ids + * If not empty, return entities that match these IDs. IDs that were found + * will be removed from the list. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of entities from the persistent cache. + */ + protected function getFromPersistentCache(array &$ids = NULL) { + if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) { + return array(); + } + $entities = array(); + // Build the list of cache entries to retrieve. + $cid_map = array(); + foreach ($ids as $id) { + $cid_map[$id] = $this->buildCacheId($id); + } + $cids = array_values($cid_map); + if ($cache = $this->cacheBackend->getMultiple($cids)) { + // Get the entities that were found in the cache. + foreach ($ids as $index => $id) { + $cid = $cid_map[$id]; + if (isset($cache[$cid])) { + $entities[$id] = $cache[$cid]->data; + unset($ids[$index]); + } + } + } + return $entities; + } + + /** + * Stores entities in the persistent cache backend. + * + * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities + * Entities to store in the cache. + */ + protected function setPersistentCache($entities) { + if (!$this->entityType->isPersistentlyCacheable()) { + return; + } + + $cache_tags = array( + $this->entityTypeId . '_values', + 'entity_field_info', + ); + foreach ($entities as $id => $entity) { + $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags); + } + } + + /** + * {@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()) { + Cache::invalidateTags(array($this->entityTypeId . '_values')); + } + } + } + + /** + * Builds the cache ID for the passed in entity ID. + * + * @param int $id + * Entity ID for which the cache ID should be built. + * + * @return string + * Cache ID that can be passed to the cache backend. + */ + protected function buildCacheId($id) { + return "values:{$this->entityTypeId}:$id"; + } + } diff -u b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php --- b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -8,7 +8,6 @@ namespace Drupal\Core\Entity\Sql; use Drupal\Component\Utility\SafeMarkup; -use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; @@ -22,7 +21,6 @@ use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\field\FieldStorageConfigInterface; @@ -110,13 +108,6 @@ protected $database; /** - * The entity manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityManager; - - /** * The entity type's storage schema object. * * @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface @@ -124,13 +115,6 @@ protected $storageSchema; /** - * Cache backend. - * - * @var \Drupal\Core\Cache\CacheBackendInterface - */ - protected $cacheBackend; - - /** * The language manager. * * @var \Drupal\Core\Language\LanguageManagerInterface @@ -176,10 +160,8 @@ * The language manager. */ public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) { - parent::__construct($entity_type); + parent::__construct($entity_type, $entity_manager, $cache); $this->database = $database; - $this->entityManager = $entity_manager; - $this->cacheBackend = $cache; $this->languageManager = $language_manager; $this->initTableLayout(); } @@ -414,7 +396,10 @@ $entities_from_cache = $this->getFromPersistentCache($ids); // Load any remaining entities from the database. - $entities_from_storage = $this->getFromStorage($ids); + if ($entities_from_storage = $this->getFromStorage($ids)) { + $this->invokeStorageLoadHook($entities_from_storage); + } + $this->setPersistentCache($entities_from_storage); return $entities_from_cache + $entities_from_storage; @@ -447,17 +432,6 @@ // 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); - } } } @@ -465,140 +439,6 @@ } /** - * Ensures integer entity IDs are valid. - * - * The identifier sanitization provided by this method has been introduced - * as Drupal used to rely on the database to facilitate this, which worked - * correctly with MySQL but led to errors with other DBMS such as PostgreSQL. - * - * @param array $ids - * The entity IDs to verify. - * @return array - * The sanitized list of entity IDs. - */ - protected function cleanIds(array $ids) { - $definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId); - $id_definition = $definitions[$this->entityType->getKey('id')]; - if ($id_definition->getType() == 'integer') { - $ids = array_filter($ids, function ($id) { - return is_numeric($id) && $id == (int) $id; - }); - $ids = array_map('intval', $ids); - } - return $ids; - } - - /** - * Gets entities from the persistent cache backend. - * - * @param array|null &$ids - * If not empty, return entities that match these IDs. IDs that were found - * will be removed from the list. - * - * @return \Drupal\Core\Entity\ContentEntityInterface[] - * Array of entities from the persistent cache. - */ - protected function getFromPersistentCache(array &$ids = NULL) { - if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) { - return array(); - } - $entities = array(); - // Build the list of cache entries to retrieve. - $cid_map = array(); - foreach ($ids as $id) { - $cid_map[$id] = $this->buildCacheId($id); - } - $cids = array_values($cid_map); - if ($cache = $this->cacheBackend->getMultiple($cids)) { - // Get the entities that were found in the cache. - foreach ($ids as $index => $id) { - $cid = $cid_map[$id]; - if (isset($cache[$cid])) { - $entities[$id] = $cache[$cid]->data; - unset($ids[$index]); - } - } - } - return $entities; - } - - /** - * Stores entities in the persistent cache backend. - * - * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities - * Entities to store in the cache. - */ - protected function setPersistentCache($entities) { - if (!$this->entityType->isPersistentlyCacheable()) { - return; - } - - $cache_tags = array( - $this->entityTypeId . '_values', - 'entity_field_info', - ); - foreach ($entities as $id => $entity) { - $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags); - } - } - - /** - * Invokes hook_entity_load_uncached(). - * - * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities - * List of entities, keyed on the entity ID. - */ - protected function invokeLoadUncachedHook(array &$entities) { - if (!empty($entities)) { - // Call hook_entity_load_uncached(). - foreach ($this->moduleHandler()->getImplementations('entity_load_uncached') as $module) { - $function = $module . '_entity_load_uncached'; - $function($entities, $this->entityTypeId); - } - // Call hook_TYPE_load_uncached(). - foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load_uncached') as $module) { - $function = $module . '_' . $this->entityTypeId . '_load_uncached'; - $function($entities); - } - } - } - - /** - * {@inheritdoc} - */ - public function resetCache(array $ids = NULL) { - if ($ids) { - $cids = array(); - foreach ($ids as $id) { - unset($this->entities[$id]); - $cids[] = $this->buildCacheId($id); - } - if ($this->entityType->isPersistentlyCacheable()) { - $this->cacheBackend->deleteMultiple($cids); - } - } - else { - $this->entities = array(); - if ($this->entityType->isPersistentlyCacheable()) { - Cache::invalidateTags(array($this->entityTypeId . '_values')); - } - } - } - - /** - * Builds the cache ID for the passed in entity ID. - * - * @param int $id - * Entity ID for which the cache ID should be built. - * - * @return string - * Cache ID that can be passed to the cache backend. - */ - protected function buildCacheId($id) { - return "values:{$this->entityTypeId}:$id"; - } - - /** * Maps from storage records to entity objects, and attaches fields. * * @param array $records only in patch2: unchanged: --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php @@ -1130,8 +1130,10 @@ public function testLoadMultipleNoPersistentCache() { $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage') ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager)) - ->setMethods(array('getFromStorage')) + ->setMethods(array('getFromStorage', 'invokeStorageLoadHook')) ->getMock(); + $entity_storage->method('invokeStorageLoadHook') + ->willReturn(NULL); $entity_storage->expects($this->once()) ->method('getFromStorage') ->with(array($id)) @@ -1180,8 +1182,10 @@ public function testLoadMultiplePersistentCacheMiss() { $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage') ->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager)) - ->setMethods(array('getFromStorage')) + ->setMethods(array('getFromStorage', 'invokeStorageLoadHook')) ->getMock(); + $entity_storage->method('invokeStorageLoadHook') + ->willReturn(NULL); $entity_storage->expects($this->once()) ->method('getFromStorage') ->with(array($id))