Commit 4b325ae0 authored by catch's avatar catch

Issue #2248983 by hchonov, amateescu, paranojik, timmillwood, tstoeckler,...

Issue #2248983 by hchonov, amateescu, paranojik, timmillwood, tstoeckler, catch, Berdir, alexpott: Define the revision metadata base fields in the entity annotation in order for the storage to create them only in the revision table
parent 801f6ca1
......@@ -7,6 +7,13 @@
*/
class ContentEntityType extends EntityType implements ContentEntityTypeInterface {
/**
* An array of entity revision metadata keys.
*
* @var array
*/
protected $revision_metadata_keys = [];
/**
* {@inheritdoc}
*/
......@@ -41,4 +48,44 @@ protected function checkStorageClass($class) {
}
}
/**
* {@inheritdoc}
*/
public function getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE) {
// Provide backwards compatibility in case the revision metadata keys are
// not defined in the entity annotation.
if (!$this->revision_metadata_keys && $include_backwards_compatibility_field_names) {
$base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
@trigger_error('The revision_user revision metadata key is not set.', E_USER_DEPRECATED);
$this->revision_metadata_keys['revision_user'] = $revision_user;
}
if ((isset($base_fields['revision_timestamp']) && $revision_timestamp = 'revision_timestamp') || (isset($base_fields['revision_created'])) && $revision_timestamp = 'revision_created') {
@trigger_error('The revision_created revision metadata key is not set.', E_USER_DEPRECATED);
$this->revision_metadata_keys['revision_created'] = $revision_timestamp;
}
if ((isset($base_fields['revision_log']) && $revision_log = 'revision_log') || (isset($base_fields['revision_log_message']) && $revision_log = 'revision_log_message')) {
@trigger_error('The revision_log_message revision metadata key is not set.', E_USER_DEPRECATED);
$this->revision_metadata_keys['revision_log_message'] = $revision_log;
}
}
return $this->revision_metadata_keys;
}
/**
* {@inheritdoc}
*/
public function getRevisionMetadataKey($key) {
$keys = $this->getRevisionMetadataKeys();
return isset($keys[$key]) ? $keys[$key] : FALSE;
}
/**
* {@inheritdoc}
*/
public function hasRevisionMetadataKey($key) {
$keys = $this->getRevisionMetadataKeys();
return isset($keys[$key]);
}
}
......@@ -6,4 +6,50 @@
* Provides an interface for a content entity type and its metadata.
*/
interface ContentEntityTypeInterface extends EntityTypeInterface {
/**
* Gets an array of entity revision metadata keys.
*
* @param bool $include_backwards_compatibility_field_names
* (optional and deprecated) Whether to provide the revision keys on a
* best-effort basis by looking at the base fields defined by the entity
* type. Note that this parameter will be removed in Drupal 9.0.0. Defaults
* to TRUE.
*
* @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 getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE);
/**
* Gets a specific entity revision metadata key.
*
* @param string $key
* The name of the entity revision metadata key to return.
*
* @return string|bool
* The entity revision metadata key, or FALSE if it does not exist.
*
* @see self::getRevisionMetadataKeys()
*/
public function getRevisionMetadataKey($key);
/**
* Indicates if a given entity revision metadata key exists.
*
* @param string $key
* The name of the entity revision metadata key to check.
*
* @return bool
* TRUE if a given entity revision metadata key exists, FALSE otherwise.
*/
public function hasRevisionMetadataKey($key);
}
......@@ -25,18 +25,18 @@ trait RevisionLogEntityTrait {
* @see \Drupal\Core\Entity\FieldableEntityInterface::baseFieldDefinitions()
*/
public static function revisionLogBaseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['revision_created'] = BaseFieldDefinition::create('created')
$fields[static::getRevisionMetadataKey($entity_type, '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')
$fields[static::getRevisionMetadataKey($entity_type, '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')
$fields[static::getRevisionMetadataKey($entity_type, 'revision_log_message')] = BaseFieldDefinition::create('string_long')
->setLabel(t('Revision log message'))
->setDescription(t('Briefly describe the changes you have made.'))
->setRevisionable(TRUE)
......@@ -56,14 +56,14 @@ public static function revisionLogBaseFieldDefinitions(EntityTypeInterface $enti
* Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionCreationTime().
*/
public function getRevisionCreationTime() {
return $this->revision_created->value;
return $this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_created')}->value;
}
/**
* Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionCreationTime().
*/
public function setRevisionCreationTime($timestamp) {
$this->revision_created->value = $timestamp;
$this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_created')}->value = $timestamp;
return $this;
}
......@@ -71,14 +71,14 @@ public function setRevisionCreationTime($timestamp) {
* Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionUser().
*/
public function getRevisionUser() {
return $this->revision_user->entity;
return $this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_user')}->entity;
}
/**
* Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionUser().
*/
public function setRevisionUser(UserInterface $account) {
$this->revision_user->entity = $account;
$this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_user')}->entity = $account;
return $this;
}
......@@ -86,14 +86,14 @@ public function setRevisionUser(UserInterface $account) {
* Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionUserId().
*/
public function getRevisionUserId() {
return $this->revision_user->target_id;
return $this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_user')}->target_id;
}
/**
* Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionUserId().
*/
public function setRevisionUserId($user_id) {
$this->revision_user->target_id = $user_id;
$this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_user')}->target_id = $user_id;
return $this;
}
......@@ -101,15 +101,41 @@ public function setRevisionUserId($user_id) {
* Implements \Drupal\Core\Entity\RevisionLogInterface::getRevisionLogMessage().
*/
public function getRevisionLogMessage() {
return $this->revision_log_message->value;
return $this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_log_message')}->value;
}
/**
* Implements \Drupal\Core\Entity\RevisionLogInterface::setRevisionLogMessage().
*/
public function setRevisionLogMessage($revision_log_message) {
$this->revision_log_message->value = $revision_log_message;
$this->{static::getRevisionMetadataKey($this->getEntityType(), 'revision_log_message')}->value = $revision_log_message;
return $this;
}
/**
* Gets the name of a revision metadata field.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* A content entity type definition.
* @param string $key
* The revision metadata key to get, must be one of 'revision_created',
* 'revision_user' or 'revision_log_message'.
*
* @return string
* The name of the field for the specified $key.
*/
protected static function getRevisionMetadataKey(EntityTypeInterface $entity_type, $key) {
// We need to prevent ContentEntityType::getRevisionMetadataKey() from
// providing fallback as that requires fetching the entity type's field
// definition leading to an infinite recursion.
/** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
$revision_metadata_keys = $entity_type->getRevisionMetadataKeys(FALSE) + [
'revision_created' => 'revision_created',
'revision_user' => 'revision_user',
'revision_log_message' => 'revision_log_message',
];
return $revision_metadata_keys[$key];
}
}
......@@ -294,17 +294,11 @@ 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();
$revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
$translatable = $this->entityType->isTranslatable();
if (!$revisionable && !$translatable) {
// The base layout stores all the base field values in the base table.
......
......@@ -51,6 +51,11 @@
* "langcode" = "langcode",
* "uuid" = "uuid"
* },
* revision_metadata_keys = {
* "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,
......
......@@ -61,6 +61,11 @@
* "published" = "status",
* "uid" = "uid",
* },
* revision_metadata_keys = {
* "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,
......
<?php
namespace Drupal\system\Tests\Entity\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
use Drupal\views\Entity\View;
/**
* Tests the upgrade path for moving the revision metadata fields.
*
* @group Update
*/
class MoveRevisionMetadataFieldsUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.2.0.bare.standard_with_entity_test_revlog_enabled.php.gz',
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.entity-data-revision-metadata-fields-2248983.php',
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.views-revision-metadata-fields-2248983.php',
];
}
/**
* Tests that the revision metadata fields are moved correctly.
*/
public function testSystemUpdate3000() {
$this->runUpdates();
foreach (['entity_test_revlog', 'entity_test_mul_revlog'] as $entity_type_id) {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
$entity_type = $storage->getEntityType();
$revision_metadata_field_names = $entity_type->getRevisionMetadataKeys();
$database_schema = \Drupal::database()->schema();
// Test that the revision metadata fields are present only in the
// revision table.
foreach ($revision_metadata_field_names as $revision_metadata_field_name) {
if ($entity_type->isTranslatable()) {
$this->assertFalse($database_schema->fieldExists($entity_type->getDataTable(), $revision_metadata_field_name));
$this->assertFalse($database_schema->fieldExists($entity_type->getRevisionDataTable(), $revision_metadata_field_name));
}
else {
$this->assertFalse($database_schema->fieldExists($entity_type->getBaseTable(), $revision_metadata_field_name));
}
$this->assertTrue($database_schema->fieldExists($entity_type->getRevisionTable(), $revision_metadata_field_name));
}
// Test that the revision metadata values have been transferred correctly
// and that the moved fields are accessible.
/** @var \Drupal\Core\Entity\RevisionLogInterface $entity_rev_first */
$entity_rev_first = $storage->loadRevision(1);
$this->assertEqual($entity_rev_first->getRevisionUserId(), '1');
$this->assertEqual($entity_rev_first->getRevisionLogMessage(), 'first revision');
$this->assertEqual($entity_rev_first->getRevisionCreationTime(), '1476268517');
/** @var \Drupal\Core\Entity\RevisionLogInterface $entity_rev_second */
$entity_rev_second = $storage->loadRevision(2);
$this->assertEqual($entity_rev_second->getRevisionUserId(), '1');
$this->assertEqual($entity_rev_second->getRevisionLogMessage(), 'second revision');
$this->assertEqual($entity_rev_second->getRevisionCreationTime(), '1476268518');
// Test that the views using revision metadata fields are updated
// properly.
$view = View::load($entity_type_id . '_for_2248983');
$displays = $view->get('display');
foreach ($displays as $display => $display_data) {
foreach ($display_data['display_options']['fields'] as $property_data) {
if (in_array($property_data['field'], $revision_metadata_field_names)) {
$this->assertEqual($property_data['table'], $entity_type->getRevisionTable());
}
}
}
}
}
}
......@@ -12,6 +12,9 @@
use Drupal\Core\Path\AliasStorage;
use Drupal\Core\Url;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PrivateStream;
......@@ -1831,6 +1834,141 @@ function system_update_8301() {
->save();
}
/**
* Move revision metadata fields to the revision table.
*/
function system_update_8302(&$sandbox) {
// Due to the fields from RevisionLogEntityTrait not being explicitly
// mentioned in the storage they might have been installed wrongly in the base
// table for revisionable untranslatable entities and in the data and revision
// data tables for revisionable and translatable entities.
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$database = \Drupal::database();
$database_schema = $database->schema();
if (!isset($sandbox['current'])) {
// This must be the first run. Initialize the sandbox.
$sandbox['current'] = 0;
$definitions = array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) use ($entity_definition_update_manager) {
if ($entity_type = $entity_definition_update_manager->getEntityType($entity_type->id())) {
return is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && ($entity_type instanceof ContentEntityTypeInterface) && $entity_type->isRevisionable();
}
return FALSE;
});
$sandbox['entity_type_ids'] = array_keys($definitions);
$sandbox['max'] = count($sandbox['entity_type_ids']);
}
$current_entity_type_key = $sandbox['current'];
for ($i = $current_entity_type_key; ($i < $current_entity_type_key + 1) && ($i < $sandbox['max']); $i++) {
$entity_type_id = $sandbox['entity_type_ids'][$i];
/** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
$entity_type = $entity_definition_update_manager->getEntityType($entity_type_id);
$base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($entity_type_id);
$revision_metadata_fields = $entity_type->getRevisionMetadataKeys();
$fields_to_update = array_intersect_key($base_fields, array_flip($revision_metadata_fields));
if (!empty($fields_to_update)) {
// Initialize the entity table names.
// @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout()
$base_table = $entity_type->getBaseTable() ?: $entity_type_id;
$data_table = $entity_type->getDataTable() ?: $entity_type_id . '_field_data';
$revision_table = $entity_type->getRevisionTable() ?: $entity_type_id . '_revision';
$revision_data_table = $entity_type->getRevisionDataTable() ?: $entity_type_id . '_field_revision';
$revision_field = $entity_type->getKey('revision');
// No data needs to be migrated if the entity type is not translatable.
if ($entity_type->isTranslatable()) {
if (!isset($sandbox[$entity_type_id])) {
// This must be the first run for this entity type. Initialize the
// sub-sandbox for it.
// Calculate the number of revisions to process.
$count = \Drupal::entityQuery($entity_type_id)
->allRevisions()
->count()
->accessCheck(FALSE)
->execute();
$sandbox[$entity_type_id]['current'] = 0;
$sandbox[$entity_type_id]['max'] = $count;
}
// Define the step size.
$steps = Settings::get('entity_update_batch_size', 50);
// Collect the revision IDs to process.
$revisions = \Drupal::entityQuery($entity_type_id)
->allRevisions()
->range($sandbox[$entity_type_id]['current'], $sandbox[$entity_type_id]['current'] + $steps)
->sort($revision_field, 'ASC')
->accessCheck(FALSE)
->execute();
$revisions = array_keys($revisions);
foreach ($fields_to_update as $revision_metadata_field_name => $definition) {
// If the revision metadata field is present in the data and the
// revision data table, install its definition again with the updated
// storage code in order for the field to be installed in the
// revision table. Afterwards, copy over the field values and remove
// the field from the data and the revision data tables.
if ($database_schema->fieldExists($data_table, $revision_metadata_field_name) && $database_schema->fieldExists($revision_data_table, $revision_metadata_field_name)) {
// Install the field in the revision table.
if (!isset($sandbox[$entity_type_id]['storage_definition_installed'][$revision_metadata_field_name])) {
$entity_definition_update_manager->installFieldStorageDefinition($revision_metadata_field_name, $entity_type_id, $entity_type->getProvider(), $definition);
$sandbox[$entity_type_id]['storage_definition_installed'][$revision_metadata_field_name] = TRUE;
}
// Apply the field value from the revision data table to the
// revision table.
foreach ($revisions as $rev_id) {
$field_value = $database->select($revision_data_table, 't')
->fields('t', [$revision_metadata_field_name])
->condition($revision_field, $rev_id)
->execute()
->fetchField();
$database->update($revision_table)
->condition($revision_field, $rev_id)
->fields([$revision_metadata_field_name => $field_value])
->execute();
}
}
}
$sandbox[$entity_type_id]['current'] += count($revisions);
$sandbox[$entity_type_id]['finished'] = ($sandbox[$entity_type_id]['current'] == $sandbox[$entity_type_id]['max']) || empty($revisions);
if ($sandbox[$entity_type_id]['finished']) {
foreach ($fields_to_update as $revision_metadata_field_name => $definition) {
// Drop the field from the data and revision data tables.
$database_schema->dropField($data_table, $revision_metadata_field_name);
$database_schema->dropField($revision_data_table, $revision_metadata_field_name);
}
$sandbox['current']++;
}
}
else {
foreach ($fields_to_update as $revision_metadata_field_name => $definition) {
if ($database_schema->fieldExists($base_table, $revision_metadata_field_name)) {
// Install the field in the revision table.
$entity_definition_update_manager->installFieldStorageDefinition($revision_metadata_field_name, $entity_type_id, $entity_type->getProvider(), $definition);
// Drop the field from the base table.
$database_schema->dropField($base_table, $revision_metadata_field_name);
}
}
$sandbox['current']++;
}
}
else {
$sandbox['current']++;
}
}
$sandbox['#finished'] = $sandbox['current'] == $sandbox['max'];
}
/**
* @} End of "addtogroup updates-8.3.0".
*/
<?php
/**
* @file
* Contains database additions to
* drupal-8.2.1.bare.standard_with_entity_test_enabled.php.gz for testing the
* upgrade path of https://www.drupal.org/node/2248983.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Data for entity type "entity_test_revlog"
$connection->insert('entity_test_revlog')
->fields([
'id',
'revision_id',
'type',
'uuid',
'langcode',
'revision_created',
'revision_user',
'revision_log_message',
'name',
])
->values([
'id' => '1',
'revision_id' => '2',
'type' => 'entity_test_revlog',
'uuid' => 'f0b962b1-391b-441b-a664-2468ad520d96',
'langcode' => 'en',
'revision_created' => '1476268518',
'revision_user' => '1',
'revision_log_message' => 'second revision',
'name' => 'entity 1',
])
->execute();
$connection->insert('entity_test_revlog_revision')
->fields([
'id',
'revision_id',
'langcode',
'revision_created',
'revision_user',
'revision_log_message',
'name',
])
->values([
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
'revision_created' => '1476268517',
'revision_user' => '1',
'revision_log_message' => 'first revision',
'name' => 'entity 1',
])
->values([
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
'revision_created' => '1476268518',
'revision_user' => '1',
'revision_log_message' => 'second revision',
'name' => 'entity 1',
])
->execute();
// Data for entity type "entity_test_mul_revlog"
$connection->insert('entity_test_mul_revlog')
->fields([
'id',
'revision_id',
'type',
'uuid',
'langcode',
])
->values([
'id' => '1',
'revision_id' => '2',
'type' => 'entity_test_mul_revlog',
'uuid' => '6f04027a-1cbd-46e3-a67e-72636b493d4f',
'langcode' => 'en',
])
->execute();
$connection->insert('entity_test_mul_revlog_field_data')
->fields([
'id',
'revision_id',
'type',
'langcode',
'revision_created',
'revision_user',
'revision_log_message',
'name',
'default_langcode',
])
->values([
'id' => '1',
'revision_id' => '2',
'type' => 'entity_test_mul_revlog',
'langcode' => 'en',
'revision_created' => '1476268518',
'revision_user' => '1',
'revision_log_message' => 'second revision',
'name' => 'entity 1',
'default_langcode' => '1',
])
->execute();
$connection->insert('entity_test_mul_revlog_field_revision')
->fields([
'id',
'revision_id',
'langcode',
'revision_created',
'revision_user',
'revision_log_message',
'name',
'default_langcode',
])
->values([
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
'revision_created' => '1476268517',
'revision_user' => '1',
'revision_log_message' => 'first revision',
'name' => 'entity 1',
'default_langcode' => '1',
])
->values([
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
'revision_created' => '1476268518',
'revision_user' => '1',
'revision_log_message' => 'second revision',
'name' => 'entity 1',
'default_langcode' => '1',
])
->execute();
$connection->insert('entity_test_mul_revlog_revision')
->fields([
'id',
'revision_id',
'langcode',
])
->values([
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
])
->values([
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
])
->execute();
<?php
/**
* @file
* Contains database additions to
* drupal-8.2.1.bare.standard_with_entity_test_enabled.php.gz for testing the
* upgrade path of https://www.drupal.org/node/2248983.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();