diff --git a/core/core.services.yml b/core/core.services.yml index ed1341c..cd7833e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1505,6 +1505,12 @@ services: - { name: event_subscriber } email.validator: class: Egulias\EmailValidator\EmailValidator + update.post_update_registry: + class: Drupal\Core\Update\UpdateRegistry + factory: ['@update.post_update_registry_factory', configure] + update.post_update_registry_factory: + class: Drupal\Core\Update\UpdateRegistryFactory + parent: container.trait response_filter.active_link: class: Drupal\Core\EventSubscriber\ActiveLinkResponseFilter diff --git a/core/includes/update.inc b/core/includes/update.inc index 3695a92..b6b5bc8 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -242,14 +242,11 @@ function update_invoke_post_update($function, &$context) { $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; - $key_value_store = \Drupal::keyValue('update__post_update'); - $executed_updates = $key_value_store->get('existing_updates', []); - $executed_updates[] = $function; - $key_value_store->set('existing_updates', $executed_updates); + \Drupal::service('update.post_update_registry')->registerInvokedUpdate($function); } - // @TODO We may want to do different error handling for different - // exception types, but for now we'll just log the exception and - // return the message for printing. + // @TODO We may want to do different error handling for different + // exception types, but for now we'll just log the exception and + // return the message for printing. catch (Exception $e) { watchdog_exception('update', $e); @@ -257,7 +254,7 @@ function update_invoke_post_update($function, &$context) { unset($variables['backtrace']); $ret['#abort'] = [ 'success' => FALSE, - 'query' => t('%type: @message in %function (line %line of %file).', $variables) + 'query' => t('%type: @message in %function (line %line of %file).', $variables), ]; } } diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 1b62360..5b3de7d 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -662,7 +662,7 @@ function hook_update_N(&$sandbox) { /** * Executes an update which is intended to update data, like entities. * - * These implementations have to be placed in a $module.post_update.php file. + * These implementations have to be placed in a MODULE.post_update.php file. * * These updates are executed after all hook_update_N() implementations. At this * stage Drupal is already fully repaired so you can use any API as you wish. @@ -679,12 +679,12 @@ function hook_update_N(&$sandbox) { * * @throws \Drupal\Core\Utility\UpdateException|PDOException * In case of error, update hooks should throw an instance of - * Drupal\Core\Utility\UpdateException with a meaningful message for the user. - * If a database query fails for whatever reason, it will throw a + * \Drupal\Core\Utility\UpdateException with a meaningful message for the + * user. If a database query fails for whatever reason, it will throw a * PDOException. * * @return string|null - * Optionally, post_update_Name hooks may return a translated string that will + * Optionally, post_update_NAME() hooks may return a translated string that will * be displayed to the user after the update has completed. If no message is * returned, no message will be presented to the user. * diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php index df662f2..1c4ade4 100644 --- a/core/lib/Drupal/Core/Update/UpdateRegistry.php +++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php @@ -14,8 +14,8 @@ /** * Provides all and missing hook_$updateName_NAME implementations. * - * It therefore scans for functions in $module.$updateName.inc named like - * $module_$updateName_NAME with NAME being a machine name. + * It therefore scans for functions in MODULE.$updateName.php named like + * MODULE_$updateName_NAME with NAME being a machine name. */ class UpdateRegistry { @@ -54,6 +54,27 @@ class UpdateRegistry { */ protected $includeTests = NULL; + /** + * The site path. + * + * @var string + */ + protected $sitePath; + + /** + * Constructs a new UpdateRegistry. + * + * @param string $root + * The app root. + * @param string $site_path + * The site path. + * @param string[] $enabled_modules + * A list of enabled modules. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value + * The key value store. + * @param bool|NULL $include_tests + * (optional) A flag whether to include tests in the scanning of modules. + */ public function __construct($root, $site_path, array $enabled_modules, KeyValueStoreInterface $key_value, $include_tests = NULL) { $this->root = $root; $this->sitePath = $site_path; @@ -66,6 +87,7 @@ public function __construct($root, $site_path, array $enabled_modules, KeyValueS * Gets all available update functions. * * @return callable[] + * A list of update functions. */ protected function getAvailableUpdateFunctions() { $regexp = '/^(?.+)_' . $this->updateName . '_(?.+)$/'; @@ -87,6 +109,7 @@ protected function getAvailableUpdateFunctions() { * Find all update functions that haven't been executed. * * @return callable[] + * A list of update functions. */ public function getPendingUpdateFunctions() { // We need a) the list of active modules (we get that from the config @@ -95,7 +118,8 @@ public function getPendingUpdateFunctions() { $this->scanExtensionsAndLoadUpdateFiles(); - // First figure out which hook_{$this->updateName}_NAME got executed already. + // First figure out which hook_{$this->updateName}_NAME got executed + // already. $existing_update_functions = $this->keyValue->get('existing_updates', []); $available_update_functions = $this->getAvailableUpdateFunctions(); @@ -166,12 +190,29 @@ public function getPendingUpdateInformation() { } /** + * Registers that a specific update got executed. + * + * @param string $function_name + * The executed update function. + * + * @return $this + */ + public function registerInvokedUpdate($function_name) { + $executed_updates = $this->keyValue->get('existing_updates', []); + $executed_updates[] = $function_name; + $this->keyValue->set('existing_updates', $executed_updates); + + return $this; + } + + /** * Returns all available updates for a given module. * * @param string $module_name * The module name. * * @return callable[] + * A list of update functions. */ public function getModuleUpdateFunctions($module_name) { $this->scanExtensionsAndLoadUpdateFiles(); diff --git a/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php new file mode 100644 index 0000000..0c076cb --- /dev/null +++ b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php @@ -0,0 +1,30 @@ +container->get('app.root'), $this->container->get('site.path'), array_keys($this->container->get('module_handler')->getModuleList()), $this->container->get('keyvalue')->get('post_update')); + } + +} diff --git a/core/modules/block/block.install b/core/modules/block/block.install index a831fcb..f296e0d 100644 --- a/core/modules/block/block.install +++ b/core/modules/block/block.install @@ -96,5 +96,11 @@ function block_update_8001() { } /** + * Placeholder for the previous 8002 update. + */ +function block_update_8002() { +} + +/** * @} End of "addtogroup updates-8.0.0-beta". */ diff --git a/core/modules/block/block.post_update.php b/core/modules/block/block.post_update.php index f6cbbd5..f191ce2 100644 --- a/core/modules/block/block.post_update.php +++ b/core/modules/block/block.post_update.php @@ -14,6 +14,16 @@ * Disable all blocks with missing context IDs in block_update_8001(). */ function block_post_update_disable_blocks_with_missing_contexts() { + // Don't execute the function if block_update_8002() got executed already, + // which used to do the same. Note: Its okay to check here, because + // update_do_one() does not update the installed schema version until the + // batch is finished. + $module_schema = drupal_get_installed_schema_version('block'); + + if ($module_schema == 8002) { + return; + } + $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []); $block_ids = array_keys($block_update_8001); diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 0378f95..db0492b 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -76,6 +76,13 @@ class DbUpdateController extends ControllerBase { protected $root; /** + * The post update registry. + * + * @var \Drupal\Core\Update\UpdateRegistry + */ + protected $postUpdateRegistry; + + /** * Constructs a new UpdateController. * * @param string $root @@ -92,8 +99,10 @@ class DbUpdateController extends ControllerBase { * The current user. * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer * The bare HTML page renderer. + * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry + * The post update registry. */ - public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) { + public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, UpdateRegistry $post_update_registry) { $this->root = $root; $this->keyValueExpirableFactory = $key_value_expirable_factory; $this->cache = $cache; @@ -101,6 +110,7 @@ public function __construct($root, KeyValueExpirableFactoryInterface $key_value_ $this->moduleHandler = $module_handler; $this->account = $account; $this->bareHtmlPageRenderer = $bare_html_page_renderer; + $this->postUpdateRegistry = $post_update_registry; } /** @@ -114,7 +124,8 @@ public static function create(ContainerInterface $container) { $container->get('state'), $container->get('module_handler'), $container->get('current_user'), - $container->get('bare_html_page_renderer') + $container->get('bare_html_page_renderer'), + $container->get('update.post_update_registry') ); } @@ -260,14 +271,14 @@ protected function selection(Request $request) { $starting_updates = array(); $incompatible_updates_exist = FALSE; + $updates_per_module = []; foreach (['update', 'post_update'] as $update_type) { switch ($update_type) { case 'update': $updates = update_get_update_list(); break; case 'post_update': - $after_update_registry = $this->getPostUpdateRegistry(); - $updates = $after_update_registry->getPendingUpdateInformation(); + $updates = $this->postUpdateRegistry->getPendingUpdateInformation(); break; } foreach ($updates as $module => $update) { @@ -283,13 +294,17 @@ protected function selection(Request $request) { continue; } if (!empty($update['pending'])) { + $updates_per_module += [$module => []]; + $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']); $build['start'][$module] = array( '#type' => 'hidden', '#value' => $update['start'], ); - $build['start'][$module . '_' . $update_type] = array( + // Store the previous items in order to merge normal updates and + // post_update functions together. + $build['start'][$module] = array( '#theme' => 'item_list', - '#items' => $update['pending'], + '#items' => $updates_per_module[$module], '#title' => $module . ' module', ); @@ -589,8 +604,7 @@ protected function triggerBatch(Request $request) { } } - $post_update_registry = $this->getPostUpdateRegistry(); - $post_updates = $post_update_registry->getPendingUpdateFunctions(); + $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions(); if ($post_updates) { // Now we rebuild all caches and after that execute the hook_post_update() @@ -615,15 +629,6 @@ protected function triggerBatch(Request $request) { } /** - * Gets the update registry instance for the hook_post_update updates. - * - * @return \Drupal\Core\Update\UpdateRegistry - */ - protected function getPostUpdateRegistry() { - return new UpdateRegistry($this->root, \Drupal::service('site.path'), array_keys($this->moduleHandler()->getModuleList()), \Drupal::keyValue('post_update')); - } - - /** * Finishes the update process and stores the results for eventual display. * * After the updates run, all caches are flushed. The update results are diff --git a/core/modules/system/src/Tests/Module/InstallTest.php b/core/modules/system/src/Tests/Module/InstallTest.php index a1d6927..5f1a091 100644 --- a/core/modules/system/src/Tests/Module/InstallTest.php +++ b/core/modules/system/src/Tests/Module/InstallTest.php @@ -63,9 +63,11 @@ public function testRequiredModuleSchemaVersions() { * Ensures that post update functions are removed on uninstall. */ public function testUninstallPostUpdateFunctions() { + \Drupal::service('module_installer')->uninstall(['module_test']); + $post_update_key_value = \Drupal::keyValue('post_update'); $existing_updates = $post_update_key_value->get('existing_updates', []); - $this->assertTrue(in_array('module_test_post_update_test', $existing_updates)); + $this->assertFalse(in_array('module_test_post_update_test', $existing_updates)); } /** diff --git a/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php index 4d3ac3d..7b84670 100644 --- a/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php +++ b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php @@ -25,6 +25,17 @@ protected function setDatabaseDumpFiles() { } /** + * {@inheritdoc} + */ + protected function doSelectionTest() { + parent::doSelectionTest(); + + // Ensure that normal and post_update updates are merged together on the + // selection page. + $this->assertRaw(''); + } + + /** * Tests hook_post_update_NAME(). */ public function testPostUpdate() { @@ -38,9 +49,12 @@ public function testPostUpdate() { ]; $this->assertIdentical($updates, \Drupal::state()->get('post_update_test_execution', [])); - $key_value = \Drupal::keyValue('update__post_update'); + $key_value = \Drupal::keyValue('post_update'); array_unshift($updates, 'block_post_update_disable_blocks_with_missing_contexts'); $this->assertEqual($updates, $key_value->get('existing_updates')); + + $this->drupalGet('update.php/selection'); + $this->assertText('No pending updates.'); } } diff --git a/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php index 1b2d186..375b50d 100644 --- a/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php +++ b/core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php @@ -2,7 +2,8 @@ /** * @file - * Partial database to mimic the installation of the update_test_post_update module. + * Partial database to mimic the installation of the update_test_post_update + * module. */ use Drupal\Core\Database\Database; diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install new file mode 100644 index 0000000..f754d94 --- /dev/null +++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.install @@ -0,0 +1,7 @@ +assertEquals(['module_b_post_update_a'], array_values($update_registry->getModuleUpdateFunctions('module_b'))); } + /** + * @covers ::registerInvokedUpdate + */ + public function testRegisterInvokedUpdateWithoutExistingUpdates() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value->set('existing_updates', ['module_a_post_update_a'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + $update_registry->registerInvokedUpdate('module_a_post_update_a'); + } + + /** + * @covers ::registerInvokedUpdate + */ + public function testRegisterInvokedUpdateWithExistingUpdates() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']); + $key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_post_update_a'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + $update_registry->registerInvokedUpdate('module_a_post_update_a'); + } + }