Loading core/includes/update.inc +8 −1 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ use Drupal\Component\Graph\Graph; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Utility\Error; use Drupal\Core\Update\EquivalentUpdate; /** * Returns whether the minimum schema requirement has been satisfied. Loading Loading @@ -166,7 +167,13 @@ function update_do_one($module, $number, $dependency_map, &$context) { } $ret = []; if (function_exists($function)) { $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate($module, $number); if ($equivalent_update instanceof EquivalentUpdate) { $ret['results']['query'] = $equivalent_update->toSkipMessage(); $ret['results']['success'] = TRUE; $context['sandbox']['#finished'] = TRUE; } elseif (function_exists($function)) { try { $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; Loading core/lib/Drupal/Core/Extension/module.api.php +76 −0 Original line number Diff line number Diff line Loading @@ -667,6 +667,82 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * See the @link batch Batch operations topic @endlink for more information on * how to use the Batch API. * * @section sec_equivalent_updates Multiple upgrade paths * There are situations where changes require a hook_update_N() to be applied to * different major branches. This results in more than one upgrade path from the * current major version to the next major version. * * For example, if an update is added to 11.1.0 and 10.4.0, then a site on * 10.3.7 can update either to 10.4.0 and from there to 11.1.0, or directly from * 10.3.7 to 11.1.1. In one case, the update will run on the 10.x code base, and * in another on the 11.x code base, but the update system needs to ensure that * it doesn't run twice on the same site. * * hook_update_N() numbers are sequential integers, and numbers lower than the * modules current schema version will never be run. This means once a site has * run an update, for example, 11100, it will not run a later update added as * 10400. Backporting of updates therefore needs to allow 'space' for the 10.4.x * codebase to include updates which don't exist in 11.x (for example to ensure * a later 11.x update goes smoothly). * * To resolve this, when handling potential backports of updates between major * branches, we use different update numbers for each branch, but record the * relationship between those updates in the older branches. This is best * explained by an example showing the different branches updates could be * applied to: * - The first update, system_update_10300 is applied to 10.3.0. * - Then, 11.0.0 is released with the update removed, * system_update_last_removed() is added which returns 10300. * - The next update, system_update_11100, is applied to 11.1.x only. * - Then 10.4.0 and 11.1.0 are released. system_update_11100 is not backported * to 11.0.x or any 10.x branch. * - Finally, a critical data loss update is necessary. The bug-fix supported * branches are 11.1.x, 11.0.x, and 10.4.x. This results in adding the updates * system_update_10400 (10.4.x), system_update_11000 (11.0.x) and * system_update_11101 (11.1.x) and making the 10.4.1, 11.0.1 and 11.1.1 * releases. * * This is a list of the example releases and the updates they contain: * - 10.3.0: system_update_10300 * - 10.4.1: system_update_10300 and system_update_10400 (equivalent to * system_update_11101) * - 11.0.0: No updates * - 11.0.1: system_update_11000 (equivalent to system_update_11101) * - 11.1.0: system_update_11100 * - 11.1.1: system_update_11100 and system_update_11101 * * In this situation, sites on 10.4.1 or 11.0.1 will be required to update to * versions that contain system_update_11101. For example, a site on 10.4.1 * would not be able to update to 11.0.0, because that would result in it going * 'backwards' in terms of database schema, but it would be able to update to * 11.1.1. The same is true for a site on 11.0.1. * * The following examples show how to implement a hook_update_N() that must be * skipped in a future update process. * * Future updates can be marked as equivalent by adding the following code to an * update. * @code * function my_module_update_10400() { * \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(11101, '11.1.1'); * * // The rest of the update function. * } * @endcode * * At the moment we need to add defensive coding in the future update to ensure * it is skipped. * @code * function my_module_update_11101() { * $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate(); * if ($equivalent_update instanceof \Drupal\Core\Update\EquivalentUpdate) { * return $equivalent_update->toSkipMessage(); * } * * // The rest of the update function. * } * @encode * * @param array $sandbox * Stores information for batch updates. See above for more information. * Loading core/lib/Drupal/Core/Update/EquivalentUpdate.php 0 → 100644 +47 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Update; use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Value object to hold information about an equivalent update. * * @see module.api.php */ final class EquivalentUpdate { /** * Constructs a EquivalentUpdate object. * * @param string $module * The module the update is for. * @param int $future_update * The equivalent future update. * @param int $ran_update * The update that already ran and registered the equivalent update. * @param string $future_version * The future version that has the expected update. */ public function __construct( public readonly string $module, public readonly int $future_update, public readonly int $ran_update, public readonly string $future_version, ) { } /** * Creates a message to explain why an update has been skipped. * * @return \Drupal\Core\StringTranslation\TranslatableMarkup * An message explaining why an update has been skipped. */ public function toSkipMessage(): TranslatableMarkup { return new TranslatableMarkup( 'Update @number for the @module module has been skipped because the equivalent change was already made in update @ran_update.', ['@number' => $this->future_update, '@module' => $this->module, '@ran_update' => $this->ran_update] ); } } core/lib/Drupal/Core/Update/UpdateHookRegistry.php +153 −5 Original line number Diff line number Diff line Loading @@ -15,6 +15,11 @@ class UpdateHookRegistry { */ public const SCHEMA_UNINSTALLED = -1; /** * Regular expression to match all possible defined hook_update_N(). */ private const FUNC_NAME_REGEXP = '/^(?<module>.+)_update_(?<version>\d+)$/'; /** * A list of enabled modules. * Loading @@ -23,12 +28,28 @@ class UpdateHookRegistry { protected $enabledModules; /** * The key value storage. * The system.schema key value storage. * * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface */ protected $keyValue; /** * The core.equivalent_updates key value storage. * * The key value keys are modules and the value is an array of equivalent * updates with the following shape: * - The array keys are the equivalent future update numbers. * - The value is an array containing two keys: * - 'ran_update': The update that registered the future update as an * equivalent. * - 'future_version_string': The version that provides the future update. * * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface * @see module.api.php */ protected KeyValueStoreInterface $equivalentUpdates; /** * A static cache of schema currentVersions per module. * Loading Loading @@ -69,6 +90,7 @@ public function __construct(array $module_list, KeyValueStoreInterface|KeyValueF } $this->enabledModules = array_keys($module_list); $this->keyValue = $key_value_factory->get('system.schema'); $this->equivalentUpdates = $key_value_factory->get('core.equivalent_updates'); } /** Loading @@ -89,9 +111,6 @@ public function getAvailableUpdates(string $module): array { $this->allAvailableSchemaVersions[$enabled_module] = []; } // Prepare regular expression to match all possible defined // hook_update_N(). $regexp = '/^(?<module>.+)_update_(?<version>\d+)$/'; $functions = get_defined_functions(); // Narrow this down to functions ending with an integer, since all // hook_update_N() functions end this way, and there are other Loading @@ -102,7 +121,7 @@ public function getAvailableUpdates(string $module): array { foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { // If this function is a module update function, add it to the list of // module updates. if (preg_match($regexp, $function, $matches)) { if (preg_match(self::FUNC_NAME_REGEXP, $function, $matches)) { $this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version']; } } Loading Loading @@ -145,6 +164,7 @@ public function getInstalledVersion(string $module): int { */ public function setInstalledVersion(string $module, int $version): self { $this->keyValue->set($module, $version); $this->deleteEquivalentUpdate($module, $version); return $this; } Loading @@ -156,6 +176,7 @@ public function setInstalledVersion(string $module, int $version): self { */ public function deleteInstalledVersion(string $module): void { $this->keyValue->delete($module); $this->equivalentUpdates->delete($module); } /** Loading @@ -170,4 +191,131 @@ public function getAllInstalledVersions(): array { return $this->keyValue->getAll(); } /** * Marks a future update as equivalent to the current update running. * * Updates can be marked as equivalent when they are backported to a * previous, but still supported, major version. For example: * - A 2.x hook_update_N() would be added as normal, for example: * MODULE_update_2005(). * - When that same update is backported to 1.x, it is given its own update * number, for example: MODULE_update_1040(). In this update, a call to * @code * \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(2005, '2.10') * @endcode * is added to ensure that a site that has run this update does not run * MODULE_update_2005(). * * @param int $future_update_number * The future update number. * @param string $future_version_string * The version that contains the future update. */ public function markFutureUpdateEquivalent(int $future_update_number, string $future_version_string): void { [$module, $ran_update_number] = $this->determineModuleAndVersion(); if ($ran_update_number > $future_update_number) { throw new \LogicException(sprintf( 'Cannot mark the update %d as an equivalent since it is less than the current update %d for the %s module ', $future_update_number, $ran_update_number, $module )); } $data = $this->equivalentUpdates->get($module, []); // It does not matter if $data[$future_update_number] is already set. If two // updates are causing the same update to be marked as equivalent then the // latest information is the correct information to use. $data[$future_update_number] = [ 'ran_update' => $ran_update_number, 'future_version_string' => $future_version_string, ]; $this->equivalentUpdates->set($module, $data); } /** * Gets the EquivalentUpdate object for an update. * * @param string|null $module * The module providing the update. If this is NULL the update to check will * be determined from the backtrace. * @param int|null $version * The update to check. If this is NULL the update to check will * be determined from the backtrace. * * @return \Drupal\Core\Update\EquivalentUpdate|null * A value object with the equivalent update information or NULL if the * update does not have an equivalent update. */ public function getEquivalentUpdate(?string $module = NULL, ?int $version = NULL): ?EquivalentUpdate { if ($module === NULL || $version === NULL) { [$module, $version] = $this->determineModuleAndVersion(); } $data = $this->equivalentUpdates->get($module, []); if (isset($data[$version]['ran_update'])) { return new EquivalentUpdate( $module, $version, $data[$version]['ran_update'], $data[$version]['future_version_string'], ); } return NULL; } /** * Determines the module and update number from the stack trace. * * @return array<string, int> * An array with two values. The first value is the module name and the * second value is the update number. */ private function determineModuleAndVersion(): array { $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); for ($i = 0; $i < count($stack); $i++) { if (preg_match(self::FUNC_NAME_REGEXP, $stack[$i]['function'], $matches)) { return [$matches['module'], $matches['version']]; } } throw new \BadMethodCallException(__METHOD__ . ' must be called from a hook_update_N() function'); } /** * Removes an equivalent update. * * @param string $module * The module providing the update. * @param int $version * The equivalent update to remove. * * @return bool * TRUE if an equivalent update was removed, or FALSE if it was not. */ protected function deleteEquivalentUpdate(string $module, int $version): bool { $data = $this->equivalentUpdates->get($module, []); if (isset($data[$version])) { unset($data[$version]); if (empty($data)) { $this->equivalentUpdates->delete($module); } else { $this->equivalentUpdates->set($module, $data); } return TRUE; } return FALSE; } /** * Returns the equivalent update information for all modules. * * @return array<string, array<int, array{ran_update:int, future_version_string: string}>> * Array of modules as the keys and values as arrays of equivalent update * information. */ public function getAllEquivalentUpdates(): array { return $this->equivalentUpdates->getAll(); } } core/misc/cspell/dictionary.txt +3 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,9 @@ autowired autowiring backlink backlinks backported backporting backports bakeware barbar barchart Loading Loading
core/includes/update.inc +8 −1 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ use Drupal\Component\Graph\Graph; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Utility\Error; use Drupal\Core\Update\EquivalentUpdate; /** * Returns whether the minimum schema requirement has been satisfied. Loading Loading @@ -166,7 +167,13 @@ function update_do_one($module, $number, $dependency_map, &$context) { } $ret = []; if (function_exists($function)) { $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate($module, $number); if ($equivalent_update instanceof EquivalentUpdate) { $ret['results']['query'] = $equivalent_update->toSkipMessage(); $ret['results']['success'] = TRUE; $context['sandbox']['#finished'] = TRUE; } elseif (function_exists($function)) { try { $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; Loading
core/lib/Drupal/Core/Extension/module.api.php +76 −0 Original line number Diff line number Diff line Loading @@ -667,6 +667,82 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * See the @link batch Batch operations topic @endlink for more information on * how to use the Batch API. * * @section sec_equivalent_updates Multiple upgrade paths * There are situations where changes require a hook_update_N() to be applied to * different major branches. This results in more than one upgrade path from the * current major version to the next major version. * * For example, if an update is added to 11.1.0 and 10.4.0, then a site on * 10.3.7 can update either to 10.4.0 and from there to 11.1.0, or directly from * 10.3.7 to 11.1.1. In one case, the update will run on the 10.x code base, and * in another on the 11.x code base, but the update system needs to ensure that * it doesn't run twice on the same site. * * hook_update_N() numbers are sequential integers, and numbers lower than the * modules current schema version will never be run. This means once a site has * run an update, for example, 11100, it will not run a later update added as * 10400. Backporting of updates therefore needs to allow 'space' for the 10.4.x * codebase to include updates which don't exist in 11.x (for example to ensure * a later 11.x update goes smoothly). * * To resolve this, when handling potential backports of updates between major * branches, we use different update numbers for each branch, but record the * relationship between those updates in the older branches. This is best * explained by an example showing the different branches updates could be * applied to: * - The first update, system_update_10300 is applied to 10.3.0. * - Then, 11.0.0 is released with the update removed, * system_update_last_removed() is added which returns 10300. * - The next update, system_update_11100, is applied to 11.1.x only. * - Then 10.4.0 and 11.1.0 are released. system_update_11100 is not backported * to 11.0.x or any 10.x branch. * - Finally, a critical data loss update is necessary. The bug-fix supported * branches are 11.1.x, 11.0.x, and 10.4.x. This results in adding the updates * system_update_10400 (10.4.x), system_update_11000 (11.0.x) and * system_update_11101 (11.1.x) and making the 10.4.1, 11.0.1 and 11.1.1 * releases. * * This is a list of the example releases and the updates they contain: * - 10.3.0: system_update_10300 * - 10.4.1: system_update_10300 and system_update_10400 (equivalent to * system_update_11101) * - 11.0.0: No updates * - 11.0.1: system_update_11000 (equivalent to system_update_11101) * - 11.1.0: system_update_11100 * - 11.1.1: system_update_11100 and system_update_11101 * * In this situation, sites on 10.4.1 or 11.0.1 will be required to update to * versions that contain system_update_11101. For example, a site on 10.4.1 * would not be able to update to 11.0.0, because that would result in it going * 'backwards' in terms of database schema, but it would be able to update to * 11.1.1. The same is true for a site on 11.0.1. * * The following examples show how to implement a hook_update_N() that must be * skipped in a future update process. * * Future updates can be marked as equivalent by adding the following code to an * update. * @code * function my_module_update_10400() { * \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(11101, '11.1.1'); * * // The rest of the update function. * } * @endcode * * At the moment we need to add defensive coding in the future update to ensure * it is skipped. * @code * function my_module_update_11101() { * $equivalent_update = \Drupal::service('update.update_hook_registry')->getEquivalentUpdate(); * if ($equivalent_update instanceof \Drupal\Core\Update\EquivalentUpdate) { * return $equivalent_update->toSkipMessage(); * } * * // The rest of the update function. * } * @encode * * @param array $sandbox * Stores information for batch updates. See above for more information. * Loading
core/lib/Drupal/Core/Update/EquivalentUpdate.php 0 → 100644 +47 −0 Original line number Diff line number Diff line <?php namespace Drupal\Core\Update; use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Value object to hold information about an equivalent update. * * @see module.api.php */ final class EquivalentUpdate { /** * Constructs a EquivalentUpdate object. * * @param string $module * The module the update is for. * @param int $future_update * The equivalent future update. * @param int $ran_update * The update that already ran and registered the equivalent update. * @param string $future_version * The future version that has the expected update. */ public function __construct( public readonly string $module, public readonly int $future_update, public readonly int $ran_update, public readonly string $future_version, ) { } /** * Creates a message to explain why an update has been skipped. * * @return \Drupal\Core\StringTranslation\TranslatableMarkup * An message explaining why an update has been skipped. */ public function toSkipMessage(): TranslatableMarkup { return new TranslatableMarkup( 'Update @number for the @module module has been skipped because the equivalent change was already made in update @ran_update.', ['@number' => $this->future_update, '@module' => $this->module, '@ran_update' => $this->ran_update] ); } }
core/lib/Drupal/Core/Update/UpdateHookRegistry.php +153 −5 Original line number Diff line number Diff line Loading @@ -15,6 +15,11 @@ class UpdateHookRegistry { */ public const SCHEMA_UNINSTALLED = -1; /** * Regular expression to match all possible defined hook_update_N(). */ private const FUNC_NAME_REGEXP = '/^(?<module>.+)_update_(?<version>\d+)$/'; /** * A list of enabled modules. * Loading @@ -23,12 +28,28 @@ class UpdateHookRegistry { protected $enabledModules; /** * The key value storage. * The system.schema key value storage. * * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface */ protected $keyValue; /** * The core.equivalent_updates key value storage. * * The key value keys are modules and the value is an array of equivalent * updates with the following shape: * - The array keys are the equivalent future update numbers. * - The value is an array containing two keys: * - 'ran_update': The update that registered the future update as an * equivalent. * - 'future_version_string': The version that provides the future update. * * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface * @see module.api.php */ protected KeyValueStoreInterface $equivalentUpdates; /** * A static cache of schema currentVersions per module. * Loading Loading @@ -69,6 +90,7 @@ public function __construct(array $module_list, KeyValueStoreInterface|KeyValueF } $this->enabledModules = array_keys($module_list); $this->keyValue = $key_value_factory->get('system.schema'); $this->equivalentUpdates = $key_value_factory->get('core.equivalent_updates'); } /** Loading @@ -89,9 +111,6 @@ public function getAvailableUpdates(string $module): array { $this->allAvailableSchemaVersions[$enabled_module] = []; } // Prepare regular expression to match all possible defined // hook_update_N(). $regexp = '/^(?<module>.+)_update_(?<version>\d+)$/'; $functions = get_defined_functions(); // Narrow this down to functions ending with an integer, since all // hook_update_N() functions end this way, and there are other Loading @@ -102,7 +121,7 @@ public function getAvailableUpdates(string $module): array { foreach (preg_grep('/_\d+$/', $functions['user']) as $function) { // If this function is a module update function, add it to the list of // module updates. if (preg_match($regexp, $function, $matches)) { if (preg_match(self::FUNC_NAME_REGEXP, $function, $matches)) { $this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version']; } } Loading Loading @@ -145,6 +164,7 @@ public function getInstalledVersion(string $module): int { */ public function setInstalledVersion(string $module, int $version): self { $this->keyValue->set($module, $version); $this->deleteEquivalentUpdate($module, $version); return $this; } Loading @@ -156,6 +176,7 @@ public function setInstalledVersion(string $module, int $version): self { */ public function deleteInstalledVersion(string $module): void { $this->keyValue->delete($module); $this->equivalentUpdates->delete($module); } /** Loading @@ -170,4 +191,131 @@ public function getAllInstalledVersions(): array { return $this->keyValue->getAll(); } /** * Marks a future update as equivalent to the current update running. * * Updates can be marked as equivalent when they are backported to a * previous, but still supported, major version. For example: * - A 2.x hook_update_N() would be added as normal, for example: * MODULE_update_2005(). * - When that same update is backported to 1.x, it is given its own update * number, for example: MODULE_update_1040(). In this update, a call to * @code * \Drupal::service('update.update_hook_registry')->markFutureUpdateEquivalent(2005, '2.10') * @endcode * is added to ensure that a site that has run this update does not run * MODULE_update_2005(). * * @param int $future_update_number * The future update number. * @param string $future_version_string * The version that contains the future update. */ public function markFutureUpdateEquivalent(int $future_update_number, string $future_version_string): void { [$module, $ran_update_number] = $this->determineModuleAndVersion(); if ($ran_update_number > $future_update_number) { throw new \LogicException(sprintf( 'Cannot mark the update %d as an equivalent since it is less than the current update %d for the %s module ', $future_update_number, $ran_update_number, $module )); } $data = $this->equivalentUpdates->get($module, []); // It does not matter if $data[$future_update_number] is already set. If two // updates are causing the same update to be marked as equivalent then the // latest information is the correct information to use. $data[$future_update_number] = [ 'ran_update' => $ran_update_number, 'future_version_string' => $future_version_string, ]; $this->equivalentUpdates->set($module, $data); } /** * Gets the EquivalentUpdate object for an update. * * @param string|null $module * The module providing the update. If this is NULL the update to check will * be determined from the backtrace. * @param int|null $version * The update to check. If this is NULL the update to check will * be determined from the backtrace. * * @return \Drupal\Core\Update\EquivalentUpdate|null * A value object with the equivalent update information or NULL if the * update does not have an equivalent update. */ public function getEquivalentUpdate(?string $module = NULL, ?int $version = NULL): ?EquivalentUpdate { if ($module === NULL || $version === NULL) { [$module, $version] = $this->determineModuleAndVersion(); } $data = $this->equivalentUpdates->get($module, []); if (isset($data[$version]['ran_update'])) { return new EquivalentUpdate( $module, $version, $data[$version]['ran_update'], $data[$version]['future_version_string'], ); } return NULL; } /** * Determines the module and update number from the stack trace. * * @return array<string, int> * An array with two values. The first value is the module name and the * second value is the update number. */ private function determineModuleAndVersion(): array { $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); for ($i = 0; $i < count($stack); $i++) { if (preg_match(self::FUNC_NAME_REGEXP, $stack[$i]['function'], $matches)) { return [$matches['module'], $matches['version']]; } } throw new \BadMethodCallException(__METHOD__ . ' must be called from a hook_update_N() function'); } /** * Removes an equivalent update. * * @param string $module * The module providing the update. * @param int $version * The equivalent update to remove. * * @return bool * TRUE if an equivalent update was removed, or FALSE if it was not. */ protected function deleteEquivalentUpdate(string $module, int $version): bool { $data = $this->equivalentUpdates->get($module, []); if (isset($data[$version])) { unset($data[$version]); if (empty($data)) { $this->equivalentUpdates->delete($module); } else { $this->equivalentUpdates->set($module, $data); } return TRUE; } return FALSE; } /** * Returns the equivalent update information for all modules. * * @return array<string, array<int, array{ran_update:int, future_version_string: string}>> * Array of modules as the keys and values as arrays of equivalent update * information. */ public function getAllEquivalentUpdates(): array { return $this->equivalentUpdates->getAll(); } }
core/misc/cspell/dictionary.txt +3 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,9 @@ autowired autowiring backlink backlinks backported backporting backports bakeware barbar barchart Loading