diff --git a/core/includes/update.inc b/core/includes/update.inc index 0869f1f..f0020ee 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -220,6 +220,11 @@ function update_do_one($module, $number, $dependency_map, &$context) { /** * Executes a single hook_update_after_$name(). + * + * @param string $function + * The function name, that should be executed. + * @param array $context + * The batch context array. */ function update_after_do_one($function, &$context) { $ret = []; @@ -229,6 +234,11 @@ function update_after_do_one($function, &$context) { try { $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; + + $key_value_store = \Drupal::keyValue('update_after'); + $executed_updates = $key_value_store->get('core.update__update_after', []); + $executed_updates[] = $function; + $key_value_store->set('core.update__update_after', $executed_updates); } // @TODO We may want to do different error handling for different // exception types, but for now we'll just log the exception and diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php index cf4bcc9..58ec918 100644 --- a/core/lib/Drupal/Core/Update/UpdateRegistry.php +++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php @@ -12,7 +12,10 @@ use Drupal\Core\KeyValueStore\KeyValueStoreInterface; /** - * Provides all and missing hook_update_data_N implementations. + * Provides all and missing hook_update_$updateName_X implementations. + * + * It therefore scans for functions in $module.$updateName.inc named like + * $module_update_$updateName_X with X being a machine name. */ class UpdateRegistry { @@ -81,7 +84,7 @@ protected function getAvailableUpdateFunctions() { } /** - * Gets the non already executed update functions. + * Find all update functions that haven't been executed * * @return callable[] */ @@ -91,11 +94,13 @@ public function getMissingUpdateFunctions() { // discovery for that. // Scan the module list. - // We don't support install profiles at that point? $extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath); $module_extensions = $extension_discovery->scan('module'); - $this->loadUpdateFiles($module_extensions); + $profile_extensions = $extension_discovery->scan('profile'); + $extensions = array_merge($module_extensions, $profile_extensions); + + $this->loadUpdateFiles($extensions); // First figure out which hook_update got executed already. $existing_update_functions = $this->keyValue->get('core.update__' . $this->updateName, []); @@ -127,5 +132,38 @@ protected function loadUpdateFile(Extension $module) { } } + /** + * Returns a list of all the pending database updates. + * + * @return array[] + * An associative array keyed by module name which contains all information + * about database updates that need to be run, and any updates that are not + * going to proceed due to missing requirements. The system module will + * always be listed first. + * + * The subarray for each module can contain the following keys: + * - start: The starting update that is to be processed. If this does not + * exist then do not process any updates for this module as there are + * other requirements that need to be resolved. + * - pending: An array of all the pending updates for the module including + * the update number and the description from source code comment for + * each update function. This array is keyed by the update number. + */ + public function getMissingUpdateList() { + $functions = $this->getMissingUpdateFunctions(); + + $ret = []; + foreach ($functions as $function) { + list($module, $update) = explode("_{$this->updateName}_", $function); + // The description for an update comes from its Doxygen. + $func = new \ReflectionFunction($function); + $description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment()), ' '); + $ret[$module]['pending'][$update] = "$update - $description"; + if (!isset($ret[$module]['start'])) { + $ret[$module]['start'] = $update; + } + } + return $ret; + } } diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index fce7819..5b44880 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -258,35 +258,45 @@ protected function selection(Request $request) { // Ensure system.module's updates appear first. $build['start']['system'] = array(); - $updates = update_get_update_list(); $starting_updates = array(); - $incompatible_updates_exist = FALSE; - foreach ($updates as $module => $update) { - if (!isset($update['start'])) { - $build['start'][$module] = array( - '#type' => 'item', - '#title' => $module . ' module', - '#markup' => $update['warning'], - '#prefix' => '
', - '#suffix' => '
', - ); - $incompatible_updates_exist = TRUE; - continue; - } - if (!empty($update['pending'])) { - $starting_updates[$module] = $update['start']; - $build['start'][$module] = array( - '#type' => 'hidden', - '#value' => $update['start'], - ); - $build['start'][$module . '_updates'] = array( - '#theme' => 'item_list', - '#items' => $update['pending'], - '#title' => $module . ' module', - ); + foreach (['update', 'update_after'] as $update_type) { + switch ($update_type) { + case 'update': + $updates = update_get_update_list(); + break; + case 'update_after': + $after_update_registry = $this->getUpdateAfterRegistry(); + $updates = $after_update_registry->getMissingUpdateList(); + break; } - if (isset($update['pending'])) { - $count = $count + count($update['pending']); + $incompatible_updates_exist = FALSE; + foreach ($updates as $module => $update) { + if (!isset($update['start'])) { + $build['start'][$module] = array( + '#type' => 'item', + '#title' => $module . ' module', + '#markup' => $update['warning'], + '#prefix' => '
', + '#suffix' => '
', + ); + $incompatible_updates_exist = TRUE; + continue; + } + if (!empty($update['pending'])) { + $starting_updates[$module] = $update['start']; + $build['start'][$module] = array( + '#type' => 'hidden', + '#value' => $update['start'], + ); + $build['start'][$module . '_updates'] = array( + '#theme' => 'item_list', + '#items' => $update['pending'], + '#title' => $module . ' module', + ); + } + if (isset($update['pending'])) { + $count = $count + count($update['pending']); + } } } @@ -576,17 +586,19 @@ protected function triggerBatch(Request $request) { } } - // Now we rebuild all caches and after that execute the hook_update_after() - // functions. - $operations[] = ['drupal_flush_all_caches', []]; - $after_update_registry = $this->getUpdateAfterRegistry(); $after_updates = $after_update_registry->getMissingUpdateFunctions(); - foreach ($after_updates as $function) { - $operations[] = ['update_after_do_one', [$function]]; + if ($after_updates) { + // Now we rebuild all caches and after that execute the hook_update_after() + // functions. + $operations[] = ['drupal_flush_all_caches', []]; + foreach ($after_updates as $function) { + $operations[] = ['update_after_do_one', [$function]]; + } } + $batch['operations'] = $operations; $batch += array( 'title' => $this->t('Updating'), @@ -600,6 +612,11 @@ protected function triggerBatch(Request $request) { return batch_process(Url::fromUri('base://results'), Url::fromUri('base://start')); } + /** + * Gets the update registry instance for the hook_update_after updates. + * + * @return \Drupal\Core\Update\UpdateRegistry + */ protected function getUpdateAfterRegistry() { return new UpdateRegistry($this->root, \Drupal::service('site.path'), array_keys($this->moduleHandler()->getModuleList()), \Drupal::keyValue('update_after')); } diff --git a/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc b/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc index 90fe2ad..b45197a 100644 --- a/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc +++ b/core/modules/system/tests/modules/update_test_after/update_test_after.update_after.inc @@ -1,7 +1,7 @@ get('update_test_after_execution', []); @@ -10,7 +10,7 @@ function update_test_after_update_after_first() { } /** - * Implements hook_update_after(). + * Second update. */ function update_test_after_update_after_second() { $execution = \Drupal::state()->get('update_test_after_execution', []); @@ -19,7 +19,7 @@ function update_test_after_update_after_second() { } /** - * Implements hook_update_after(). + * Test1 update. */ function update_test_after_update_after_test1() { $execution = \Drupal::state()->get('update_test_after_execution', []); @@ -28,7 +28,7 @@ function update_test_after_update_after_test1() { } /** - * Implements hook_update_after(). + * Test2 update. */ function update_test_after_update_after_test0() { $execution = \Drupal::state()->get('update_test_after_execution', []); diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php index b4cd0b5..2206fc2 100644 --- a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php @@ -37,9 +37,15 @@ protected function setupBasicModules($directories) { $module_a = <<<'EOS' getMissingUpdateFunctions())); + + } + + /** + * @covers ::getMissingUpdateList + */ + public function testGetMissingUpdateList() { + $this->setupBasicModules([]); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('core.update__update_after', [])->wilLReturn([]); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $expected = []; + $expected['module_a']['pending']['a'] = 'a - Module A update A.'; + $expected['module_a']['pending']['b'] = 'b - Module A update B.'; + $expected['module_a']['start'] = 'a'; + $expected['module_b']['pending']['a'] = 'a - Module B update A.'; + $expected['module_b']['start'] = 'a'; + + $this->assertEquals($expected, $update_registry->getMissingUpdateList()); + } + + /** + * @covers ::getMissingUpdateList + */ + public function testGetMissingUpdateListWithExistingUpdates() { + $this->setupBasicModules([]); + + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('core.update__update_after', [])->wilLReturn(['module_a_update_after_a']); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $expected = []; + $expected['module_a']['pending']['b'] = 'b - Module A update B.'; + $expected['module_a']['start'] = 'b'; + $expected['module_b']['pending']['a'] = 'a - Module B update A.'; + $expected['module_b']['start'] = 'a'; + + $this->assertEquals($expected, $update_registry->getMissingUpdateList()); } }