Commit 3148601b authored by catch's avatar catch

Issue #2984782 by amateescu, plach, catch, jibran, kristiaanvandeneynde: Add...

Issue #2984782 by amateescu, plach, catch, jibran, kristiaanvandeneynde: Add an API for converting entity type schemas to (non-)revisionable/(non-)translatable, with or without data
parent ca8a550c
......@@ -241,6 +241,20 @@ public function uninstallEntityType(EntityTypeInterface $entity_type) {
$this->entityTypeListener->onEntityTypeDelete($entity_type);
}
/**
* {@inheritdoc}
*/
public function updateFieldableEntityType(EntityTypeInterface $entity_type, array $field_storage_definitions, array &$sandbox = NULL) {
$original = $this->getEntityType($entity_type->id());
if ($this->requiresEntityDataMigration($entity_type, $original) && $sandbox === NULL) {
throw new \InvalidArgumentException('The entity schema update for the ' . $entity_type->id() . ' entity type requires a data migration.');
}
$original_field_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
$this->entityTypeListener->onFieldableEntityTypeUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
}
/**
* {@inheritdoc}
*/
......@@ -442,6 +456,22 @@ protected function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInter
return ($storage instanceof DynamicallyFieldableEntityStorageSchemaInterface) && $storage->requiresFieldStorageSchemaChanges($storage_definition, $original);
}
/**
* Checks if existing data would be lost if the schema changes were applied.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The updated entity type definition.
* @param \Drupal\Core\Entity\EntityTypeInterface $original
* The original entity type definition.
*
* @return bool
* TRUE if data migration is required, FALSE otherwise.
*/
protected function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
$storage = $this->entityTypeManager->getStorage($entity_type->id());
return ($storage instanceof EntityStorageSchemaInterface) && $storage->requiresEntityDataMigration($entity_type, $original);
}
/**
* Clears necessary caches to apply entity/field definition updates.
*/
......
......@@ -144,6 +144,20 @@ public function updateEntityType(EntityTypeInterface $entity_type);
*/
public function uninstallEntityType(EntityTypeInterface $entity_type);
/**
* Applies any change performed to a fieldable entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The updated entity type definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
* The updated field storage definitions, including possibly new ones.
* @param array &$sandbox
* (optional) A sandbox array provided by a hook_update_N() implementation
* or a Batch API callback. If the entity schema update requires a data
* migration, this parameter is mandatory. Defaults to NULL.
*/
public function updateFieldableEntityType(EntityTypeInterface $entity_type, array $field_storage_definitions, array &$sandbox = NULL);
/**
* Returns a field storage definition ready to be manipulated.
*
......
......@@ -548,6 +548,19 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
$this->container->get('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
}
/**
* {@inheritdoc}
*
* @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0.
* Use \Drupal\Core\Entity\EntityTypeListenerInterface::onFieldableEntityTypeUpdate()
* instead.
*
* @see https://www.drupal.org/project/drupal/issues/2984782
*/
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
$this->container->get('entity_type.listener')->onFieldableEntityTypeUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
}
/**
* {@inheritdoc}
*
......
......@@ -533,6 +533,17 @@ protected function doPostSave(EntityInterface $entity, $update) {
unset($entity->original);
}
/**
* {@inheritdoc}
*/
public function restore(EntityInterface $entity) {
// Allow code to run before saving.
$entity->preSave($this);
// The restore process does not invoke any post-save operations.
$this->doSave($entity->id(), $entity);
}
/**
* Builds an entity query.
*
......
......@@ -154,6 +154,23 @@ public function delete(array $entities);
*/
public function save(EntityInterface $entity);
/**
* Restores a previously saved entity.
*
* Note that the entity is assumed to be in a valid state for the storage, so
* the restore process does not invoke any hooks, nor does it perform any
* post-save operations.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to restore.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures, an exception is thrown.
*
* @internal
*/
public function restore(EntityInterface $entity);
/**
* Determines if the storage contains any data.
*
......
......@@ -65,6 +65,12 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
}
/**
* {@inheritdoc}
*/
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
}
/**
* {@inheritdoc}
*/
......
......@@ -115,4 +115,27 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->entityLastInstalledSchemaRepository->deleteLastInstalledDefinition($entity_type_id);
}
/**
* {@inheritdoc}
*/
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
$entity_type_id = $entity_type->id();
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->entityTypeManager->createHandlerInstance($entity_type->getStorageClass(), $entity_type);
if ($storage instanceof EntityTypeListenerInterface) {
$storage->onFieldableEntityTypeUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
}
if ($sandbox === NULL || (isset($sandbox['#finished']) && $sandbox['#finished'] == 1)) {
$this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original));
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($entity_type_id, $field_storage_definitions);
}
}
}
}
......@@ -25,6 +25,24 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type);
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original);
/**
* Reacts to the update of a fieldable entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The updated entity type definition.
* @param \Drupal\Core\Entity\EntityTypeInterface $original
* The original entity type definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
* The updated field storage definitions, including possibly new ones.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions
* The original field storage definitions.
* @param array &$sandbox
* (optional) A sandbox array provided by a hook_update_N() implementation
* or a Batch API callback. If the entity schema update requires a data
* migration, this parameter is mandatory. Defaults to NULL.
*/
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL);
/**
* Reacts to the deletion of the entity type.
*
......
......@@ -787,6 +787,57 @@ public function save(EntityInterface $entity) {
}
}
/**
* {@inheritdoc}
*/
public function restore(EntityInterface $entity) {
$transaction = $this->database->startTransaction();
try {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
// Allow code to run before saving.
$entity->preSave($this);
$this->invokeFieldMethod('preSave', $entity);
// Insert the entity data in the base and data tables only for default
// revisions.
if ($entity->isDefaultRevision()) {
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable);
$this->database
->insert($this->baseTable)
->fields((array) $record)
->execute();
if ($this->dataTable) {
$this->saveToSharedTables($entity);
}
}
// Insert the entity data in the revision and revision data tables.
if ($this->revisionTable) {
$record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
$this->database
->insert($this->revisionTable)
->fields((array) $record)
->execute();
if ($this->revisionDataTable) {
$this->saveToSharedTables($entity, $this->revisionDataTable);
}
}
// Insert the entity data in the dedicated tables.
$this->saveToDedicatedTables($entity, FALSE, []);
// Ignore replica server temporarily.
\Drupal::service('database.replica_kill_switch')->trigger();
}
catch (\Exception $e) {
$transaction->rollBack();
watchdog_exception($this->entityTypeId, $e);
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
......@@ -1425,6 +1476,15 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
});
}
/**
* {@inheritdoc}
*/
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
$this->wrapSchemaException(function () use ($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, &$sandbox) {
$this->getStorageSchema()->onFieldableEntityTypeUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
});
}
/**
* {@inheritdoc}
*/
......
......@@ -44,7 +44,7 @@ public function preSave() {
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
$original = $entity->original;
$langcode = $entity->language()->getId();
if (!$entity->isNew() && $original->hasTranslation($langcode)) {
if (!$entity->isNew() && $original && $original->hasTranslation($langcode)) {
$original_value = $original->getTranslation($langcode)->get($this->getFieldDefinition()->getName())->value;
if ($this->value == $original_value && $entity->hasTranslationChanges()) {
$this->value = REQUEST_TIME;
......
......@@ -63,6 +63,13 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
$this->storeEvent(EntityTypeEvents::UPDATE);
}
/**
* {@inheritdoc}
*/
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
$this->storeEvent(EntityTypeEvents::UPDATE);
}
/**
* {@inheritdoc}
*/
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\system\Functional\Entity\Traits;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\entity_test\FieldStorageDefinition;
/**
......@@ -294,4 +295,131 @@ protected function deleteEntityType() {
$this->state->set('entity_test_update.entity_type', 'null');
}
/**
* Returns an entity type definition, possibly updated to be rev or mul.
*
* @param bool $revisionable
* (optional) Whether the entity type should be revisionable or not.
* Defaults to FALSE.
* @param bool $translatable
* (optional) Whether the entity type should be translatable or not.
* Defaults to FALSE.
*
* @return \Drupal\Core\Entity\EntityTypeInterface
* An entity type definition.
*/
protected function getUpdatedEntityTypeDefinition($revisionable = FALSE, $translatable = FALSE) {
$entity_type = clone $this->entityManager->getDefinition('entity_test_update');
if ($revisionable) {
$keys = $entity_type->getKeys();
$keys['revision'] = 'revision_id';
$entity_type->set('entity_keys', $keys);
$entity_type->set('revision_table', 'entity_test_update_revision');
}
else {
$keys = $entity_type->getKeys();
$keys['revision'] = '';
$entity_type->set('entity_keys', $keys);
$entity_type->set('revision_table', NULL);
}
if ($translatable) {
$entity_type->set('translatable', TRUE);
$entity_type->set('data_table', 'entity_test_update_data');
}
else {
$entity_type->set('translatable', FALSE);
$entity_type->set('data_table', NULL);
}
if ($revisionable && $translatable) {
$entity_type->set('revision_data_table', 'entity_test_update_revision_data');
}
else {
$entity_type->set('revision_data_table', NULL);
}
$this->state->set('entity_test_update.entity_type', $entity_type);
$this->container->get('entity_type.manager')->clearCachedDefinitions();
$this->container->get('entity_type.bundle.info')->clearCachedBundles();
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
$this->container->get('entity_type.repository')->clearCachedDefinitions();
return $entity_type;
}
/**
* Returns the required rev / mul field definitions for an entity type.
*
* @param bool $revisionable
* (optional) Whether the entity type should be revisionable or not.
* Defaults to FALSE.
* @param bool $translatable
* (optional) Whether the entity type should be translatable or not.
* Defaults to FALSE.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
* An array of field storage definition objects.
*/
protected function getUpdatedFieldStorageDefinitions($revisionable = FALSE, $translatable = FALSE) {
$field_storage_definitions = $this->entityManager->getFieldStorageDefinitions('entity_test_update');
if ($revisionable) {
// The 'langcode' is already available for the 'entity_test_update' entity
// type because it has the 'langcode' entity key defined.
$field_storage_definitions['langcode']->setRevisionable(TRUE);
$field_storage_definitions['revision_id'] = BaseFieldDefinition::create('integer')
->setName('revision_id')
->setTargetEntityTypeId('entity_test_update')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision ID'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$field_storage_definitions['revision_default'] = BaseFieldDefinition::create('boolean')
->setName('revision_default')
->setTargetEntityTypeId('entity_test_update')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Default revision'))
->setDescription(new TranslatableMarkup('A flag indicating whether this was a default revision when it was saved.'))
->setStorageRequired(TRUE)
->setInternal(TRUE)
->setTranslatable(FALSE)
->setRevisionable(TRUE);
}
if ($translatable) {
// The 'langcode' is already available for the 'entity_test_update' entity
// type because it has the 'langcode' entity key defined.
$field_storage_definitions['langcode']->setTranslatable(TRUE);
$field_storage_definitions['default_langcode'] = BaseFieldDefinition::create('boolean')
->setName('default_langcode')
->setTargetEntityTypeId('entity_test_update')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Default translation'))
->setDescription(new TranslatableMarkup('A flag indicating whether this is the default translation.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDefaultValue(TRUE);
}
if ($revisionable && $translatable) {
$field_storage_definitions['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
->setName('revision_translation_affected')
->setTargetEntityTypeId('entity_test_update')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision translation affected'))
->setDescription(new TranslatableMarkup('Indicates if the last edit of a translation belongs to current revision.'))
->setReadOnly(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
}
return $field_storage_definitions;
}
}
......@@ -409,7 +409,7 @@ public function testGetSchemaRevisionable() {
->method('getRevisionMetadataKeys')
->will($this->returnValue([]));
$this->storage->expects($this->exactly(2))
$this->storage->expects($this->exactly(1))
->method('getRevisionTable')
->will($this->returnValue('entity_test_revision'));
......@@ -505,6 +505,7 @@ public function testGetSchemaTranslatable() {
'id' => 'id',
'langcode' => 'langcode',
],
'translatable' => TRUE,
]);
$this->storage->expects($this->any())
......@@ -622,24 +623,22 @@ public function testGetSchemaRevisionableTranslatable() {
'revision' => 'revision_id',
'langcode' => 'langcode',
],
'revision_data_table' => 'entity_test_revision_field_data',
],
])
->setMethods(['getRevisionMetadataKeys'])
->setMethods(['isRevisionable', 'isTranslatable', 'getRevisionMetadataKeys'])
->getMock();
$this->entityType->expects($this->any())
->method('getRevisionMetadataKeys')
->will($this->returnValue([]));
->method('isRevisionable')
->will($this->returnValue(TRUE));
$this->entityType->expects($this->any())
->method('isTranslatable')
->will($this->returnValue(TRUE));
$this->storage->expects($this->exactly(3))
$this->storage->expects($this->exactly(2))
->method('getRevisionTable')
->will($this->returnValue('entity_test_revision'));
$this->storage->expects($this->once())
->method('getDataTable')
->will($this->returnValue('entity_test_field_data'));
$this->storage->expects($this->once())
->method('getRevisionDataTable')
->will($this->returnValue('entity_test_revision_field_data'));
$this->setUpStorageDefinition('revision_id', [
'columns' => [
......
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