diff --git a/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php b/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php new file mode 100644 index 0000000..2c5ca86 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/RevisionableSchemaConverter.php @@ -0,0 +1,241 @@ +lastInstalledSchemaRepository = $last_installed_schema_repository; + $this->entityTypeManager = $entity_type_manager; + $this->entityDefinitionUpdateManager = $entity_definition_update_manager; + $this->entityFieldManager = $entity_field_manager; + $this->database = $database; + } + + /** + * {@inheritdoc} + */ + public function convertSchema($entity_type_id, $options = []) { + $default_options = [ + 'revision' => 'revision_id', + 'revision_table' => $entity_type_id . '_revision', + 'revision_data_table' => $entity_type_id . '_field_revision', + 'revision_created' => 'revision_created', + 'revision_user' => 'revision_user', + 'revision_log_message' => 'revision_log_message', + 'langcode' => 'langcode', + ]; + $options = array_merge($default_options, $options); + $this->updateEntityType($entity_type_id, $options); + $this->createTables($entity_type_id, $options); + $this->installRevisionableFields($entity_type_id, $options); + } + + /** + * {@inheritdoc} + */ + public function copyData(EntityTypeInterface $entity_type, array &$sandbox) { + // If 'progress' is not set, then this will be the first run of the batch. + if (!isset($sandbox['progress'])) { + $base_table = $entity_type->getBaseTable(); + $sandbox['progress'] = 0; + $sandbox['current_id'] = 0; + $sandbox['max'] = $this->database->select($base_table) + ->countQuery() + ->execute() + ->fetchField(); + } + + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping */ + $table_mapping = $this->entityTypeManager->getStorage($entity_type->id())->getTableMapping(); + $table_names = $table_mapping->getTableNames(); + $id = $entity_type->getKey('id'); + $revision_id = $entity_type->getKey('revision'); + + $data = []; + // Loop through all tables for the entity type and combine the data. + foreach ($table_names as $table_name) { + $column_names = $table_mapping->getAllColumns($table_name); + // Process 5 entities per batch. + $results = $this->database->select($table_name, 't') + ->fields('t') + ->condition($id, $sandbox['current_id'], '>') + ->range(0, 5) + ->orderBy($id) + ->execute() + ->fetchAll(); + foreach ($results as $key => $result) { + foreach ($column_names as $column_name) { + if (!empty($result->{$column_name})) { + $data[$key][$column_name] = $result->{$column_name}; + } + } + } + } + + // Loop through all the collected data and update / insert missing rows. + foreach ($data as $record) { + if (!empty($record[$id])) { + $record[$revision_id] = $record[$id]; + } + foreach ($table_names as $table_name) { + $values = []; + $column_names = $table_mapping->getAllColumns($table_name); + foreach ($column_names as $column_name) { + if (!empty($record[$column_name])) { + $values[$column_name] = $record[$column_name]; + } + } + $this->database->upsert($table_name)->key('id')->fields($values)->execute(); + } + $sandbox['progress']++; + $sandbox['current_id'] = $record['id']; + } + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + } + + /** + * Updates installed entity type definition. + * + * @param string $entity_type_id + * ID of the entity type to update. + * @param array $options + * Options to update the entity type with. + */ + protected function updateEntityType($entity_type_id, $options) { + $last_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id); + $keys = $last_entity_type->getKeys(); + $keys['revision'] = $options['revision']; + $last_entity_type->set('entity_keys', $keys); + $last_entity_type->set('revision_table', $options['revision_table']); + $last_entity_type->set('revision_data_table', $options['revision_data_table']); + $this->lastInstalledSchemaRepository->setLastInstalledDefinition($last_entity_type); + } + + /** + * Creates missing tables. + * + * @param string $entity_type_id + * ID of the entity type to update. + */ + protected function createTables($entity_type_id) { + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $entity_type = $this->entityDefinitionUpdateManager->getEntityType($entity_type_id); + if ($storage instanceof DynamicallyFieldableEntityStorageSchemaInterface) { + $storage->entityTypeResolveMissingSchema($entity_type); + } + } + + /** + * Installs new fields. + * + * @param string $entity_type_id + * ID of the entity type to update. + * @param array $options + * Options to update the entity type with. + */ + protected function installRevisionableFields($entity_type_id, $options) { + $revision_id = BaseFieldDefinition::create('integer') + ->setLabel(new TranslatableMarkup('Revision ID')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + $this->entityDefinitionUpdateManager + ->installFieldStorageDefinition($options['revision'], $entity_type_id, $entity_type_id, $revision_id); + + $revision_created = BaseFieldDefinition::create('created') + ->setLabel(t('Revision create time')) + ->setDescription(t('The time that the current revision was created.')) + ->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager + ->installFieldStorageDefinition($options['revision_created'], $entity_type_id, $entity_type_id, $revision_created); + + $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); + $this->entityDefinitionUpdateManager + ->installFieldStorageDefinition($options['revision_user'], $entity_type_id, $entity_type_id, $revision_user); + + $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, + ], + ]); + $this->entityDefinitionUpdateManager + ->installFieldStorageDefinition($options['revision_log_message'], $entity_type_id, $entity_type_id, $revision_log_message); + + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */ + $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + foreach ($field_definitions as $field_definition) { + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($field_definition); + } + + $schema = $this->database->schema(); + $schema->dropPrimaryKey($options['revision_data_table']); + $schema->addPrimaryKey($options['revision_data_table'], [$options['revision'], $options['langcode']]); + } + +} diff --git a/core/lib/Drupal/Core/Entity/RevisionableSchemaConverterInterface.php b/core/lib/Drupal/Core/Entity/RevisionableSchemaConverterInterface.php new file mode 100644 index 0000000..7239af4 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/RevisionableSchemaConverterInterface.php @@ -0,0 +1,29 @@ +wrapSchemaException(function () use ($entity_type) { + $this->getStorageSchema()->entityTypeResolveMissingSchema($entity_type); + }); + } + + /** + * {@inheritdoc} + */ public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) { return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original); } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index ec3ebaf..93de0de 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -227,6 +227,39 @@ protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterfac /** * {@inheritdoc} */ + public function entityTypeResolveMissingSchema(EntityTypeInterface $entity_type) { + $this->checkEntityType($entity_type); + $schema_handler = $this->database->schema(); + + // Create entity tables. + $schema = $this->getEntitySchema($entity_type, TRUE); + foreach ($schema as $table_name => $table_schema) { + if (!$schema_handler->tableExists($table_name)) { + $schema_handler->createTable($table_name, $table_schema); + } + } + + // Create dedicated field tables. + $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type->id()); + $table_mapping = $this->storage->getTableMapping($field_storage_definitions); + foreach ($field_storage_definitions as $field_storage_definition) { + if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { + $this->createDedicatedTableSchema($field_storage_definition); + } + elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) { + // The shared tables are already fully created, but we need to save the + // per-field schema definitions for later use. + $this->createSharedTableSchema($field_storage_definition, TRUE); + } + } + + // Save data about entity indexes and keys. + $this->saveEntitySchemaData($entity_type, $schema); + } + + /** + * {@inheritdoc} + */ public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { // If the original storage has existing entities, or it is impossible to // determine if that is the case, require entity data to be migrated. @@ -1138,6 +1171,12 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names); if (!$only_save) { foreach ($schema[$table_name]['fields'] as $name => $specifier) { + // Use the value of the entity ID as the initial value for the + // revision ID field. + if($created_field_name == $this->entityType->getKey('revision')) { + $id_column_name = $table_mapping->getColumnNames($this->entityType->getKey('id')); + $specifier['initial_from_field'] = reset($id_column_name); + } // Check if the field exists because it might already have been // created as part of the earlier entity type update event. if (!$schema_handler->fieldExists($table_name, $name)) { diff --git a/core/modules/shortcut/shortcut.install b/core/modules/shortcut/shortcut.install index 8df408f..dbed0f5 100644 --- a/core/modules/shortcut/shortcut.install +++ b/core/modules/shortcut/shortcut.install @@ -67,3 +67,40 @@ function shortcut_uninstall() { \Drupal::configFactory()->getEditable('seven.settings')->clear('third_party_settings.shortcut')->save(TRUE); } } + +/** + * Convert Shortcut entity schema. + */ +function shortcut_update_8001() { + $schema_converter = new \Drupal\Core\Entity\RevisionableSchemaConverter( + \Drupal::service('entity.last_installed_schema.repository'), + \Drupal::service('entity_type.manager'), + \Drupal::service('entity.definition_update_manager'), + \Drupal::service('entity_field.manager'), + \Drupal::service('database') + ); + $schema_converter->convertSchema('shortcut', [ + 'revision' => 'revision_id', + 'revision_table' => 'shortcut_revision', + 'revision_data_table' => 'shortcut_field_revision', + 'revision_created' => 'revision_created', + 'revision_user' => 'revision_user', + 'revision_log_message' => 'revision_log_message', + 'langcode' => 'langcode', + ]); +} + +/** + * Copy Shortcut entity data to new schema. + */ +function shortcut_update_8002(&$sandbox) { + $entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('shortcut'); + $schema_converter = new \Drupal\Core\Entity\RevisionableSchemaConverter( + \Drupal::service('entity.last_installed_schema.repository'), + \Drupal::service('entity_type.manager'), + \Drupal::service('entity.definition_update_manager'), + \Drupal::service('entity_field.manager'), + \Drupal::service('database') + ); + $schema_converter->copyData($entity_type, $sandbox); +} diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php index 6ddaf05..9f60fe0 100644 --- a/core/modules/shortcut/src/Entity/Shortcut.php +++ b/core/modules/shortcut/src/Entity/Shortcut.php @@ -3,9 +3,9 @@ namespace Drupal\shortcut\Entity; use Drupal\Core\Cache\Cache; -use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\RevisionableContentEntityBase; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\link\LinkItemInterface; use Drupal\shortcut\ShortcutInterface; @@ -30,9 +30,12 @@ * }, * base_table = "shortcut", * data_table = "shortcut_field_data", + * revision_table = "shortcut_revision", + * revision_data_table = "shortcut_field_revision", * translatable = TRUE, * entity_keys = { * "id" = "id", + * "revision" = "revision_id", * "uuid" = "uuid", * "bundle" = "shortcut_set", * "label" = "title", @@ -47,7 +50,7 @@ * bundle_entity_type = "shortcut_set" * ) */ -class Shortcut extends ContentEntityBase implements ShortcutInterface { +class Shortcut extends RevisionableContentEntityBase implements ShortcutInterface { /** * {@inheritdoc} @@ -122,6 +125,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(t('The name of the shortcut.')) ->setRequired(TRUE) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) ->setSetting('max_length', 255) ->setDisplayOptions('form', array( 'type' => 'string_textfield', @@ -139,6 +143,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Path')) ->setDescription(t('The location this shortcut points to.')) ->setRequired(TRUE) + ->setRevisionable(TRUE) ->setSettings(array( 'link_type' => LinkItemInterface::LINK_INTERNAL, 'title' => DRUPAL_DISABLED, diff --git a/core/modules/shortcut/tests/src/Kernel/ShortcutSevenIntegrationTest.php b/core/modules/shortcut/tests/src/Kernel/ShortcutSevenIntegrationTest.php index 0d58c31..c8517ce 100644 --- a/core/modules/shortcut/tests/src/Kernel/ShortcutSevenIntegrationTest.php +++ b/core/modules/shortcut/tests/src/Kernel/ShortcutSevenIntegrationTest.php @@ -21,6 +21,7 @@ public function testInstallUninstall() { \Drupal::service('theme_installer')->install(['seven']); $this->assertNull($this->config('seven.settings')->get('third_party_settings.shortcut'), 'There are no shortcut settings in seven.settings.'); + \Drupal::service('module_installer')->install(['user']); \Drupal::service('module_installer')->install(['shortcut']); $this->assertTrue($this->config('seven.settings')->get('third_party_settings.shortcut.module_link'), 'The shortcut module_link setting is in seven.settings.'); diff --git a/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php b/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php new file mode 100644 index 0000000..b244252 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php @@ -0,0 +1,51 @@ +databaseDumpFiles = [__DIR__ . '/../../../../../modules/system/tests/fixtures/update/drupal-8.filled.standard.php.gz']; + } + + /** + * Tests the entity type is revisionable and shortcut entities are accessible. + */ + public function testMakeRevisionable() { + $this->runUpdates(); + + $entity_type_manager = $this->container->get('entity_type.manager'); + $shortcut = $entity_type_manager->getStorage('shortcut')->getEntityType(); + + /** @var ContentEntityInterface $last_installed_shortcut */ + $last_installed_shortcut = $this->container->get('entity.last_installed_schema.repository')->getLastInstalledDefinition($shortcut->id()); + $this->assertTrue($last_installed_shortcut->isRevisionable()); + + $shortcut_storage = $entity_type_manager->getStorage($shortcut->id()); + $shortcut_entities = $shortcut_storage->loadMultiple(); + $this->assertEqual(count($shortcut_entities), 4, "Four Shortcut entities found"); + + /** @var \Drupal\shortcut\ShortcutInterface $revision_one */ + $revision_one = $shortcut_storage->loadRevision(1); + $this->assertEqual(1, $revision_one->getRevisionId(), "Revision 1 is revision 1"); + + $revision_one->setNewRevision(TRUE); + $revision_one->save(); + $this->assertEqual(5, $revision_one->getRevisionId(), "New shortcut revision created"); + } + +}