Commit db4cb40e authored by catch's avatar catch

Issue #2860654 by amateescu: Field storage CRUD operations must use the last...

Issue #2860654 by amateescu: Field storage CRUD operations must use the last installed entity type and field storage definitions
parent 54e77b68
......@@ -5,6 +5,7 @@
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
......@@ -69,7 +70,17 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$storage = clone $this->entityTypeManager->getStorage($entity_type_id);
// Entity type definition updates can change the schema by adding or
// removing entity tables (for example when switching an entity type from
// non-revisionable to revisionable), so CRUD operations on a field storage
// definition need to use the last installed entity type schema.
if ($storage instanceof SqlContentEntityStorage
&& ($last_installed_entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id))) {
$storage->setEntityType($last_installed_entity_type);
}
if ($storage instanceof FieldStorageDefinitionListenerInterface) {
$storage->onFieldStorageDefinitionCreate($storage_definition);
}
......@@ -88,7 +99,17 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$storage = clone $this->entityTypeManager->getStorage($entity_type_id);
// Entity type definition updates can change the schema by adding or
// removing entity tables (for example when switching an entity type from
// non-revisionable to revisionable), so CRUD operations on a field storage
// definition need to use the last installed entity type schema.
if ($storage instanceof SqlContentEntityStorage
&& ($last_installed_entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id))) {
$storage->setEntityType($last_installed_entity_type);
}
if ($storage instanceof FieldStorageDefinitionListenerInterface) {
$storage->onFieldStorageDefinitionUpdate($storage_definition, $original);
}
......@@ -107,7 +128,17 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$storage = clone $this->entityTypeManager->getStorage($entity_type_id);
// Entity type definition updates can change the schema by adding or
// removing entity tables (for example when switching an entity type from
// non-revisionable to revisionable), so CRUD operations on a field storage
// definition need to use the last installed entity type schema.
if ($storage instanceof SqlContentEntityStorage
&& ($last_installed_entity_type = $this->entityLastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id))) {
$storage->setEntityType($last_installed_entity_type);
}
if ($storage instanceof FieldStorageDefinitionListenerInterface) {
$storage->onFieldStorageDefinitionDelete($storage_definition);
}
......
......@@ -184,3 +184,14 @@ function comment_update_8301() {
$entity_type->set('entity_keys', $keys);
$definition_update_manager->updateEntityType($entity_type);
}
/**
* Update the status field.
*/
function comment_update_8400() {
// The status field was promoted to an entity key in comment_update_8301(),
// which makes it NOT NULL in the default SQL storage, which means its storage
// definition needs to be updated as well.
$entity_definition_update_manager = \Drupal::service('entity.definition_update_manager');
$entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('status', 'comment'));
}
......@@ -18,13 +18,14 @@ class MigrateBundleTest extends MigrateTestBase {
*
* @var array
*/
public static $modules = ['taxonomy', 'text'];
public static $modules = ['taxonomy', 'text', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
......
......@@ -17,13 +17,14 @@ class MigrateRollbackEntityConfigTest extends MigrateTestBase {
*
* @var array
*/
public static $modules = ['field', 'taxonomy', 'text', 'language', 'config_translation'];
public static $modules = ['field', 'taxonomy', 'text', 'language', 'config_translation', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
......
......@@ -20,13 +20,14 @@ class MigrateRollbackTest extends MigrateTestBase {
*
* @var array
*/
public static $modules = ['field', 'taxonomy', 'text'];
public static $modules = ['field', 'taxonomy', 'text', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
......
<?php
namespace Drupal\system\Tests\Update;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\system\Tests\Entity\EntityDefinitionTestTrait;
/**
* Tests the upgrade path for making an entity revisionable and publishable.
*
* @see https://www.drupal.org/node/2841291
*
* @group Update
*/
class EntityUpdateToRevisionableAndPublishableTest extends UpdatePathTestBase {
use EntityDefinitionTestTrait;
use DbUpdatesTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $entityDefinitionUpdateManager;
/**
* The last installed schema repository service.
*
* @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
*/
protected $lastInstalledSchemaRepository;
/**
* The key-value collection for tracking installed storage schema.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $installedStorageSchema;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
$this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository');
$this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
$this->state = \Drupal::state();
}
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz',
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php',
];
}
/**
* Tests the conversion of an entity type to revisionable and publishable.
*/
public function testConvertToRevisionableAndPublishable() {
// Check that entity type is not revisionable nor publishable prior to
// running the update process.
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$this->assertFalse($entity_test_update->isRevisionable());
$this->assertFalse($entity_test_update->getKey('published'));
// Make the entity type revisionable, translatable and publishable.
$this->updateEntityTypeDefinition();
$this->enableUpdates('entity_test_update', 'entity_rev_pub_updates', 8400);
$this->runUpdates();
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_update */
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$this->assertTrue($entity_test_update->isRevisionable());
$this->assertEqual('status', $entity_test_update->getKey('published'));
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
$this->assertEqual(count($storage->loadMultiple()), 102, 'All test entities were found.');
// The conversion to revisionable is already tested by
// \Drupal\system\Tests\Entity\Update\SqlContentEntityStorageSchemaConverterTest::testMakeRevisionable()
// so we only need to check that some special cases are handled.
// All the checks implemented here are taking into consideration the special
// conditions in which the test database was created.
// @see _entity_test_update_create_test_entities()
// The test entity with ID 50 was created before Content Translation was
// enabled, which means it didn't have a 'content_translation_status' field.
// content_translation_update_8400() added values for that field which
// should now be reflected in the entity's 'status' field.
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision = $storage->loadRevision(50);
$this->assertEqual(1, $revision->status->value);
$translation = $revision->getTranslation('ro');
$this->assertEqual(1, $translation->status->value);
// The test entity with ID 100 was created with Content Translation enabled
// and it should have the same values as entity 50.
$revision = $storage->loadRevision(100);
$this->assertEqual(1, $revision->status->value);
$translation = $revision->getTranslation('ro');
$this->assertEqual(1, $translation->status->value);
// The test entity 101 had 'content_translation_status' set to 0 for the
// English (source) language.
$revision = $storage->loadRevision(101);
$this->assertEqual(0, $revision->status->value);
$translation = $revision->getTranslation('ro');
$this->assertEqual(1, $translation->status->value);
// The test entity 102 had 'content_translation_status' set to 0 for the
// Romanian language.
$revision = $storage->loadRevision(102);
$this->assertEqual(1, $revision->status->value);
$translation = $revision->getTranslation('ro');
$this->assertEqual(0, $translation->status->value);
}
/**
* Updates the 'entity_test_update' entity type to revisionable,
* translatable, publishable and adds revision metadata keys.
*/
protected function updateEntityTypeDefinition() {
$entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
$keys = $entity_type->getKeys();
$keys['revision'] = 'revision_id';
$keys['published'] = 'status';
$entity_type->set('entity_keys', $keys);
$revision_metadata_keys = [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message'
];
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
$entity_type->set('translatable', TRUE);
$entity_type->set('data_table', 'entity_test_update_data');
$entity_type->set('revision_table', 'entity_test_update_revision');
$entity_type->set('revision_data_table', 'entity_test_update_revision_data');
$this->state->set('entity_test_update.entity_type', $entity_type);
// Also add the status and revision metadata base fields to the entity type.
$status = BaseFieldDefinition::create('boolean')
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating the published state.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setRequired(TRUE)
->setDefaultValue(TRUE);
$revision_created = BaseFieldDefinition::create('created')
->setLabel(t('Revision create time'))
->setDescription(t('The time that the current revision was created.'))
->setRevisionable(TRUE);
$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);
$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->state->set('entity_test_update.additional_base_field_definitions', [
'status' => $status,
'revision_created' => $revision_created,
'revision_user' => $revision_user,
'revision_log_message' => $revision_log_message,
]);
$this->entityTypeManager->clearCachedDefinitions();
}
}
<?php
/**
* @file
* Install, update and uninstall functions for the Entity Test Update module.
*/
use Drupal\system\Tests\Update\DbUpdatesTrait;
DbUpdatesTrait::includeUpdates('entity_test_update', 'entity_rev_pub_updates');
<?php
/**
* @file
* Defines the 8400 db update for the "entity_rev_pub_updates" group.
*/
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Implements hook_update_dependencies().
*/
function entity_test_update_update_dependencies() {
// The update function that adds the status field must run after
// content_translation_update_8400() which fixes NULL values for the
// 'content_translation_status' field.
$dependencies['entity_test_update'][8400] = [
'content_translation' => 8400,
];
return $dependencies;
}
/**
* Add the 'published' and revisionable metadata fields to entity_test_update.
*/
function entity_test_update_update_8400() {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
// Add the published entity key and revisionable metadata fields to the
// entity_test_update entity type.
$entity_type = $definition_update_manager->getEntityType('entity_test_update');
$entity_keys = $entity_type->getKeys();
$entity_keys['published'] = 'status';
$entity_type->set('entity_keys', $entity_keys);
$revision_metadata_keys = [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message'
];
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
$definition_update_manager->updateEntityType($entity_type);
// Add the status field.
$status = BaseFieldDefinition::create('boolean')
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating the published state.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE);
$has_content_translation_status_field = \Drupal::moduleHandler()->moduleExists('content_translation') && $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'entity_test_update');
if ($has_content_translation_status_field) {
$status->setInitialValueFromField('content_translation_status');
}
else {
$status->setInitialValue(TRUE);
}
$definition_update_manager->installFieldStorageDefinition('status', 'entity_test_update', 'entity_test_update', $status);
// Add the revision metadata fields.
$revision_created = BaseFieldDefinition::create('created')
->setLabel(t('Revision create time'))
->setDescription(t('The time that the current revision was created.'))
->setRevisionable(TRUE);
$definition_update_manager->installFieldStorageDefinition('revision_created', 'entity_test_update', 'entity_test_update', $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);
$definition_update_manager->installFieldStorageDefinition('revision_user', 'entity_test_update', 'entity_test_update', $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,
],
]);
$definition_update_manager->installFieldStorageDefinition('revision_log_message', 'entity_test_update', 'entity_test_update', $revision_log_message);
// Uninstall the 'content_translation_status' field if needed.
$database = \Drupal::database();
if ($has_content_translation_status_field) {
// First we have to remove the field data.
$database->update($entity_type->getDataTable())
->fields(['content_translation_status' => NULL])
->execute();
// A site may have disabled revisionability for this entity type.
if ($entity_type->isRevisionable()) {
$database->update($entity_type->getRevisionDataTable())
->fields(['content_translation_status' => NULL])
->execute();
}
$content_translation_status = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'entity_test_update');
$definition_update_manager->uninstallFieldStorageDefinition($content_translation_status);
}
}
......@@ -777,6 +777,11 @@ public function testBaseFieldEntityKeyUpdateWithExistingData() {
// of a NOT NULL constraint.
$this->makeBaseFieldEntityKey();
// Field storage CRUD operations use the last installed entity type
// definition so we need to update it before doing any other field storage
// updates.
$this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type'));
// Try to apply the update and verify they fail since we have a NULL value.
$message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
try {
......
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