diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php index 3404957..602ac23 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php @@ -131,20 +131,6 @@ public static function createInstance(ContainerInterface $container, EntityTypeI } /** - * {@inheritdoc} - */ - public function loadRevision($revision_id) { - return NULL; - } - - /** - * {@inheritdoc} - */ - public function deleteRevision($revision_id) { - return NULL; - } - - /** * Returns the prefix used to create the configuration name. * * The prefix consists of the config prefix from the entity type plus a dot diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 1e198fe..7855924 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -124,6 +124,13 @@ protected $translationInitialize = FALSE; /** + * The original revision identifier. + * + * @var int + */ + protected $originalRevisionId; + + /** * Boolean indicating whether a new revision should be created on save. * * @var bool @@ -233,6 +240,10 @@ public function __construct(array $values, $entity_type, $bundle = FALSE, $trans } } } + + // Store the original revision identfier the entity has been loaded with to + // keep it safe from changes. + $this->originalRevisionId = $this->getRevisionId(); } /** @@ -338,6 +349,20 @@ public function getRevisionId() { /** * {@inheritdoc} */ + public function getOriginalRevisionId() { + return $this->originalRevisionId; + } + + /** + * {@inheritdoc} + */ + public function resetOriginalRevisionId() { + $this->originalRevisionId = $this->getRevisionId(); + } + + /** + * {@inheritdoc} + */ public function isTranslatable() { // Check that the bundle is translatable, the entity has a language defined // and if we have more than one language on the site. diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 5eb5c30..f5be3c0 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -26,6 +26,13 @@ protected $bundleKey = FALSE; /** + * Static cache of entity revisions, keyed by entity revision ID. + * + * @var array + */ + protected $entityRevisions = []; + + /** * The entity manager. * * @var \Drupal\Core\Entity\EntityManagerInterface @@ -220,7 +227,62 @@ public function finalizePurge(FieldStorageDefinitionInterface $storage_definitio /** * {@inheritdoc} */ + public function load($id) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = parent::load($id); + + // If there is already an entry in the static entity revisions cache we + // return a reference to the already loaded entity object. + // + // @todo find a better way to determine the default revision id without + // having to load the entity object from the storage, then check if there + // is already an entry in the static entity revisions cache for it and if + // not load the entity from the storage. + if ($this->entityType->isStaticallyCacheable() && $entity->getRevisionId()) { + // If the default revision is not present in the static entity revision + // cache we add it and return the entity. + if (!isset($this->entityRevisions[$entity->getRevisionId()])) { + $this->entityRevisions[$entity->getRevisionId()] = $entity; + } + return $this->entityRevisions[$entity->getRevisionId()]; + } + + return $entity; + } + + /** + * {@inheritdoc} + */ public function loadRevision($revision_id) { + if ($this->entityType->isStaticallyCacheable()) { + if (!isset($this->entityRevisions[$revision_id])) { + if ($revision = $this->getRevisionFromPersistentCache($revision_id)) { + $this->entityRevisions[$revision_id] = $revision; + } + else { + $revision = $this->doLoadRevision($revision_id); + $this->entityRevisions[$revision_id] = $revision; + if ($revision->isDefaultRevision()) { + $this->setPersistentCache([$revision->id() => $revision]); + } + } + } + return $this->entityRevisions[$revision_id]; + } + + return $this->doLoadRevision($revision_id); + } + + /** + * Load a specific entity revision. + * + * @param int|string $revision_id + * The revision id. + * + * @return \Drupal\Core\Entity\ContentEntityInterface|null + * The specified entity revision or NULL if not found. + */ + protected function doLoadRevision($revision_id) { $revision = $this->doLoadRevisionFieldItems($revision_id); if ($revision) { @@ -312,6 +374,15 @@ protected function doPostSave(EntityInterface $entity, $update) { if ($this->entityType->isRevisionable()) { $entity->setNewRevision(FALSE); } + + // Update the original revision id to the new value. + $entity->resetOriginalRevisionId(); + + // Set the static entity revision cache reference, which has been removed + // by the parent class call to ::resetCache. + if ($this->entityType->isStaticallyCacheable() && $entity->getOriginalRevisionId()) { + $this->entityRevisions[$entity->getOriginalRevisionId()] = $entity; + } } /** @@ -588,6 +659,29 @@ protected function getFromPersistentCache(array &$ids = NULL) { } /** + * Gets entity from the persistent cache backend by revision id. + * + * @param int|null $revision_id + * If not empty, return the entity that match these revision ID. + * + * @return \Drupal\Core\Entity\ContentEntityInterface|null + * The entity from the persistent cache or NULL if not present. + */ + protected function getRevisionFromPersistentCache($revision_id = NULL) { + $revision = NULL; + if (!$this->entityType->isPersistentlyCacheable() || empty($revision_id)) { + return $revision; + } + // Build the list of cache entries to retrieve. + $cid = $this->buildRevisionCacheId($revision_id); + if ($cache = $this->cacheBackend->get($cid)) { + // Get the entity that was found in the cache. + $revision = $cache->data; + } + return $revision; + } + + /** * Stores entities in the persistent cache backend. * * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities @@ -604,6 +698,9 @@ protected function setPersistentCache($entities) { ); foreach ($entities as $id => $entity) { $this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags); + if ($revision_id = $entity->getRevisionId()) { + $this->cacheBackend->set($this->buildRevisionCacheId($revision_id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags); + } } } @@ -612,24 +709,71 @@ protected function setPersistentCache($entities) { */ public function resetCache(array $ids = NULL) { if ($ids) { - $cids = array(); + $cids = []; + $revision_ids = []; foreach ($ids as $id) { + if (isset($this->entities[$id])) { + $original_revision_id = $this->entities[$id]->getOriginalRevisionId(); + $revision_id = $this->entities[$id]->getRevisionId(); + $revision_ids[$original_revision_id] = $original_revision_id; + $revision_ids[$revision_id] = $revision_id; + } unset($this->entities[$id]); $cids[] = $this->buildCacheId($id); } + $this->resetRevisionCache($revision_ids, TRUE); if ($this->entityType->isPersistentlyCacheable()) { $this->cacheBackend->deleteMultiple($cids); } } else { - $this->entities = array(); + $this->entities = []; + $this->entityRevisions = []; if ($this->entityType->isPersistentlyCacheable()) { - Cache::invalidateTags(array($this->entityTypeId . '_values')); + Cache::invalidateTags([$this->entityTypeId . '_values']); + } + } + } + + /** + * Resets the internal, static entity revision cache. + * + * @param $revision_ids + * The revision cache will be reset for the entities with the given + * revision ids. + */ + protected function resetRevisionCache(array $revision_ids) { + if ($revision_ids) { + $cids = []; + foreach ($revision_ids as $revision_id) { + unset($this->entityRevisions[$revision_id]); + $cids[] = $this->buildRevisionCacheId($revision_id); + } + if ($this->entityType->isPersistentlyCacheable()) { + $this->cacheBackend->deleteMultiple($cids); } } } /** + * {@inheritdoc} + */ + public function loadRevisionUnchanged($revision_id) { + // If the revision for the given revision id is present in the static cache + // and it represents a default revision, then delete the entity and the + // revision cache for it, otherwise there will be no entity cache and in + // this case we have to delete only the revision cache. + if (isset($this->entityRevisions[$revision_id]) && $this->entityRevisions[$revision_id]->isDefaultRevision()) { + $this->resetCache([$this->entityRevisions[$revision_id]->id()]); + } + else { + $this->resetRevisionCache([$revision_id]); + } + + return $this->loadRevision($revision_id); + } + + /** * Builds the cache ID for the passed in entity ID. * * @param int $id @@ -642,4 +786,17 @@ protected function buildCacheId($id) { return "values:{$this->entityTypeId}:$id"; } + /** + * Builds the cache ID for the passed in entity revision ID. + * + * @param int $revision_id + * Entity revision ID for which the cache ID should be built. + * + * @return string + * Cache ID that can be passed to the cache backend. + */ + protected function buildRevisionCacheId($revision_id) { + return "values:{$this->entityTypeId}:revision:$revision_id"; + } + } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php index e973c39..cd79bbd 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageInterface.php @@ -13,6 +13,41 @@ interface ContentEntityStorageInterface extends EntityStorageInterface { /** + * Load a specific entity revision. + * + * @param int|string $revision_id + * The revision id. + * + * @return \Drupal\Core\Entity\ContentEntityInterface|null + * The specified entity revision or NULL if not found. + */ + public function loadRevision($revision_id); + + /** + * Loads an unchanged entity by revision id from the database. + * + * @param mixed $revision_id + * The revision ID of the entity to load. + * + * @return \Drupal\Core\Entity\ContentEntityInterface|null + * The unchanged entity, or NULL if the entity cannot be loaded. + * + * @todo Remove this method once we have a reliable way to retrieve the + * unchanged entity from the entity object. + */ + public function loadRevisionUnchanged($revision_id); + + /** + * Delete a specific entity revision. + * + * A revision can only be deleted if it's not the currently active one. + * + * @param int $revision_id + * The revision id. + */ + public function deleteRevision($revision_id); + + /** * Constructs a new entity translation object, without permanently saving it. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index 31bf8ed..6548165 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -19,7 +19,7 @@ * * @var array */ - protected $entities = array(); + protected $entities = []; /** * Entity type ID for this storage. @@ -480,6 +480,12 @@ protected function doPostSave(EntityInterface $entity, $update) { $entity->setOriginalId($entity->id()); unset($entity->original); + + // Set the static entity cache reference, which has been removed by the + // call to ::resetCache. + if ($this->entityType->isStaticallyCacheable()) { + $this->entities[$entity->id()] = $entity; + } } /** diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php index f7b4b4a..a00f088 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php @@ -77,27 +77,6 @@ public function load($id); public function loadUnchanged($id); /** - * Load a specific entity revision. - * - * @param int|string $revision_id - * The revision id. - * - * @return \Drupal\Core\Entity\EntityInterface|null - * The specified entity revision or NULL if not found. - */ - public function loadRevision($revision_id); - - /** - * Delete a specific entity revision. - * - * A revision can only be deleted if it's not the currently active one. - * - * @param int $revision_id - * The revision id. - */ - public function deleteRevision($revision_id); - - /** * Load entities by their property values. * * @param array $values diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php index e696116..b9dbda3 100644 --- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueContentEntityStorage.php @@ -18,6 +18,27 @@ class KeyValueContentEntityStorage extends KeyValueEntityStorage implements Cont /** * {@inheritdoc} */ + public function loadRevision($revision_id) { + // @todo + } + + /** + * {@inheritdoc} + */ + public function loadRevisionUnchanged($revision_id) { + // @todo + } + + /** + * {@inheritdoc} + */ + public function deleteRevision($revision_id) { + // @todo + } + + /** + * {@inheritdoc} + */ public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) { // @todo Complete the content entity storage implementation in // https://www.drupal.org/node/2618436. diff --git a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php index 890a9b4..3bf9f18 100644 --- a/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/KeyValueStore/KeyValueEntityStorage.php @@ -131,20 +131,6 @@ public function doLoadMultiple(array $ids = NULL) { /** * {@inheritdoc} */ - public function loadRevision($revision_id) { - return NULL; - } - - /** - * {@inheritdoc} - */ - public function deleteRevision($revision_id) { - return NULL; - } - - /** - * {@inheritdoc} - */ public function doDelete($entities) { $entity_ids = array_keys($entities); $this->keyValueStore->deleteMultiple($entity_ids); diff --git a/core/lib/Drupal/Core/Entity/RevisionableInterface.php b/core/lib/Drupal/Core/Entity/RevisionableInterface.php index f9433bc..d68ce7a 100644 --- a/core/lib/Drupal/Core/Entity/RevisionableInterface.php +++ b/core/lib/Drupal/Core/Entity/RevisionableInterface.php @@ -45,6 +45,29 @@ public function setNewRevision($value = TRUE); public function getRevisionId(); /** + * Gets the original revision identifier of the entity. + * + * After calling ::setNewRevision, the revision identifier will be unset to + * enforce the creation of a new revision identifier on save and + * ::getRevisionId will not be able to return the revision identifier, for + * which the entity has been loaded, but ::getOriginalRevisionId will. + * + * @return + * The original revision identifier of the entity, or NULL if the entity + * does not have a revision identifier. + */ + public function getOriginalRevisionId(); + + /** + * Resets the original revision identifier after the entity got a new one. + * + * After the entity has been saved, the storage should call this function to + * set the original revision identifier to the new one, the entity received + * after it has been saved. + */ + public function resetOriginalRevisionId(); + + /** * Checks if this entity is the default revision. * * @param bool $new_value diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityStaticCacheTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityStaticCacheTest.php new file mode 100644 index 0000000..7c40ba1 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityStaticCacheTest.php @@ -0,0 +1,80 @@ +installEntitySchema('entity_test_rev'); + + $entity = EntityTestRev::create([ + 'name' => 'test' + ]); + $entity->save(); + $this->entityId = $entity->id(); + } + + /** + * Tests the static entity cache. + */ + public function testStaticEntityCache() { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_rev'); + + // Test load. + $entity_load_first = $storage->load($this->entityId); + $entity_load_second = $storage->load($this->entityId); + $this->assertSame($entity_load_first, $entity_load_second, 'Calling ContentEntityStorageBase::load multiple times returns a reference to the same entity object.'); + + $entity_unchanged_first = $storage->loadUnchanged($this->entityId); + $entity_unchanged_second = $storage->loadUnchanged($this->entityId); + $this->assertNotSame($entity_unchanged_first, $entity_unchanged_second, 'Calling ContentEntityStorageBase::loadUnchanged multiple times returns each time a new entity object.'); + $this->assertNotSame($entity_load_first, $entity_unchanged_first, 'ContentEntityStorageBase::load and ContentEntityStorageBase::loadUnchanged return different entity objects.'); + + // Test loadRevision. + $entity_rev_id = $entity_load_first->getRevisionId(); + $entity_revision_first = $storage->loadRevision($entity_rev_id); + $entity_revision_second = $storage->loadRevision($entity_rev_id); + $this->assertSame($entity_revision_first, $entity_revision_second, 'Calling ContentEntityStorageBase::loadRevision multiple times returns a reference to the same entity object.'); + + $entity_revision_unchanged_first = $storage->loadRevisionUnchanged($entity_rev_id); + $entity_revision_unchanged_second = $storage->loadRevisionUnchanged($entity_rev_id); + $this->assertNotSame($entity_revision_unchanged_first, $entity_revision_unchanged_second, 'Calling ContentEntityStorageBase::loadRevisionUnchanged multiple times returns each time a new entity object.'); + $this->assertNotSame($entity_revision_first, $entity_revision_unchanged_first, 'ContentEntityStorageBase::loadRevision and ContentEntityStorageBase::loadRevisionUnchanged return different entity objects.'); + + // Test loading a default revision with load and loadRevision. + $this->assertSame($entity_load_first, $entity_revision_first, 'ContentEntityStorageBase::load and ContentEntityStorageBase::loadRevision return a reference to the same entity object when loading a default revision.'); + } + +}