diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index b5fccbd..92de320 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -335,6 +335,13 @@ public function getRevisionId() { /** * {@inheritdoc} */ + public function getRevisionHash() { + return $this->getEntityKey('revision_hash'); + } + + /** + * {@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. @@ -1133,6 +1140,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setReadOnly(TRUE) ->setSetting('unsigned', TRUE); } + if ($entity_type->hasKey('revision_hash')) { + $fields[$entity_type->getKey('revision_hash')] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('Revision hash')) + ->setReadOnly(TRUE); + } if ($entity_type->hasKey('langcode')) { $fields[$entity_type->getKey('langcode')] = BaseFieldDefinition::create('language') ->setLabel(new TranslatableMarkup('Language')) diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index a63415b..c7919f9 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -288,10 +288,18 @@ public function __construct($definition) { // Ensure defaults. $this->entity_keys += array( 'revision' => '', + 'revision_hash' => '', 'bundle' => '', 'langcode' => '', 'default_langcode' => 'default_langcode', ); + + // If the entity type is revisionable, provide a default value for the + // 'revision_hash' key. + if (!empty($this->entity_keys['revision'])) { + $this->entity_keys['revision_hash'] = 'revision_hash'; + } + $this->handlers += array( 'access' => 'Drupal\Core\Entity\EntityAccessControlHandler', ); diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index 398347f..50c9983 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -88,6 +88,9 @@ public function getOriginalClass(); * revision ID of the entity. The Field API assumes that all revision IDs * are unique across all entities of a type. If this entry is omitted * the entities of this type are not revisionable. + * - revision_hash: (optional) The name of the property that contains the + * revision hash of the entity. Defaults to "revision_hash" if the + * 'revision' key is set. * - bundle: (optional) The name of the property that contains the bundle * name for the entity. The bundle name defines which set of fields are * attached to the entity (e.g. what nodes call "content type"). This diff --git a/core/lib/Drupal/Core/Entity/RevisionableInterface.php b/core/lib/Drupal/Core/Entity/RevisionableInterface.php index 14690e3..a5fb85f 100644 --- a/core/lib/Drupal/Core/Entity/RevisionableInterface.php +++ b/core/lib/Drupal/Core/Entity/RevisionableInterface.php @@ -40,6 +40,15 @@ public function setNewRevision($value = TRUE); public function getRevisionId(); /** + * Gets the revision hash of the entity. + * + * @return string + * The revision hash of the entity, or NULL if the entity does not + * have a revision hash. + */ + public function getRevisionHash(); + + /** * Checks if this entity is the default revision. * * @param bool $new_value diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index d584454..3552f18 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -55,6 +55,15 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt protected $revisionKey = FALSE; /** + * Name of entity's revision hash database field, if it supports revisions. + * + * Has the value FALSE if this entity does not use revisions. + * + * @var string + */ + protected $revisionHashKey = FALSE; + + /** * The entity langcode key. * * @var string|bool @@ -170,6 +179,7 @@ protected function initTableLayout() { // are correctly reflected in the table layout. $this->tableMapping = NULL; $this->revisionKey = NULL; + $this->revisionHashKey = NULL; $this->revisionTable = NULL; $this->dataTable = NULL; $this->revisionDataTable = NULL; @@ -180,6 +190,7 @@ protected function initTableLayout() { $revisionable = $this->entityType->isRevisionable(); if ($revisionable) { $this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id'; + $this->revisionHashKey = $this->entityType->getKey('revision_hash') ?: 'revision_hash'; $this->revisionTable = $this->entityType->getRevisionTable() ?: $this->entityTypeId . '_revision'; } $translatable = $this->entityType->isTranslatable(); @@ -286,7 +297,7 @@ public function getTableMapping(array $storage_definitions = NULL) { return $table_mapping->allowsSharedTableStorage($definition); }); - $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey))); + $key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->revisionHashKey, $this->langcodeKey))); $all_fields = array_keys($shared_table_definitions); $revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) { return $definition->isRevisionable(); @@ -316,7 +327,7 @@ public function getTableMapping(array $storage_definitions = NULL) { // denormalized in the base table but also stored in the revision table // together with the entity ID and the revision ID as identifiers. $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields)); - $revision_key_fields = array($this->idKey, $this->revisionKey); + $revision_key_fields = array($this->idKey, $this->revisionKey, $this->revisionHashKey); $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); } elseif (!$revisionable && $translatable) { @@ -343,11 +354,9 @@ public function getTableMapping(array $storage_definitions = NULL) { // Like in the multilingual, non-revisionable case the UUID is not // in the data table. Additionally, do not store revision metadata // fields in the data table. - $data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields)); - $table_mapping->setFieldNames($this->dataTable, $data_fields); + $data_fields = array_values(array_diff($all_fields, array($this->uuidKey, $this->revisionHashKey), $revision_metadata_fields)); $table_mapping->setFieldNames($this->dataTable, $data_fields); - $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields); - $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields); + $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->revisionHashKey, $this->langcodeKey), $revision_metadata_fields); $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields); $revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey); $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey)); @@ -1022,6 +1031,16 @@ protected function saveRevision(ContentEntityInterface $entity) { $entity->preSaveRevision($this, $record); if ($entity->isNewRevision()) { + // Generating a new revision should generate a new revision UUID. + if ($this->revisionHashKey) { + $array = $entity->toArray(); + // Don't include local IDs to keep it consistent across environments. + foreach ([$this->idKey, $this->revisionKey] as $key) { + unset($array[$key]); + } + $record->{$this->revisionHashKey} = md5(serialize($array)); + } + $insert_id = $this->database ->insert($this->revisionTable, array('return' => Database::RETURN_INSERT_ID)) ->fields((array) $record) @@ -1033,7 +1052,10 @@ protected function saveRevision(ContentEntityInterface $entity) { } if ($entity->isDefaultRevision()) { $this->database->update($this->entityType->getBaseTable()) - ->fields(array($this->revisionKey => $record->{$this->revisionKey})) + ->fields([ + $this->revisionKey => $record->{$this->revisionKey}, + $this->revisionHashKey => $record->{$this->revisionHashKey}, + ]) ->condition($this->idKey, $record->{$this->idKey}) ->execute(); } @@ -1046,8 +1068,11 @@ protected function saveRevision(ContentEntityInterface $entity) { ->execute(); } - // Make sure to update the new revision key for the entity. + // Make sure to update the new revision ID and hash for the entity. $entity->{$this->revisionKey}->value = $record->{$this->revisionKey}; + if ($this->revisionHashKey) { + $entity->{$this->revisionHashKey}->value = $record->{$this->revisionHashKey}; + } return $record->{$this->revisionKey}; } diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index e970e07..2b88d47 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -154,6 +154,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['revision_id']->setDescription(t('The revision ID.')); + $fields['revision_hash'] = BaseFieldDefinition::create('string') + ->setLabel(t('Revision hash')) + ->setDescription(t('The revision hash.')) + ->setReadOnly(TRUE); + $fields['langcode']->setDescription(t('The custom block language code.')); $fields['type']->setLabel(t('Block type'))