Commit 7f00c341 authored by effulgentsia's avatar effulgentsia
Browse files

Issue #2926483 by plach, amateescu, effulgentsia, xjm, Sam152, timmillwood,...

Issue #2926483 by plach, amateescu, effulgentsia, xjm, Sam152, timmillwood, jibran, larowlan, Gábor Hojtsy: Add API methods for determining whether an entity object is the latest (translation-affecting) revision
parent 0dc30938
......@@ -331,6 +331,26 @@ public function isDefaultRevision($new_value = NULL) {
return $this->isNew() || $return;
}
/**
* {@inheritdoc}
*/
public function isLatestRevision() {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
return $this->getLoadedRevisionId() == $storage->getLatestRevisionId($this->id());
}
/**
* {@inheritdoc}
*/
public function isLatestTranslationAffectedRevision() {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
return $this->getLoadedRevisionId() == $storage->getLatestTranslationAffectedRevisionId($this->id(), $this->language()->getId());
}
/**
* {@inheritdoc}
*/
......
......@@ -166,6 +166,47 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
return $translation;
}
/**
* {@inheritdoc}
*/
public function getLatestRevisionId($entity_id) {
if (!$this->entityType->isRevisionable()) {
return NULL;
}
$result = $this->getQuery()
->latestRevision()
->condition($this->entityType->getKey('id'), $entity_id)
->accessCheck(FALSE)
->execute();
return key($result);
}
/**
* {@inheritdoc}
*/
public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
if (!$this->entityType->isRevisionable()) {
return NULL;
}
if (!$this->entityType->isTranslatable()) {
return $this->getLatestRevisionId($entity_id);
}
$result = $this->getQuery()
->allRevisions()
->condition($this->entityType->getKey('id'), $entity_id)
->condition($this->entityType->getKey('revision_translation_affected'), 1, '=', $langcode)
->range(0, 1)
->sort($this->entityType->getKey('revision'), 'DESC')
->accessCheck(FALSE)
->execute();
return key($result);
}
/**
* {@inheritdoc}
*/
......
......@@ -30,4 +30,18 @@ public function loadMultipleRevisions(array $revision_ids) {
return [];
}
/**
* {@inheritdoc}
*/
public function getLatestRevisionId($entity_id) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getLatestTranslationAffectedRevisionId($entity_id, $langcode) {
return NULL;
}
}
......@@ -51,6 +51,14 @@ public function getRevisionId();
*/
public function isDefaultRevision($new_value = NULL);
/**
* Checks if this entity is the latest revision.
*
* @return bool
* TRUE if the entity is the latest revision, FALSE otherwise.
*/
public function isLatestRevision();
/**
* Acts on a revision before it gets saved.
*
......
......@@ -40,4 +40,15 @@ public function loadMultipleRevisions(array $revision_ids);
*/
public function deleteRevision($revision_id);
/**
* Returns the latest revision identifier for an entity.
*
* @param int|string $entity_id
* The entity identifier.
*
* @return int|string|null
* The latest revision identifier or NULL if no revision could be found.
*/
public function getLatestRevisionId($entity_id);
}
......@@ -7,6 +7,15 @@
*/
interface TranslatableRevisionableInterface extends TranslatableInterface, RevisionableInterface {
/**
* Checks whether this is the latest revision affecting this translation.
*
* @return bool
* TRUE if this revision is the latest one affecting the active translation,
* FALSE otherwise.
*/
public function isLatestTranslationAffectedRevision();
/**
* Marks the current revision translation as affected.
*
......
......@@ -6,4 +6,19 @@
* A storage that supports translatable and revisionable entity types.
*/
interface TranslatableRevisionableStorageInterface extends TranslatableStorageInterface, RevisionableStorageInterface {
/**
* Returns the latest revision affecting the specified translation.
*
* @param int|string $entity_id
* The entity identifier.
* @param string $langcode
* The language code of the translation.
*
* @return int|string|null
* A revision ID or NULL if no revision affecting the specified translation
* could be found.
*/
public function getLatestTranslationAffectedRevisionId($entity_id, $langcode);
}
......@@ -168,6 +168,35 @@
* @endcode
* This involves the same hooks and operations as regular entity loading.
*
* The "latest revision" of an entity is the most recently created one,
* regardless of it being default or pending. If the entity is translatable,
* revision translations are not taken into account either. In other words, any
* time a new revision is created, that becomes the latest revision for the
* entity overall, regardless of the affected translations. To load the latest
* revision of an entity:
* @code
* $revision_id = $storage->getLatestRevisionId($entity_id);
* $entity = $storage->loadRevision($revision_id);
* @endcode
* As usual, if the entity is translatable, this code instantiates into $entity
* the default translation of the revision, even if the latest revision contains
* only changes to a different translation:
* @code
* $is_default = $entity->isDefaultTranslation(); // returns TRUE
* @endcode
*
* The "latest translation-affected revision" is the most recently created one
* that affects the specified translation. For example, when a new revision
* introducing some changes to an English translation is saved, that becomes the
* new "latest revision". However, if an existing Italian translation was not
* affected by those changes, then the "latest translation-affected revision"
* for Italian remains what it was. To load the Italian translation at its
* latest translation-affected revision:
* @code
* $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity_id, 'it');
* $it_translation = $storage->loadRevision($revision_id)->getTranslation('it');
* @endcode
*
* @section save Save operations
* To update an existing entity, you will need to load it, change properties,
* and then save; as described above, when creating a new entity, you will also
......@@ -198,6 +227,10 @@
* - Comment: hook_comment_publish() and hook_comment_unpublish() as
* appropriate.
*
* Note that all translations available for the entity are stored during a save
* operation. When saving a new revision, a copy of every translation is stored,
* regardless of it being affected by the revision.
*
* @section edit Editing operations
* When an entity's add/edit form is used to add or edit an entity, there
* are several hooks that are invoked:
......
......@@ -5,8 +5,6 @@
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Symfony\Component\Routing\Route;
......@@ -71,8 +69,11 @@ public function convert($value, $definition, $name, array $defaults) {
// If the entity type is revisionable and the parameter has the
// "load_latest_revision" flag, load the latest revision.
if ($entity instanceof ContentEntityInterface && !empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) {
$latest_revision_id = $this->getLatestRevisionId($storage, $entity_definition, $value);
if ($entity->getLoadedRevisionId() !== $latest_revision_id) {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$latest_revision_id = $storage->getLatestRevisionId($value);
// We explicitly perform a loose equality check, since a revision ID may
// be returned as an integer or a string.
if ($entity->getLoadedRevisionId() != $latest_revision_id) {
$entity = $storage->loadRevision($latest_revision_id);
}
}
......@@ -86,33 +87,6 @@ public function convert($value, $definition, $name, array $defaults) {
return $entity;
}
/**
* Get the latest revision ID.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_definition
* The entity definition.
* @param mixed $value
* The raw value.
*
* @return int
* The latest revision ID for a given entity.
*/
protected function getLatestRevisionId(EntityStorageInterface $storage, EntityTypeInterface $entity_definition, $value) {
// @todo, replace this query with a standardized way of getting the
// latest revision in https://www.drupal.org/node/2784201.
$result = $storage
->getQuery()
->latestRevision()
->condition($entity_definition->getKey('id'), $value)
// The entity converter is not concerned with access checking, skip the
// access check when looking up the latest revision.
->accessCheck(FALSE)
->execute();
return key($result);
}
/**
* {@inheritdoc}
*/
......
......@@ -130,10 +130,10 @@ function quickedit_preprocess_page_title(&$variables) {
function quickedit_preprocess_field(&$variables) {
$variables['#cache']['contexts'][] = 'user.permissions';
$element = $variables['element'];
/** @var $entity \Drupal\Core\Entity\EntityInterface */
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $element['#object'];
if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !_quickedit_entity_is_latest_revision($entity)) {
if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !$entity->isLatestRevision()) {
return;
}
......@@ -157,8 +157,9 @@ function quickedit_preprocess_field(&$variables) {
* Implements hook_entity_view_alter().
*/
function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$build['#cache']['contexts'][] = 'user.permissions';
if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !_quickedit_entity_is_latest_revision($entity)) {
if (!\Drupal::currentUser()->hasPermission('access in-place editing') || !$entity->isLatestRevision()) {
return;
}
......@@ -174,22 +175,14 @@ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityVie
* @return bool
* TRUE if the loaded entity is the latest revision, FALSE otherwise.
*
* @todo Remove this method once better support for pending revisions is added
* to core https://www.drupal.org/node/2784201.
* @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use
* \Drupal\Core\Entity\RevisionableInterface::isLatestRevision() instead.
* As internal API, _quickedit_entity_is_latest_revision() may also be removed
* in a minor release.
*
* @internal
*/
function _quickedit_entity_is_latest_revision(ContentEntityInterface $entity) {
if (!$entity->getEntityType()->isRevisionable() || $entity->isNew()) {
return TRUE;
}
$latest_revision = \Drupal::entityTypeManager()
->getStorage($entity->getEntityTypeId())
->getQuery()
->latestRevision()
->condition($entity->getEntityType()->getKey('id'), $entity->id())
->execute();
return !empty($latest_revision) && $entity->getLoadedRevisionId() == key($latest_revision) ? TRUE : FALSE;
@trigger_error('_quickedit_entity_is_latest_revision() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Entity\RevisionableInterface::isLatestRevision() instead. As internal API, _quickedit_entity_is_latest_revision() may also be removed in a minor release.', E_USER_DEPRECATED);
return $entity->isLatestRevision();
}
......@@ -8,9 +8,11 @@
/**
* Tests the loaded Revision of an entity.
*
* @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase
*
* @group entity
*/
class EntityLoadedRevisionTest extends EntityKernelTestBase {
class EntityRevisionsTest extends EntityKernelTestBase {
/**
* Modules to enable.
......@@ -164,7 +166,99 @@ public function testSaveInHookEntityInsert() {
$loadedRevisionId = \Drupal::state()->get('entity_test.loadedRevisionId');
$this->assertEquals($entity->getLoadedRevisionId(), $loadedRevisionId);
$this->assertEquals($entity->getRevisionId(), $entity->getLoadedRevisionId());
}
/**
* Tests that latest revisions are working as expected.
*
* @covers ::isLatestRevision
*/
public function testIsLatestRevision() {
// Create a basic EntityTestMulRev entity and save it.
$entity = EntityTestMulRev::create();
$entity->save();
$this->assertTrue($entity->isLatestRevision());
// Load the created entity and create a new pending revision.
$pending_revision = EntityTestMulRev::load($entity->id());
$pending_revision->setNewRevision(TRUE);
$pending_revision->isDefaultRevision(FALSE);
// The pending revision should still be marked as the latest one before it
// is saved.
$this->assertTrue($pending_revision->isLatestRevision());
$pending_revision->save();
$this->assertTrue($pending_revision->isLatestRevision());
// Load the default revision and check that it is not marked as the latest
// revision.
$default_revision = EntityTestMulRev::load($entity->id());
$this->assertFalse($default_revision->isLatestRevision());
}
/**
* Tests that latest affected revisions are working as expected.
*
* The latest revision affecting a particular translation behaves as the
* latest revision for monolingual entities.
*
* @covers ::isLatestTranslationAffectedRevision
* @covers \Drupal\Core\Entity\ContentEntityStorageBase::getLatestRevisionId
* @covers \Drupal\Core\Entity\ContentEntityStorageBase::getLatestTranslationAffectedRevisionId
*/
public function testIsLatestAffectedRevisionTranslation() {
ConfigurableLanguage::createFromLangcode('it')->save();
// Create a basic EntityTestMulRev entity and save it.
$entity = EntityTestMulRev::create();
$entity->setName($this->randomString());
$entity->save();
$this->assertTrue($entity->isLatestTranslationAffectedRevision());
// Load the created entity and create a new pending revision.
$pending_revision = EntityTestMulRev::load($entity->id());
$pending_revision->setName($this->randomString());
$pending_revision->setNewRevision(TRUE);
$pending_revision->isDefaultRevision(FALSE);
// Check that no revision affecting Italian is available, given that no
// Italian translation has been created yet.
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityManager->getStorage($entity->getEntityTypeId());
$this->assertNull($storage->getLatestTranslationAffectedRevisionId($entity->id(), 'it'));
$this->assertEquals($pending_revision->getLoadedRevisionId(), $storage->getLatestRevisionId($entity->id()));
// The pending revision should still be marked as the latest affected one
// before it is saved.
$this->assertTrue($pending_revision->isLatestTranslationAffectedRevision());
$pending_revision->save();
$this->assertTrue($pending_revision->isLatestTranslationAffectedRevision());
// Load the default revision and check that it is not marked as the latest
// (translation-affected) revision.
$default_revision = EntityTestMulRev::load($entity->id());
$this->assertFalse($default_revision->isLatestRevision());
$this->assertFalse($default_revision->isLatestTranslationAffectedRevision());
// Add a translation in a new pending revision and verify that both the
// English and Italian revision translations are the latest affected
// revisions for their respective languages, while the English revision is
// not the latest revision.
/** @var \Drupal\entity_test\Entity\EntityTestMulRev $en_revision */
$en_revision = clone $pending_revision;
/** @var \Drupal\entity_test\Entity\EntityTestMulRev $it_revision */
$it_revision = $pending_revision->addTranslation('it');
$it_revision->setName($this->randomString());
$it_revision->setNewRevision(TRUE);
$it_revision->isDefaultRevision(FALSE);
// @todo Remove this once the "original" property works with revisions. See
// https://www.drupal.org/project/drupal/issues/2859042.
$it_revision->original = $storage->loadRevision($it_revision->getLoadedRevisionId());
$it_revision->save();
$this->assertTrue($it_revision->isLatestRevision());
$this->assertTrue($it_revision->isLatestTranslationAffectedRevision());
$this->assertFalse($en_revision->isLatestRevision());
$this->assertTrue($en_revision->isLatestTranslationAffectedRevision());
}
}
......@@ -122,4 +122,32 @@ public function testWithTranslatedPendingRevision() {
$this->assertEquals($translated_entity->getLoadedRevisionId(), $converted->getLoadedRevisionId());
}
/**
* Tests that pending revisions are loaded only when needed.
*/
public function testOptimizedConvert() {
$entity = EntityTestMulRev::create();
$entity->save();
// Populate static cache for the current entity.
$entity = EntityTestMulRev::load($entity->id());
// Delete the base table entry for the current entity, however, since the
// storage will query the revision table to get the latest revision, the
// logic handling pending revisions will work correctly anyway.
/** @var \Drupal\Core\Database\Connection $database */
$database = $this->container->get('database');
$database->delete('entity_test_mulrev')
->condition('id', $entity->id())
->execute();
// If optimization works, converting a default revision should not trigger
// a storage load, thus making the following assertion pass.
$converted = $this->converter->convert(1, [
'load_latest_revision' => TRUE,
'type' => 'entity:entity_test_mulrev',
], 'foo', []);
$this->assertEquals($entity->getLoadedRevisionId(), $converted->getLoadedRevisionId());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment