Commit ba95bbb2 authored by effulgentsia's avatar effulgentsia

Issue #2542748 by plach, effulgentsia, jhedstrom, Gábor Hojtsy, alexpott,...

Issue #2542748 by plach, effulgentsia, jhedstrom, Gábor Hojtsy, alexpott, mpdonadio, catch, dawehner: Automatic entity updates can fail when there is existing content, leaving the site's schema in an unpredictable state
parent 3e82ee5d
......@@ -1599,6 +1599,10 @@ function install_bootstrap_full() {
* The batch definition.
*/
function install_profile_modules(&$install_state) {
// We need to manually trigger the installation of core-provided entity types,
// as those will not be handled by the module installer.
install_core_entity_type_definitions();
$modules = \Drupal::state()->get('install_profile_modules') ?: array();
$files = system_rebuild_module_data();
\Drupal::state()->delete('install_profile_modules');
......@@ -1638,6 +1642,18 @@ function install_profile_modules(&$install_state) {
return $batch;
}
/**
* Installs entity type definitions provided by core.
*/
function install_core_entity_type_definitions() {
$update_manager = \Drupal::entityDefinitionUpdateManager();
foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == 'core') {
$update_manager->installEntityType($entity_type);
}
}
}
/**
* Installs themes.
*
......@@ -1665,12 +1681,6 @@ function install_profile_themes(&$install_state) {
* An array of information about the current installation state.
*/
function install_install_profile(&$install_state) {
// Now that all modules are installed, make sure the entity storage and other
// handlers are up to date with the current entity and field definitions. For
// example, Path module adds a base field to nodes and taxonomy terms after
// those modules are already installed.
\Drupal::service('entity.definition_update_manager')->applyUpdates();
\Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE);
// Install all available optional config. During installation the module order
// is determined by dependencies. If there are no dependencies between modules
......
......@@ -218,26 +218,6 @@ function update_do_one($module, $number, $dependency_map, &$context) {
$context['message'] = 'Updating ' . Html::escape($module) . ' module';
}
/**
* Performs entity definition updates, which can trigger schema updates.
*
* @param $context
* The batch context array.
*/
function update_entity_definitions(&$context) {
try {
\Drupal::service('entity.definition_update_manager')->applyUpdates();
}
catch (EntityStorageException $e) {
watchdog_exception('update', $e);
$variables = Error::decodeException($e);
unset($variables['backtrace']);
$ret['#abort'] = array('success' => FALSE, 'query' => t('%type: @message in %function (line %line of %file).', $variables));
$context['results']['core']['update_entity_definitions'] = $ret;
$context['results']['#abort'][] = 'update_entity_definitions';
}
}
/**
* Returns a list of all the pending database updates.
*
......
......@@ -687,4 +687,14 @@ public static function destination() {
return static::getContainer()->get('redirect.destination');
}
/**
* Returns the entity definition update manager.
*
* @return \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
* The entity definition update manager.
*/
public static function entityDefinitionUpdateManager() {
return static::getContainer()->get('entity.definition_update_manager');
}
}
......@@ -9,6 +9,7 @@
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
......@@ -95,13 +96,13 @@ public function getChangeSummary() {
* {@inheritdoc}
*/
public function applyUpdates() {
$change_list = $this->getChangeList();
if ($change_list) {
$complete_change_list = $this->getChangeList();
if ($complete_change_list) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
foreach ($change_list as $entity_type_id => $change_list) {
foreach ($complete_change_list as $entity_type_id => $change_list) {
// Process entity type definition changes before storage definitions ones
// this is necessary when you change an entity type from non-revisionable
// to revisionable and at the same time add revisionable fields to the
......@@ -127,42 +128,76 @@ public function applyUpdates() {
/**
* {@inheritdoc}
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) {
$change_list = $this->getChangeList();
if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) {
return FALSE;
public function getEntityType($entity_type_id) {
$entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
return $entity_type ? clone $entity_type : NULL;
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
/**
* {@inheritdoc}
*/
public function installEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeCreate($entity_type);
}
$this->doEntityUpdate($op, $entity_type_id);
return TRUE;
/**
* {@inheritdoc}
*/
public function updateEntityType(EntityTypeInterface $entity_type) {
$original = $this->getEntityType($entity_type->id());
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) {
$change_list = $this->getChangeList();
if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) {
return FALSE;
public function uninstallEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeDelete($entity_type);
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
/**
* {@inheritdoc}
*/
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition) {
// @todo Pass a mutable field definition interface when we have one. See
// https://www.drupal.org/node/2346329.
if ($storage_definition instanceof BaseFieldDefinition) {
$storage_definition
->setName($name)
->setTargetEntityTypeId($entity_type_id)
->setProvider($provider)
->setTargetBundle(NULL);
}
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
}
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
$original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
$storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL;
$original_storage_definition = isset($original_storage_definitions[$field_name]) ? $original_storage_definitions[$field_name] : NULL;
/**
* {@inheritdoc}
*/
public function getFieldStorageDefinition($name, $entity_type_id) {
$storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL;
}
$this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
return TRUE;
/**
* {@inheritdoc}
*/
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
$original = $this->getFieldStorageDefinition($storage_definition->getName(), $storage_definition->getTargetEntityTypeId());
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original);
}
/**
* {@inheritdoc}
*/
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionDelete($storage_definition);
}
/**
......
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines an interface for managing entity definition updates.
*
......@@ -25,12 +27,22 @@
* report the differences or when to apply each update. This interface is for
* managing that.
*
* This interface also provides methods to retrieve instances of the definitions
* to be updated ready to be manipulated. In fact when definitions change in
* code the system needs to be notified about that and the definitions stored in
* state need to be reconciled with the ones living in code. This typically
* happens in Update API functions, which need to take the system from a known
* state to another known state. Relying on the definitions living in code might
* prevent this, as the system might transition directly to the last available
* state, and thus skipping the intermediate steps. Manipulating the definitions
* in state allows to avoid this and ensures that the various steps of the
* update process are predictable and repeatable.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityTypeListenerInterface
* @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
* @see hook_update_N()
*/
interface EntityDefinitionUpdateManagerInterface {
......@@ -75,6 +87,9 @@ public function getChangeSummary();
/**
* Applies all the detected valid changes.
*
* Use this with care, as it will apply updates for any module, which will
* lead to unpredictable results.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
......@@ -84,67 +99,92 @@ public function getChangeSummary();
public function applyUpdates();
/**
* Performs a single entity definition update.
* Returns an entity type definition ready to be manipulated.
*
* This method should be used from hook_update_N() functions to process
* entity definition updates as part of the update function. This is only
* necessary if the hook_update_N() implementation relies on the entity
* definition update. All remaining entity definition updates will be run
* automatically after the hook_update_N() implementations.
* When needing to apply updates to existing entity type definitions, this
* method should always be used to retrieve a definition ready to be
* manipulated.
*
* @param string $op
* The operation to perform, either static::DEFINITION_CREATED or
* static::DEFINITION_UPDATED.
* @param string $entity_type_id
* The entity type to update.
* @param bool $reset_cached_definitions
* (optional). Determines whether to clear the Entity Manager's cached
* definitions before applying the update. Defaults to TRUE. Can be used
* to prevent unnecessary cache invalidation when a hook_update_N() makes
* multiple calls to this method.
* The entity type identifier.
*
* @return bool
* TRUE if the entity update is processed, FALSE if not.
* @return \Drupal\Core\Entity\EntityTypeInterface
* The entity type definition.
*/
public function getEntityType($entity_type_id);
/**
* Installs a new entity type definition.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function installEntityType(EntityTypeInterface $entity_type);
/**
* Applies any change performed to the passed entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function updateEntityType(EntityTypeInterface $entity_type);
/**
* Uninstalls an entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
public function uninstallEntityType(EntityTypeInterface $entity_type);
/**
* Performs a single field storage definition update.
* Returns a field storage definition ready to be manipulated.
*
* This method should be used from hook_update_N() functions to process field
* storage definition updates as part of the update function. This is only
* necessary if the hook_update_N() implementation relies on the field storage
* definition update. All remaining field storage definition updates will be
* run automatically after the hook_update_N() implementations.
* When needing to apply updates to existing field storage definitions, this
* method should always be used to retrieve a storage definition ready to be
* manipulated.
*
* @param string $op
* The operation to perform, possible values are static::DEFINITION_CREATED,
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @param string $name
* The field name.
* @param string $entity_type_id
* The entity type to update.
* @param string $field_name
* The field name to update.
* @param bool $reset_cached_definitions
* (optional). Determines whether to clear the Entity Manager's cached
* definitions before applying the update. Defaults to TRUE. Can be used
* to prevent unnecessary cache invalidation when a hook_update_N() makes
* multiple calls to this method.
* The entity type identifier.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface
* The field storage definition.
*
* @todo Make this return a mutable storage definition interface when we have
* one. See https://www.drupal.org/node/2346329.
*/
public function getFieldStorageDefinition($name, $entity_type_id);
* @return bool
* TRUE if the entity update is processed, FALSE if not.
/**
* Installs a new field storage definition.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
* @param string $name
* The field storage definition name.
* @param string $entity_type_id
* The target entity type identifier.
* @param string $provider
* The name of the definition provider.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition);
/**
* Applies any change performed to the passed field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
/**
* Uninstalls a field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
}
......@@ -189,9 +189,10 @@ public function getFieldTableName($field_name) {
public function getColumnNames($field_name) {
if (!isset($this->columnMapping[$field_name])) {
$this->columnMapping[$field_name] = array();
$storage_definition = $this->fieldStorageDefinitions[$field_name];
if (isset($this->fieldStorageDefinitions[$field_name])) {
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name);
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
}
}
}
return $this->columnMapping[$field_name];
......
......@@ -10,6 +10,8 @@
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
use Drupal\Core\Entity\EntityBundleListenerInterface;
......@@ -1351,7 +1353,9 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $stor
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
$this->wrapSchemaException(function () use ($entity_type) {
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
});
}
/**
......@@ -1364,14 +1368,18 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
// definition.
$this->initTableLayout();
// Let the schema handler adapt to possible table layout changes.
$this->wrapSchemaException(function () use ($entity_type, $original) {
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
});
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->wrapSchemaException(function () use ($entity_type) {
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
});
}
/**
......@@ -1386,14 +1394,18 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $
if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
$this->tableMapping = NULL;
}
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
});
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
$this->wrapSchemaException(function () use ($storage_definition, $original) {
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
});
}
/**
......@@ -1421,7 +1433,31 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
}
// Update the field schema.
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
});
}
/**
* Wraps a database schema exception into an entity storage exception.
*
* @param callable $callback
* The callback to be executed.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* When a database schema exception is thrown.
*/
protected function wrapSchemaException(callable $callback) {
$message = 'Exception thrown while performing a schema update.';
try {
$callback();
}
catch (SchemaException $e) {
throw new EntityStorageException($message, 0, $e);
}
catch (DatabaseExceptionWrapper $e) {
throw new EntityStorageException($message, 0, $e);
}
}
/**
......
......@@ -1391,6 +1391,20 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
if ($field_name == $updated_field_name) {
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
// Handle NOT NULL constraints.
foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
$not_null = !empty($specifier['not null']);
$original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
if ($not_null !== $original_not_null) {
if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
}
$column_schema = $original_schema[$table_name]['fields'][$column_name];
$column_schema['not null'] = $not_null;
$schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
}
}
// Drop original indexes and unique keys.
if (!empty($original_schema[$table_name]['indexes'])) {
foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
......@@ -1422,6 +1436,26 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
}
}
/**
* Checks whether a field property has NULL values.
*
* @param string $table_name
* The name of the table to inspect.
* @param string $column_name
* The name of the column holding the field property data.
*
* @return bool
* TRUE if NULL data is found, FALSE otherwise.
*/
protected function hasNullFieldPropertyData($table_name, $column_name) {
$query = $this->database->select($table_name, 't')
->fields('t', [$column_name])
->range(0, 1);
$query->isNull('t.' . $column_name);
$result = $query->execute()->fetchAssoc();
return (bool) $result;
}
/**
* Gets the schema for a single field definition.
*
......@@ -1803,15 +1837,34 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def
}
if (!$storage_definition->hasCustomStorage()) {
$schema = $this->getSchemaFromStorageDefinition($storage_definition);
foreach ($this->loadFieldSchemaData($original) as $table => $spec) {
if ($spec['fields'] != $schema[$table]['fields']) {
$keys = array_flip($this->getColumnSchemaRelevantKeys());
$definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
foreach ($table_schema['fields'] as $name => $spec) {
$definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
$stored_spec = array_intersect_key($spec, $keys);
if ($definition_spec != $stored_spec) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Returns a list of column schema keys affecting data storage.
*
* When comparing schema definitions, only changes in certain properties
* actually affect how data is stored and thus, if applied, may imply data
* manipulation.
*
* @return string[]
* An array of key names.
*/
protected function getColumnSchemaRelevantKeys() {
return ['type', 'size', 'length', 'unsigned'];
}
}
......@@ -10,9 +10,9 @@
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Default implementation of the module installer.
......@@ -212,14 +212,34 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
$version = max(max($versions), $version);
}
// Notify interested components that this module's entity types are new.
// For example, a SQL-based storage handler can use this as an
// opportunity to create the necessary database tables.
// Notify interested components that this module's entity types and
// field storage definitions are new. For example, a SQL-based storage
// handler can use this as an opportunity to create the necessary
// database tables.
// @todo Clean this up in https://www.drupal.org/node/2350111.
$entity_manager = \Drupal::entityManager();
$update_manager = \Drupal::entityDefinitionUpdateManager();
foreach ($entity_manager->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == $module) {
$entity_manager->onEntityTypeCreate($entity_type);
$update_manager->installEntityType($entity_type);
}
elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
// The module being installed may be adding new fields to existing
// entity types. Field definitions for any entity type defined by
// the module are handled in the if branch.
foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) {
if ($storage_definition->getProvider() == $module) {
// If the module being installed is also defining a storage key
// for the entity type, the entity schema may not exist yet. It
// will be created later in that case.
try {
$update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition);
}
catch (EntityStorageException $e) {
watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]);
}
}
}
}
}
......@@ -362,9 +382,25 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
// deleted. For example, a SQL-based storage handler can use this as an
// opportunity to drop the corresponding database tables.
// @todo Clean this up in https://www.drupal.org/node/2350111.
$update_manager = \Drupal::entityDefinitionUpdateManager();
foreach ($entity_manager->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == $module) {
$entity_manager->onEntityTypeDelete($entity_type);
$update_manager->uninstallEntityType($entity_type);
}
elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) {
// The module being installed may be adding new fields to existing
// entity types. Field definitions for any entity type defined by
// the module are handled in the if branch.
$entity_type_id = $entity_type->id();
/** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
$storage = $entity_manager->getStorage($entity_type_id);
foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
// @todo We need to trigger field purging here.
// See https://www.drupal.org/node/2282119.
if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) {
$update_manager->uninstallFieldStorageDefinition($storage_definition);
}
}
}
}
......
......@@ -6,6 +6,7 @@
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Url;
use Drupal\Core\Utility\UpdateException;
......@@ -35,8 +36,8 @@
* - Database schema changes: adding, changing, or removing a database table or
* field; moving stored data to different fields or tables; changing the
* format of stored data.
* - Content entity or field changes: these updates are normally handled
* automatically by the entity system, but should at least be tested.
* - Content entity or field changes: adding, changing, or removing a field
* definition, entity definition, or any of their properties.
*
* @section sec_how How to write update code
* Update code for a module is put into an implementation of hook_update_N(),
......@@ -531,6 +532,16 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
* ignored, and make sure that the configuration data you are saving matches
* the configuration schema at the time when you write the update function
* (later updates may change it again to match new schema changes).
* - Never assume your field or entity type definitions are the same when the
* update will run as they are when you wrote the update function. Always
* retrieve the correct version via
* \Drupal::entityDefinitionUpdateManager()::getEntityType() or
* \Drupal::entityDefinitionUpdateManager()::getFieldStorageDefinition(). When
* adding a new definition always replicate it in the update function body as
* you would do with a schema definition.
* - Never call \Drupal::entityDefinitionUpdateManager()::applyUpdates() in an
* update function, as it will apply updates for any module not only yours,
* which will lead to unpredictable results.
* - Be careful about API functions and especially CRUD operations that you use
* in your update function. If they invoke hooks or use services, they may
* not behave as expected, and it may actually not be appropriate to use the
......@@ -548,6 +559,9 @@ function hook_install_tasks_alter(&$tasks, $install_state) {
* long as you make sure that your update data matches the schema, and you
* use the $has_trusted_data argument in the save operation.
* - Marking a container for rebuild.
* - Using the API provided by \Drupal::entityDefinitionUpdateManager() to