diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 89b83cf..44c98e2 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -161,6 +161,13 @@ class EntityType implements EntityTypeInterface { protected $revision_table = NULL; /** + * An array of entity metadata fields. + * + * @var array + */ + protected $revision_fields = []; + + /** * The name of the entity type's data table. * * @var string|null @@ -384,6 +391,29 @@ public function hasKey($key) { /** * {@inheritdoc} */ + public function getRevisionFields() { + return $this->revision_fields; + } + + /** + * {@inheritdoc} + */ + public function getRevisionField($field) { + $keys = $this->getRevisionFields(); + return isset($keys[$field]) ? $keys[$field] : FALSE; + } + + /** + * {@inheritdoc} + */ + public function hasRevisionField($field) { + $keys = $this->getRevisionFields(); + return !empty($keys[$field]); + } + + /** + * {@inheritdoc} + */ public function id() { return $this->id; } diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index c6336c9..847a66b 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -133,6 +133,45 @@ public function getKey($key); public function hasKey($key); /** + * Gets an array of entity revision metadata fields. + * + * @return array + * An array describing how the Field API can extract revision metadata + * information of this entity type: + * - revision_log_message: The name of the property that contains description + * of the changes that were made in the current revision. + * - revision_user: The name of the property that contains the user ID of + * the author of the current revision. + * - revision_created: The name of the property that contains the timestamp + * of the current revision. + */ + public function getRevisionFields(); + + /** + * Gets a specific entity revision metadata field. + * + * @param string $key + * The name of the entity revision metadata field to return. + * + * @return string|bool + * The entity revision metadata field, or FALSE if it does not exist. + * + * @see self::getRevisionFields() + */ + public function getRevisionField($field); + + /** + * Indicates if a given entity revision metadata field exists. + * + * @param string $key + * The name of the entity revision metadata field to check. + * + * @return bool + * TRUE if a given entity revision metadata field exists, FALSE otherwise. + */ + public function hasRevisionField($field); + + /** * Indicates whether entities should be statically cached. * * @return bool diff --git a/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php b/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php index 7392858..d6f4787 100644 --- a/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php +++ b/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php @@ -25,30 +25,34 @@ * @see \Drupal\Core\Entity\FieldableEntityInterface::baseFieldDefinitions() */ public static function revisionLogBaseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields['revision_created'] = BaseFieldDefinition::create('created') - ->setLabel(t('Revision create time')) - ->setDescription(t('The time that the current revision was created.')) - ->setRevisionable(TRUE); - - $fields['revision_user'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Revision user')) - ->setDescription(t('The user ID of the author of the current revision.')) - ->setSetting('target_type', 'user') - ->setRevisionable(TRUE); - - $fields['revision_log_message'] = BaseFieldDefinition::create('string_long') - ->setLabel(t('Revision log message')) - ->setDescription(t('Briefly describe the changes you have made.')) - ->setRevisionable(TRUE) - ->setDefaultValue('') - ->setDisplayOptions('form', [ - 'type' => 'string_textarea', - 'weight' => 25, - 'settings' => [ - 'rows' => 4, - ], - ]); - + $fields = []; + if ($entity_type->hasRevisionField('revision_created')) { + $fields[$entity_type->getRevisionField('revision_created')] = BaseFieldDefinition::create('created') + ->setLabel(t('Revision create time')) + ->setDescription(t('The time that the current revision was created.')) + ->setRevisionable(TRUE); + } + if ($entity_type->hasRevisionField('revision_user')) { + $fields[$entity_type->getRevisionField('revision_user')] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Revision user')) + ->setDescription(t('The user ID of the author of the current revision.')) + ->setSetting('target_type', 'user') + ->setRevisionable(TRUE); + } + if ($entity_type->hasRevisionField('revision_log_message')) { + $fields[$entity_type->getRevisionField('revision_log_message')] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Revision log message')) + ->setDescription(t('Briefly describe the changes you have made.')) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textarea', + 'weight' => 25, + 'settings' => [ + 'rows' => 4, + ], + ]); + } return $fields; } @@ -56,14 +60,16 @@ public static function revisionLogBaseFieldDefinitions(EntityTypeInterface $enti * Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionCreationTime(). */ public function getRevisionCreationTime() { - return $this->revision_created->value; + $field = $this->getEntityType()->getRevisionField('revision_created'); + return $this->{$field}->value; } /** * Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionCreationTime(). */ public function setRevisionCreationTime($timestamp) { - $this->revision_created->value = $timestamp; + $field = $this->getEntityType()->getRevisionField('revision_created'); + $this->{$field}->value = $timestamp; return $this; } @@ -71,14 +77,16 @@ public function setRevisionCreationTime($timestamp) { * Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionUser(). */ public function getRevisionUser() { - return $this->revision_user->entity; + $field = $this->getEntityType()->getRevisionField('revision_user'); + return $this->{$field}->entity; } /** * Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionUser(). */ public function setRevisionUser(UserInterface $account) { - $this->revision_user->entity = $account; + $field = $this->getEntityType()->getRevisionField('revision_user'); + $this->{$field}->entity = $account; return $this; } @@ -86,14 +94,16 @@ public function setRevisionUser(UserInterface $account) { * Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionUserId(). */ public function getRevisionUserId() { - return $this->revision_user->target_id; + $field = $this->getEntityType()->getRevisionField('revision_user'); + return $this->{$field}->target_id; } /** * Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionUserId(). */ public function setRevisionUserId($user_id) { - $this->revision_user->target_id = $user_id; + $field = $this->getEntityType()->getRevisionField('revision_user'); + $this->{$field}->target_id = $user_id; return $this; } @@ -101,14 +111,16 @@ public function setRevisionUserId($user_id) { * Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionLogMessage(). */ public function getRevisionLogMessage() { - return $this->revision_log_message->value; + $field = $this->getEntityType()->getRevisionField('revision_log_message'); + return $this->{$field}->value; } /** * Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionLogMessage(). */ public function setRevisionLogMessage($revision_log_message) { - $this->revision_log_message->value = $revision_log_message; + $field = $this->getEntityType()->getRevisionField('revision_log_message'); + $this->{$field}->value = $revision_log_message; return $this; } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index 2ea450d..f2f4afd 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -294,17 +294,14 @@ public function getTableMapping(array $storage_definitions = NULL) { // Make sure the key fields come first in the list of fields. $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields)); - // Nodes have all three of these fields, while custom blocks only have - // log. - // @todo Provide automatic definitions for revision metadata fields in - // https://www.drupal.org/node/2248983. - $revision_metadata_fields = array_intersect(array( - 'revision_timestamp', - 'revision_uid', - 'revision_log', - ), $all_fields); - + // If the entity is revisionable, gather the fields that need to be put + // in the revision table. $revisionable = $this->entityType->isRevisionable(); + if ($revisionable) { + $revision_fields = $this->entityType->getRevisionFields(); + $revision_metadata_fields = array_intersect(array_values($revision_fields), $all_fields); + } + $translatable = $this->entityType->isTranslatable(); if (!$revisionable && !$translatable) { // The base layout stores all the base field values in the base table. diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index 13af342..0bd3c99 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -50,6 +50,11 @@ * "langcode" = "langcode", * "uuid" = "uuid" * }, + * revision_fields = { + * "revision_user" = "revision_user", + * "revision_created" = "revision_created", + * "revision_log_message" = "revision_log" + * }, * bundle_entity_type = "block_content_type", * field_ui_base_route = "entity.block_content_type.edit_form", * render_cache = FALSE, diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index bdb8050..cfbc00e 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -57,6 +57,11 @@ * "status" = "status", * "uid" = "uid", * }, + * revision_fields = { + * "revision_user" = "revision_uid", + * "revision_created" = "revision_timestamp", + * "revision_log_message" = "revision_log" + * }, * bundle_entity_type = "node_type", * field_ui_base_route = "entity.node_type.edit_form", * common_reference_target = TRUE, diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 3ef24a5..f9fe50e 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1730,3 +1730,76 @@ function system_update_8201() { /** * @} End of "addtogroup updates-8.2.0". */ + +/** + * @addtogroup updates-8.3.0 + * @{ + */ + +/** + * Move revision metadata fields from the data tables to the revision table. + * + * Due to the fields from RevisionLogEntityTrait not being explicitly mentioned + * in the storage they might have been installed wrongly in the data tables for + * revisionable and translatable entities. + */ +function system_update_8300(&$sandbox) { + static $revisions = NULL; + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + $database = \Drupal::database(); + $database_schema = $database->schema(); + $revision_metadata_fields_to_update = array_flip([ + 'revision_created', + 'revision_user', + 'revision_log_message' + ]); + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) { + if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) { + $class = $entity_type->getClass(); + $base_fields = $class::baseFieldDefinitions($entity_type); + $fields_to_update = array_intersect_key($base_fields, $revision_metadata_fields_to_update); + + if (!empty($fields_to_update)) { + $data_table = $entity_type->getDataTable() ?: $entity_type_id . '_field_data'; + $revision_data_table = $entity_type->getRevisionDataTable() ?: $entity_type_id . '_field_revision'; + $revision_table = $entity_type->getRevisionTable() ?: $entity_type_id . '_revision'; + $revision_field = $entity_type->getKey('revision'); + + foreach ($fields_to_update as $field_name => $definition) { + // Ensure the field is present in the data tables. + if ($database_schema->fieldExists($data_table, $field_name) && $database_schema->fieldExists($revision_data_table, $field_name)) { + // Install the field in the revision table. + $entity_definition_update_manager->installFieldStorageDefinition($field_name, $entity_type_id, $entity_type->getProvider(), $definition); + + // Collect the revision ids only once for an entity type. + $revisions = isset($revisions) ? $revisions : \Drupal::entityQuery($entity_type_id) + ->allRevisions() + ->execute(); + + // Apply the field value from the revision data table to the + // revision table. + foreach ($revisions as $rev_id => $id) { + $field_value = $database->select($revision_data_table, 'rd') + ->fields('rd', [$field_name]) + ->condition($revision_field, $rev_id) + ->execute() + ->fetchField(); + $database->update($revision_table) + ->condition($revision_field, $rev_id) + ->fields([$field_name => $field_value]) + ->execute(); + } + // Drop the field from the data tables. + $database_schema->dropField($data_table, $field_name); + $database_schema->dropField($revision_data_table, $field_name); + } + } + $revisions = NULL; + } + } + } +} + +/** + * @} End of "addtogroup updates-8.3.0". + */ diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php index 4f4f4f1..d9e7c06 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php @@ -35,6 +35,11 @@ * "label" = "name", * "langcode" = "langcode", * }, + * revision_fields = { + * "revision_user" = "revision_uid", + * "revision_created" = "revision_timestamp", + * "revision_log_message" = "revision_log" + * }, * links = { * "canonical" = "/entity_test_revlog/manage/{entity_test_revlog}", * "delete-form" = "/entity_test/delete/entity_test_revlog/{entity_test_revlog}", diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php index 56b4b00..099c331 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php @@ -533,6 +533,9 @@ public function testGetTableMappingRevisionable(array $entity_keys) { array('bundle', $entity_keys['bundle']), array('revision', $entity_keys['revision']), ))); + $this->entityType->expects($this->any()) + ->method('getRevisionFields') + ->will($this->returnValue([])); $this->setUpEntityStorage(); @@ -573,13 +576,13 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { // PHPUnit does not allow for multiple data providers. $test_cases = array( array(), - array('revision_timestamp'), - array('revision_uid'), - array('revision_log'), - array('revision_timestamp', 'revision_uid'), - array('revision_timestamp', 'revision_log'), - array('revision_uid', 'revision_log'), - array('revision_timestamp', 'revision_uid', 'revision_log'), + array('revision_created' => 'revision_timestamp'), + array('revision_user' => 'revision_uid'), + array('revision_log_message' => 'revision_log'), + array('revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid'), + array('revision_created' => 'revision_timestamp', 'revision_log_message' => 'revision_log'), + array('revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'), + array('revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'), ); foreach ($test_cases as $revision_metadata_field_names) { $this->setUp(); @@ -604,6 +607,10 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { array('revision', $entity_keys['revision']), ))); + $this->entityType->expects($this->any()) + ->method('getRevisionFields') + ->will($this->returnValue($revision_metadata_field_names)); + $this->setUpEntityStorage(); $mapping = $this->entityStorage->getTableMapping(); @@ -760,6 +767,11 @@ public function testGetTableMappingRevisionableTranslatable(array $entity_keys) 'uuid' => $entity_keys['uuid'], 'langcode' => 'langcode', ); + $revision_fields = array( + 'revision_created' => 'revision_timestamp', + 'revision_user' => 'revision_uid', + 'revision_log_message' => 'revision_log' + ); $this->entityType->expects($this->atLeastOnce()) ->method('isRevisionable') @@ -779,6 +791,9 @@ public function testGetTableMappingRevisionableTranslatable(array $entity_keys) array('revision', $entity_keys['revision']), array('langcode', $entity_keys['langcode']), ))); + $this->entityType->expects($this->any()) + ->method('getRevisionFields') + ->will($this->returnValue($revision_fields)); $this->setUpEntityStorage(); @@ -864,13 +879,13 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent // PHPUnit does not allow for multiple data providers. $test_cases = array( array(), - array('revision_timestamp'), - array('revision_uid'), - array('revision_log'), - array('revision_timestamp', 'revision_uid'), - array('revision_timestamp', 'revision_log'), - array('revision_uid', 'revision_log'), - array('revision_timestamp', 'revision_uid', 'revision_log'), + array('revision_created' => 'revision_timestamp'), + array('revision_user' => 'revision_uid'), + array('revision_log_message' => 'revision_log'), + array('revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid'), + array('revision_created' => 'revision_timestamp', 'revision_log_message' => 'revision_log'), + array('revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'), + array('revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'), ); foreach ($test_cases as $revision_metadata_field_names) { $this->setUp(); @@ -900,6 +915,9 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent array('revision', $entity_keys['revision']), array('langcode', $entity_keys['langcode']), ))); + $this->entityType->expects($this->any()) + ->method('getRevisionFields') + ->will($this->returnValue($revision_metadata_field_names)); $this->setUpEntityStorage();