Commit 4f60dd53 authored by catch's avatar catch

Issue #2535082 by alexpott, jhedstrom, xjm, plach, Fabianx, effulgentsia,...

Issue #2535082 by alexpott, jhedstrom, xjm, plach, Fabianx, effulgentsia, Berdir: Allow hook_update_N() implementations to run before the automated entity updates
parent 8b83aa08
......@@ -1656,6 +1656,12 @@ 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
......
......@@ -224,14 +224,10 @@ function update_do_one($module, $number, $dependency_map, &$context) {
/**
* Performs entity definition updates, which can trigger schema updates.
*
* @param $module
* The module whose update will be run.
* @param $number
* The update number to run.
* @param $context
* The batch context array.
*/
function update_entity_definitions($module, $number, &$context) {
function update_entity_definitions(&$context) {
try {
\Drupal::service('entity.definition_update_manager')->applyUpdates();
}
......@@ -243,7 +239,7 @@ function update_entity_definitions($module, $number, &$context) {
// \Drupal\Component\Utility\SafeMarkup::checkPlain() by
// \Drupal\Core\Utility\Error::decodeException().
$ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
$context['results'][$module][$number] = $ret;
$context['results']['core']['update_entity_definitions'] = $ret;
$context['results']['#abort'][] = 'update_entity_definitions';
}
}
......
......@@ -97,7 +97,7 @@ public function getChangeSummary() {
public function applyUpdates() {
$change_list = $this->getChangeList();
if ($change_list) {
// getChangeList() only disables the cache and does not invalidate.
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
......@@ -107,18 +107,7 @@ public function applyUpdates() {
// to revisionable and at the same time add revisionable fields to the
// entity type.
if (!empty($change_list['entity_type'])) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
switch ($change_list['entity_type']) {
case static::DEFINITION_CREATED:
$this->entityManager->onEntityTypeCreate($entity_type);
break;
case static::DEFINITION_UPDATED:
$original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
break;
}
$this->doEntityUpdate($change_list['entity_type'], $entity_type_id);
}
// Process field storage definition changes.
......@@ -127,24 +116,105 @@ public function applyUpdates() {
$original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
switch ($change) {
case static::DEFINITION_CREATED:
$this->entityManager->onFieldStorageDefinitionCreate($storage_definitions[$field_name]);
break;
case static::DEFINITION_UPDATED:
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]);
break;
case static::DEFINITION_DELETED:
$this->entityManager->onFieldStorageDefinitionDelete($original_storage_definitions[$field_name]);
break;
}
$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;
$this->doFieldUpdate($change, $storage_definition, $original_storage_definition);
}
}
}
}
/**
* {@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;
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
$this->doEntityUpdate($op, $entity_type_id);
return TRUE;
}
/**
* {@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;
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
$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;
$this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
return TRUE;
}
/**
* Performs an entity type definition update.
*
* @param string $op
* The operation to perform, either static::DEFINITION_CREATED or
* static::DEFINITION_UPDATED.
* @param string $entity_type_id
* The entity type ID.
*/
protected function doEntityUpdate($op, $entity_type_id) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
switch ($op) {
case static::DEFINITION_CREATED:
$this->entityManager->onEntityTypeCreate($entity_type);
break;
case static::DEFINITION_UPDATED:
$original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
break;
}
}
/**
* Performs a field storage definition update.
*
* @param string $op
* The operation to perform, possible values are static::DEFINITION_CREATED,
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @param array|null $storage_definition
* The new field storage definition.
* @param array|null $original_storage_definition
* The original field storage definition.
*/
protected function doFieldUpdate($op, $storage_definition = NULL, $original_storage_definition = NULL) {
switch ($op) {
case static::DEFINITION_CREATED:
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
break;
case static::DEFINITION_UPDATED:
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original_storage_definition);
break;
case static::DEFINITION_DELETED:
$this->entityManager->onFieldStorageDefinitionDelete($original_storage_definition);
break;
}
}
/**
* Gets a list of changes to entity type and field storage definitions.
*
......
......@@ -83,4 +83,68 @@ public function getChangeSummary();
*/
public function applyUpdates();
/**
* Performs a single entity definition update.
*
* 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.
*
* @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.
*
* @return bool
* TRUE if the entity update is processed, FALSE if not.
*
* @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.
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
/**
* Performs a single field storage definition update.
*
* 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.
*
* @param string $op
* The operation to perform, possible values are static::DEFINITION_CREATED,
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @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.
* @return bool
* TRUE if the entity update is processed, FALSE if not.
*
* @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.
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
}
......@@ -556,13 +556,6 @@ protected function triggerBatch(Request $request) {
$operations = array();
// First of all perform entity definition updates, which will update
// storage schema if needed, so that module update functions work with
// the correct entity schema.
if ($this->entityDefinitionUpdateManager->needsUpdates()) {
$operations[] = array('update_entity_definitions', array('system', '0 - Update entity definitions'));
}
// Resolve any update dependencies to determine the actual updates that will
// be run and the order they will be run in.
$start = $this->getModuleUpdates();
......@@ -578,7 +571,7 @@ protected function triggerBatch(Request $request) {
}
// Determine updates to be performed.
foreach ($updates as $update) {
foreach ($updates as $function => $update) {
if ($update['allowed']) {
// Set the installed version of each module so updates will start at the
// correct place. (The updates are already sorted, so we can simply base
......@@ -587,11 +580,20 @@ protected function triggerBatch(Request $request) {
drupal_set_installed_schema_version($update['module'], $update['number'] - 1);
unset($start[$update['module']]);
}
// Add this update function to the batch.
$function = $update['module'] . '_update_' . $update['number'];
$operations[] = array('update_do_one', array($update['module'], $update['number'], $dependency_map[$function]));
}
}
// Lastly, perform entity definition updates, which will update storage
// schema if needed. If module update functions need to work with specific
// entity schema they should call the entity update service for the specific
// update themselves.
// @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyEntityUpdate()
// @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyFieldUpdate()
if ($this->entityDefinitionUpdateManager->needsUpdates()) {
$operations[] = array('update_entity_definitions', array());
}
$batch['operations'] = $operations;
$batch += array(
'title' => $this->t('Updating'),
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\IntegrityConstraintViolationException;
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeEvents;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
......@@ -604,4 +605,25 @@ public function testEntityTypeSchemaUpdateAndRevisionableBaseFieldCreateWithoutD
}
}
/**
* Tests ::applyEntityUpdate() and ::applyFieldUpdate().
*/
public function testSingleActionCalls() {
// Ensure that the methods return FALSE when called with bogus information.
$this->assertFalse($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'foo'), 'Calling applyEntityUpdate() with a non-existent entity returns FALSE.');
$this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'foo', 'bar'), 'Calling applyFieldUpdate() with a non-existent entity returns FALSE.');
$this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update', 'bar'), 'Calling applyFieldUpdate() with a non-existent field returns FALSE.');
$this->assertFalse($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update'), 'Calling applyEntityUpdate() with an $op that is not applicable to the entity type returns FALSE.');
$this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_DELETED, 'entity_test_update', 'new_base_field'), 'Calling applyFieldUpdate() with an $op that is not applicable to the field returns FALSE.');
// Create a new base field.
$this->addRevisionableBaseField();
$this->assertTrue($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update', 'new_base_field'), 'Calling applyFieldUpdate() correctly returns TRUE.');
$this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
// Make the entity type revisionable.
$this->updateEntityTypeToRevisionable();
$this->assertTrue($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_UPDATED, 'entity_test_update'), 'Calling applyEntityUpdate() correctly returns TRUE.');
$this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
}
}
......@@ -16,6 +16,11 @@
*/
class SqlContentEntityStorageSchemaIndexTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['update_order_test'];
/**
* {@inheritdoc}
*/
......@@ -27,16 +32,51 @@ public function setUp() {
}
/**
* Test for the new index.
* Tests entity and field schema database updates and execution order.
*/
public function testIndex() {
// Enable the hook implementations in the update_order_test module.
\Drupal::state()->set('update_order_test', TRUE);
// The initial Drupal 8 database dump before any updates does not include
// the entity ID in the entity field data table indices that were added in
// https://www.drupal.org/node/2261669.
$this->assertTrue(db_index_exists('node_field_data', 'node__default_langcode'), 'Index node__default_langcode exists prior to running updates.');
$this->assertFalse(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode does not exist prior to running updates.');
$this->assertFalse(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode does not exist prior to running updates.');
// Running database updates should automatically update the entity schemata
// to add the indices from https://www.drupal.org/node/2261669.
$this->runUpdates();
$this->assertFalse(db_index_exists('node_field_data', 'node__default_langcode'), 'Index node__default_langcode properly removed.');
$this->assertTrue(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode properly created on the node_field_data table.');
$this->assertTrue(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode properly created on the user_field_data table.');
// Ensure that hook_update_N() implementations were in the expected order
// relative to the entity and field updates. The expected order is:
// 1. Initial Drupal 8.0.0-beta12 installation with no indices.
// 2. update_order_test_update_8001() is invoked.
// 3. update_order_test_update_8002() is invoked.
// 4. update_order_test_update_8002() explicitly applies the updates for
// the update_order_test_field storage. See update_order_test.module.
// 5. update_order_test_update_8002() explicitly applies the updates for
// the node entity type indices listed above.
// 6. The remaining entity schema updates are applied automatically after
// all update hook implementations have run, which applies the user
// index update.
$this->assertTrue(\Drupal::state()->get('update_order_test_update_8001', FALSE), 'Index node__default_langcode still existed during update_order_test_update_8001(), indicating that it ran before the entity type updates.');
// Node updates were run during update_order_test_update_8002().
$this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_node__default_langcode', TRUE), 'The node__default_langcode index was removed during update_order_test_update_8002().');
$this->assertTrue(\Drupal::state()->get('update_order_test_update_8002_node__id__default_langcode__langcode', FALSE), 'The node__id__default_langcode__langcode index was created during update_order_test_update_8002().');
// Ensure that the base field created by update_order_test_update_8002() is
// created when we expect.
$this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_update_order_test_before', TRUE), 'The update_order_test field was not been created on Node before update_order_test_update_8002().');
$this->assertTrue(\Drupal::state()->get('update_order_test_update_8002_update_order_test_after', FALSE), 'The update_order_test field was created on Node by update_order_test_update_8002().');
// User update were not run during update_order_test_update_8002().
$this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_user__id__default_langcode__langcode', TRUE));
}
}
name: 'Update order test'
type: module
description: 'Support module for update testing.'
package: Testing
version: VERSION
core: 8.x
<?php
/**
* @file
* Update hooks for the update_order_test module.
*/
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
/**
* Only declare the update hooks once the test is running.
*
* @see \Drupal\system\Tests\Entity\Update\SqlContentEntityStorageSchemaIndexTest
*/
if (\Drupal::state()->get('update_order_test', FALSE)) {
/**
* Runs before entity schema updates.
*/
function update_order_test_update_8001() {
// Store whether the node__default_langcode index exists when this hook is
// invoked.
\Drupal::state()->set('update_order_test_update_8001', db_index_exists('node_field_data', 'node__default_langcode'));
}
/**
* Runs before entity schema updates.
*/
function update_order_test_update_8002() {
// Check and store whether the update_order_test field exists when this
// hook is first invoked.
\Drupal::state()->set('update_order_test_update_8002_update_order_test_before', db_field_exists('node_field_data', 'update_order_test'));
// Attempt to apply the update for the update_order_test field and then
// check and store again whether it exists.
if (\Drupal::service('entity.definition_update_manager')->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'node', 'update_order_test')) {
\Drupal::state()->set('update_order_test_update_8002_update_order_test_after', db_field_exists('node_field_data', 'update_order_test'));
}
// Attempt to apply all node entity type updates.
if (\Drupal::service('entity.definition_update_manager')->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_UPDATED, 'node')) {
// Node updates have now run. Check and store whether the updated node
// indices now exist.
\Drupal::state()->set('update_order_test_update_8002_node__default_langcode', db_index_exists('node_field_data', 'node__default_langcode'));
\Drupal::state()->set('update_order_test_update_8002_node__id__default_langcode__langcode', db_index_exists('node_field_data', 'node__id__default_langcode__langcode'));
// User updates have not yet run. Check and store whether the updated
// user indices now exist.
\Drupal::state()->set('update_order_test_update_8002_user__id__default_langcode__langcode', db_index_exists('users_field_data', 'user__id__default_langcode__langcode'));
}
}
}
<?php
/**
* @file
* Hooks for the update_order_test module.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Only declare the new entity base field once the test is running.
*/
if (\Drupal::state()->get('update_order_test', FALSE)) {
/**
* Implements hook_entity_base_field_info().
*/
function update_order_test_entity_base_field_info(EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'node') {
$fields['update_order_test'] = BaseFieldDefinition::create('integer')
->setLabel(t('Update order test'));
return $fields;
}
}
}
......@@ -34,5 +34,10 @@ function testMinimal() {
$this->drupalGet('');
$this->assertText(t('Tools'));
$this->assertText(t('Administration'));
// Ensure that there are no pending updates after installation.
$this->drupalLogin($this->rootUser);
$this->drupalGet('update.php/selection');
$this->assertText('No pending updates.');
}
}
......@@ -152,6 +152,11 @@ function testStandard() {
$this->adminUser->save();
$this->drupalGet('node/add');
$this->assertResponse(200);
// Ensure that there are no pending updates after installation.
$this->drupalLogin($this->rootUser);
$this->drupalGet('update.php/selection');
$this->assertText('No pending updates.');
}
}
......@@ -16,12 +16,6 @@
* @see system_install()
*/
function standard_install() {
// 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();
// Set front page to "node".
\Drupal::configFactory()->getEditable('system.site')->set('page.front', '/node')->save(TRUE);
......
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