Commit d7a9d44e authored by catch's avatar catch
Browse files

Issue #2020895 by yched, pcambra, swentel: Move save() / delete() logic in...

Issue #2020895 by yched, pcambra, swentel: Move save() / delete() logic in Field / FieldInstance to [pre|post]Save(), [pre|post]Delete().
parent dd5dcd1b
......@@ -1106,7 +1106,6 @@ public function onFieldUpdate(FieldInterface $field) {
*/
public function onFieldDelete(FieldInterface $field) {
// Mark all data associated with the field for deletion.
$field->deleted = FALSE;
$table = static::_fieldTableName($field);
$revision_table = static::_fieldRevisionTableName($field);
$this->database->update($table)
......@@ -1115,9 +1114,10 @@ public function onFieldDelete(FieldInterface $field) {
// Move the table to a unique name while the table contents are being
// deleted.
$field->deleted = TRUE;
$new_table = static::_fieldTableName($field);
$revision_new_table = static::_fieldRevisionTableName($field);
$deleted_field = clone $field;
$deleted_field->deleted = TRUE;
$new_table = static::_fieldTableName($deleted_field);
$revision_new_table = static::_fieldRevisionTableName($deleted_field);
$this->database->schema()->renameTable($table, $new_table);
$this->database->schema()->renameTable($revision_table, $revision_new_table);
}
......
......@@ -10,6 +10,7 @@
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\field\FieldException;
use Drupal\field\FieldInterface;
......@@ -270,42 +271,35 @@ public function getExportProperties() {
}
/**
* Overrides \Drupal\Core\Entity\Entity::save().
*
* @return int
* Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
* Overrides \Drupal\Core\Entity\Entity::preSave().
*
* @throws \Drupal\field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
public function save() {
public function preSave(EntityStorageControllerInterface $storage_controller) {
// Clear the derived data about the field.
unset($this->schema);
if ($this->isNew()) {
return $this->saveNew();
return $this->preSaveNew($storage_controller);
}
else {
return $this->saveUpdated();
return $this->preSaveUpdated($storage_controller);
}
}
/**
* Saves a new field definition.
* Prepares saving a new field definition.
*
* @return int
* SAVED_NEW if the definition was saved.
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
* The entity storage controller.
*
* @throws \Drupal\field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
* @throws \Drupal\field\FieldException If the field definition is invalid.
*/
protected function saveNew() {
protected function preSaveNew(EntityStorageControllerInterface $storage_controller) {
$entity_manager = \Drupal::entityManager();
$storage_controller = $entity_manager->getStorageController($this->entityType);
// Assign the ID.
$this->id = $this->id();
......@@ -353,100 +347,103 @@ protected function saveNew() {
// Notify the entity storage controller.
$entity_manager->getStorageController($this->entity_type)->onFieldCreate($this);
// Save the configuration.
$result = parent::save();
field_cache_clear();
return $result;
}
/**
* Saves an updated field definition.
*
* @return int
* SAVED_UPDATED if the definition was saved.
* Prepares saving an updated field definition.
*
* @throws \Drupal\field\FieldException
* If the field definition is invalid.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
* @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage_controller
* The entity storage controller.
*/
protected function saveUpdated() {
protected function preSaveUpdated(EntityStorageControllerInterface $storage_controller) {
$module_handler = \Drupal::moduleHandler();
$entity_manager = \Drupal::entityManager();
$storage_controller = $entity_manager->getStorageController($this->entityType);
$original = $storage_controller->loadUnchanged($this->id());
$this->original = $original;
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
// Some updates are always disallowed.
if ($this->type != $original->type) {
if ($this->type != $this->original->type) {
throw new FieldException("Cannot change an existing field's type.");
}
if ($this->entity_type != $original->entity_type) {
if ($this->entity_type != $this->original->entity_type) {
throw new FieldException("Cannot change an existing field's entity_type.");
}
// Make sure all settings are present, so that a complete field definition
// is saved. This allows calling code to perform partial updates on field
// objects.
$this->settings += $original->settings;
// Make sure all settings are present, so that a complete field
// definition is passed to the various hooks and written to config.
$this->settings += $field_type_manager->getDefaultSettings($this->type);
// See if any module forbids the update by throwing an exception. This
// invokes hook_field_update_forbid().
$module_handler->invokeAll('field_update_forbid', array($this, $original));
$module_handler->invokeAll('field_update_forbid', array($this, $this->original));
// Notify the storage controller. The controller can reject the definition
// update as invalid by raising an exception, which stops execution before
// the definition is written to config.
$entity_manager->getStorageController($this->entity_type)->onFieldUpdate($this, $original);
$entity_manager->getStorageController($this->entity_type)->onFieldUpdate($this);
}
// Save the configuration.
$result = parent::save();
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Clear the cache.
field_cache_clear();
return $result;
}
/**
* {@inheritdoc}
*/
public function delete() {
if (!$this->deleted) {
$instance_controller = \Drupal::entityManager()->getStorageController('field_instance');
$state = \Drupal::state();
// Delete all non-deleted instances.
$instance_ids = array();
foreach ($this->getBundles() as $bundle) {
$instance_ids[] = "{$this->entity_type}.$bundle.{$this->name}";
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $fields) {
$state = \Drupal::state();
$instance_controller = \Drupal::entityManager()->getStorageController('field_instance');
// Delete instances first. Note: when deleting a field through
// FieldInstance::postDelete(), the instances have been deleted already, so
// no instances will be found here.
$instance_ids = array();
foreach ($fields as $field) {
if (!$field->deleted) {
foreach ($field->getBundles() as $bundle) {
$instance_ids[] = "{$field->entity_type}.$bundle.{$field->name}";
}
}
foreach ($instance_controller->loadMultiple($instance_ids) as $instance) {
// By default, FieldInstance::delete() will automatically try to delete
// a field definition when it is deleting the last instance of the
// field. Since the whole field is being deleted here, pass FALSE as
// the $field_cleanup parameter to prevent a loop.
$instance->delete(FALSE);
}
if ($instance_ids) {
$instances = $instance_controller->loadMultiple($instance_ids);
// Tag the objects to preserve recursive deletion of the field.
foreach ($instances as $instance) {
$instance->noFieldDelete = TRUE;
}
$instance_controller->delete($instances);
}
\Drupal::entityManager()->getStorageController($this->entity_type)->onFieldDelete($this);
// Delete the configuration of this field and save the field configuration
// in the key_value table so we can use it later during
// field_purge_batch(). This makes sure a new field can be created
// immediately with the same name.
$deleted_fields = $state->get('field.field.deleted') ?: array();
$config = $this->getExportProperties();
$config['deleted'] = TRUE;
$deleted_fields[$this->uuid] = $config;
$state->set('field.field.deleted', $deleted_fields);
parent::delete();
// Keep the field definitions in the state storage so we can use them later
// during field_purge_batch().
$deleted_fields = $state->get('field.field.deleted') ?: array();
foreach ($fields as $field) {
if (!$field->deleted) {
$config = $field->getExportProperties();
$config['deleted'] = TRUE;
$config['bundles'] = $field->getBundles();
$deleted_fields[$field->uuid] = $config;
}
}
$state->set('field.field.deleted', $deleted_fields);
}
// Clear the cache.
field_cache_clear();
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $fields) {
// Notify the storage.
foreach ($fields as $field) {
if (!$field->deleted) {
\Drupal::entityManager()->getStorageController($field->entity_type)->onFieldDelete($field);
}
}
// Clear the cache.
field_cache_clear();
}
/**
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageControllerInterface;
use Drupal\field\FieldException;
use Drupal\field\FieldInstanceInterface;
......@@ -319,163 +320,122 @@ public function getExportProperties() {
}
/**
* Overrides \Drupal\Core\Entity\Entity::save().
*
* @return
* Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
* Overrides \Drupal\Core\Entity\Entity::preSave().
*
* @throws \Drupal\field\FieldException
* If the field instance definition is invalid.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
*/
public function save() {
public function preSave(EntityStorageControllerInterface $storage_controller) {
$entity_manager = \Drupal::entityManager();
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
if ($this->isNew()) {
return $this->saveNew();
// Ensure the field instance is unique within the bundle.
if ($prior_instance = $storage_controller->load($this->id())) {
throw new FieldException(format_string('Attempt to create an instance of field %name on bundle @bundle that already has an instance of that field.', array('%name' => $this->field->name, '@bundle' => $this->bundle)));
}
// Set the field UUID.
$this->field_uuid = $this->field->uuid;
// Set the default instance settings.
$this->settings += $field_type_manager->getDefaultInstanceSettings($this->field->type);
// Notify the entity storage controller.
$entity_manager->getStorageController($this->entity_type)->onInstanceCreate($this);
}
else {
return $this->saveUpdated();
// Some updates are always disallowed.
if ($this->entity_type != $this->original->entity_type) {
throw new FieldException("Cannot change an existing instance's entity_type.");
}
if ($this->bundle != $this->original->bundle && empty($this->bundle_rename_allowed)) {
throw new FieldException("Cannot change an existing instance's bundle.");
}
if ($this->field_uuid != $this->original->field_uuid) {
throw new FieldException("Cannot change an existing instance's field.");
}
// Set the default instance settings.
$this->settings += $field_type_manager->getDefaultInstanceSettings($this->field->type);
// Notify the entity storage controller.
$entity_manager->getStorageController($this->entity_type)->onInstanceUpdate($this);
}
}
/**
* Saves a new field instance definition.
*
* @return
* SAVED_NEW if the definition was saved.
*
* @throws \Drupal\field\FieldException
* If the field instance definition is invalid.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
* {@inheritdoc}
*/
protected function saveNew() {
$instance_controller = \Drupal::entityManager()->getStorageController($this->entityType);
// Ensure the field instance is unique within the bundle.
if ($prior_instance = $instance_controller->load($this->id())) {
throw new FieldException(format_string('Attempt to create an instance of field %name on bundle @bundle that already has an instance of that field.', array('%name' => $this->field->name, '@bundle' => $this->bundle)));
}
// Set the field UUID.
$this->field_uuid = $this->field->uuid;
// Ensure default values are present.
$this->prepareSave();
// Save the configuration.
$result = parent::save();
public function postSave(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
// Clear the cache.
field_cache_clear();
return $result;
}
/**
* Saves an updated field instance definition.
*
* @return
* SAVED_UPDATED if the definition was saved.
*
* @throws \Drupal\field\FieldException
* If the field instance definition is invalid.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures at the configuration storage level.
* {@inheritdoc}
*/
protected function saveUpdated() {
$instance_controller = \Drupal::entityManager()->getStorageController($this->entityType);
$original = $instance_controller->loadUnchanged($this->getOriginalId());
$this->original = $original;
// Some updates are always disallowed.
if ($this->entity_type != $original->entity_type) {
throw new FieldException("Cannot change an existing instance's entity_type.");
}
if ($this->bundle != $original->bundle && empty($this->bundle_rename_allowed)) {
throw new FieldException("Cannot change an existing instance's bundle.");
}
if ($this->field_uuid != $original->field_uuid) {
throw new FieldException("Cannot change an existing instance's field.");
public static function preDelete(EntityStorageControllerInterface $storage_controller, array $instances) {
$state = \Drupal::state();
// Keep the instance definitions in the state storage so we can use them
// later during field_purge_batch().
$deleted_instances = $state->get('field.instance.deleted') ?: array();
foreach ($instances as $instance) {
if (!$instance->deleted) {
$config = $instance->getExportProperties();
$config['deleted'] = TRUE;
$deleted_instances[$instance->uuid] = $config;
}
}
// Ensure default values are present.
$this->prepareSave();
// Notify the entity storage controller.
\Drupal::entityManager()->getStorageController($this->entity_type)->onInstanceUpdate($this);
// Save the configuration.
$result = parent::save();
field_cache_clear();
return $result;
}
/**
* Prepares the instance definition for saving.
*/
protected function prepareSave() {
$field_type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinition($this->field->type);
// Set the default instance settings.
$this->settings += $field_type_info['instance_settings'];
$state->set('field.instance.deleted', $deleted_instances);
}
/**
* Overrides \Drupal\Core\Entity\Entity::delete().
*
* @param bool $field_cleanup
* (optional) If TRUE, the field will be deleted as well if its last
* instance is being deleted. If FALSE, it is the caller's responsibility to
* handle the case of fields left without instances. Defaults to TRUE.
* {@inheritdoc}
*/
public function delete($field_cleanup = TRUE) {
if (!$this->deleted) {
$state = \Drupal::state();
// Delete the configuration of this instance and save the configuration
// in the key_value table so we can use it later during
// field_purge_batch().
$deleted_instances = $state->get('field.instance.deleted') ?: array();
$config = $this->getExportProperties();
$config['deleted'] = TRUE;
$deleted_instances[$this->uuid] = $config;
$state->set('field.instance.deleted', $deleted_instances);
parent::delete();
public static function postDelete(EntityStorageControllerInterface $storage_controller, array $instances) {
$field_controller = \Drupal::entityManager()->getStorageController('field_entity');
// Notify the entity storage controller.
\Drupal::entityManager()->getStorageController($this->entity_type)->onInstanceDelete($this);
// Clear the cache.
field_cache_clear();
// Clear the cache upfront, to refresh the results of getBundles().
field_cache_clear();
// Remove the instance from the entity form displays.
$ids = array();
$form_modes = array('default' => array()) + entity_get_form_modes($this->entity_type);
foreach (array_keys($form_modes) as $form_mode) {
$ids[] = $this->entity_type . '.' . $this->bundle . '.' . $form_mode;
}
foreach (entity_load_multiple('entity_form_display', $ids) as $form_display) {
$form_display->removeComponent($this->field->name)->save();
// Notify the entity storage controller.
foreach ($instances as $instance) {
if (!$instance->deleted) {
\Drupal::entityManager()->getStorageController($instance->entity_type)->onInstanceDelete($instance);
}
}
// Remove the instance from the entity displays.
$ids = array();
$view_modes = array('default' => array()) + entity_get_view_modes($this->entity_type);
foreach (array_keys($view_modes) as $view_mode) {
$ids[] = $this->entity_type . '.' . $this->bundle . '.' . $view_mode;
}
foreach (entity_load_multiple('entity_display', $ids) as $display) {
$display->removeComponent($this->field->name)->save();
// Delete fields that have no more instances.
$fields_to_delete = array();
foreach ($instances as $instance) {
if (!$instance->deleted && empty($instance->noFieldDelete) && count($instance->field->getBundles()) == 0) {
// Key by field UUID to avoid deleting the same field twice.
$fields_to_delete[$instance->field_uuid] = $instance->getField();
}
}
if ($fields_to_delete) {
$field_controller->delete($fields_to_delete);
}
// Delete the field itself if we just deleted its last instance.
if ($field_cleanup && count($this->field->getBundles()) == 0) {
$this->field->delete();
// Cleanup entity displays.
$displays_to_update = array();
foreach ($instances as $instance) {
if (!$instance->deleted) {
$view_modes = array('default' => array()) + entity_get_view_modes($instance->entity_type);
foreach (array_keys($view_modes) as $mode) {
$displays_to_update['entity_display'][$instance->entity_type . '.' . $instance->bundle . '.' . $mode][] = $instance->field->name;
}
$form_modes = array('default' => array()) + entity_get_form_modes($instance->entity_type);
foreach (array_keys($form_modes) as $mode) {
$displays_to_update['entity_form_display'][$instance->entity_type . '.' . $instance->bundle . '.' . $mode][] = $instance->field->name;
}
}
}
foreach ($displays_to_update as $type => $ids) {
foreach (entity_load_multiple($type, array_keys($ids)) as $id => $display) {
foreach ($ids[$id] as $field_name) {
$display->removeComponent($field_name);
}
$display->save();
}
}
}
......
......@@ -174,13 +174,46 @@ function testDeleteFieldInstance() {
// Make sure the other field instance is not deleted.
$another_instance = field_read_instance('entity_test', $another_instance_definition['field_name'], $another_instance_definition['bundle']);
$this->assertTrue(!empty($another_instance) && empty($another_instance->deleted), 'A non-deleted field instance is not marked for deletion.');
}
// Make sure the field is deleted when its last instance is deleted.
$another_instance->delete();
$deleted_fields = \Drupal::state()->get('field.field.deleted');
$this->assertTrue(isset($deleted_fields[$another_instance->field_uuid]), 'A deleted field is marked for deletion.');
$field = field_read_field($another_instance->entity_type, $another_instance->getFieldName());
$this->assertFalse($field, 'The field marked to be deleted is not found anymore in the configuration.');
/**
* Tests the cross deletion behavior between fields and instances.
*/
function testDeleteFieldInstanceCrossDeletion() {
$instance_definition_2 = $this->instance_definition;
$instance_definition_2['bundle'] .= '_another_bundle';
// Check that deletion of a field deletes its instances.
$field = $this->field;
entity_create('field_instance', $this->instance_definition)->save();
entity_create('field_instance', $instance_definition_2)->save();
$field->delete();
$this->assertFalse(field_info_instance('entity_test', $this->instance_definition['bundle'], $field->name));
$this->assertFalse(field_info_instance('entity_test', $instance_definition_2['bundle'], $field->name));
// Chack that deletion of the last instance deletes the field.
$field = entity_create('field_entity', $this->field_definition);
$field->save();
$instance = entity_create('field_instance', $this->instance_definition);
$instance->save();
$instance_2 = entity_create('field_instance', $instance_definition_2);
$instance_2->save();
$instance->delete();
$this->assertTrue(field_info_field('entity_test', $field->name));
$instance_2->delete();
$this->assertFalse(field_info_field('entity_test', $field->name));
// Check that deletion of all instances of the same field simultaneously
// deletes the field.
$field = entity_create('field_entity', $this->field_definition);
$field->save();
$instance = entity_create('field_instance', $this->instance_definition);
$instance->save();
$instance_2 = entity_create('field_instance', $instance_definition_2);
$instance_2->save();
$instance_controller = $this->container->get('entity.manager')->getStorageController('field_instance');
$instance_controller->delete(array($instance, $instance_2));
$this->assertFalse(field_info_field('entity_test', $field->name));
}
}
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