class UpdateHookRegistry

Same name and namespace in other branches
  1. 9 core/lib/Drupal/Core/Update/UpdateHookRegistry.php \Drupal\Core\Update\UpdateHookRegistry
  2. 11.x core/lib/Drupal/Core/Update/UpdateHookRegistry.php \Drupal\Core\Update\UpdateHookRegistry

Provides module updates versions handling.

Hierarchy

Expanded class hierarchy of UpdateHookRegistry

4 files declare their use of UpdateHookRegistry
ModuleExtensionList.php in core/lib/Drupal/Core/Extension/ModuleExtensionList.php
ModuleInstaller.php in core/lib/Drupal/Core/Extension/ModuleInstaller.php
ModulesUninstallForm.php in core/modules/system/src/Form/ModulesUninstallForm.php
UpdateHookRegistryTest.php in core/tests/Drupal/Tests/Core/Update/UpdateHookRegistryTest.php
1 string reference to 'UpdateHookRegistry'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses UpdateHookRegistry
update.update_hook_registry in core/core.services.yml
Drupal\Core\Update\UpdateHookRegistry

File

core/lib/Drupal/Core/Update/UpdateHookRegistry.php, line 11

Namespace

Drupal\Core\Update
View source
class UpdateHookRegistry {
  
  /**
   * Indicates that a module has not been installed yet.
   */
  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.
   *
   * @var string[]
   */
  protected $enabledModules;
  
  /**
   * 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.
   *
   * Stores schema versions of the modules based on their defined hook_update_N
   * implementations.
   * Example:
   * ```
   * [
   *   'example_module' => [
   *     8000,
   *     8001,
   *     8002
   *   ]
   * ]
   * ```
   *
   * @var int[][]
   * @see \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates()
   */
  protected $allAvailableSchemaVersions = [];
  
  /**
   * Constructs a new UpdateHookRegistry.
   *
   * @param array $module_list
   *   An associative array whose keys are the names of installed modules.
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
   *   The key value factory.
   */
  public function __construct(array $module_list, KeyValueStoreInterface|KeyValueFactoryInterface $key_value_factory) {
    if ($module_list !== [] && array_is_list($module_list)) {
      @trigger_error('Calling ' . __METHOD__ . '() with the $enabled_modules argument is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use an associative array whose keys are the names of installed modules instead. See https://www.drupal.org/node/3423659', E_USER_DEPRECATED);
      $module_list = \Drupal::service('module_handler')->getModuleList();
    }
    if ($key_value_factory instanceof KeyValueStoreInterface) {
      @trigger_error('Calling ' . __METHOD__ . '() with the $key_value_factory argument as a KeyValueStoreInterface instead of a KeyValueFactoryInterface is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3423659', E_USER_DEPRECATED);
      $key_value_factory = \Drupal::service('keyvalue');
    }
    $this->enabledModules = array_keys($module_list);
    $this->keyValue = $key_value_factory->get('system.schema');
    $this->equivalentUpdates = $key_value_factory->get('core.equivalent_updates');
  }
  
  /**
   * Returns an array of available schema versions for a module.
   *
   * @param string $module
   *   A module name.
   *
   * @return int[]
   *   An array of available updates sorted by version. Empty array returned if
   *   no updates available.
   */
  public function getAvailableUpdates(string $module) : array {
    if (!isset($this->allAvailableSchemaVersions[$module])) {
      $this->allAvailableSchemaVersions[$module] = [];
      foreach ($this->enabledModules as $enabled_module) {
        $this->allAvailableSchemaVersions[$enabled_module] = [];
      }
      $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
      // possible functions which match '_update_'. We use preg_grep() here
      // since looping through all PHP functions can take significant page
      // execution time and this function is called on every administrative page
      // via system_requirements().
      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(self::FUNC_NAME_REGEXP, $function, $matches)) {
          $this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version'];
        }
      }
      // Ensure that updates are applied in numerical order.
      array_walk($this->allAvailableSchemaVersions, static function (&$module_updates) {
        sort($module_updates, SORT_NUMERIC);
      });
    }
    return $this->allAvailableSchemaVersions[$module];
  }
  
  /**
   * Returns the currently installed schema version for a module.
   *
   * @param string $module
   *   A module name.
   *
   * @return int
   *   The currently installed schema version, or self::SCHEMA_UNINSTALLED if the
   *   module is not installed.
   */
  public function getInstalledVersion(string $module) : int {
    return $this->keyValue
      ->get($module, self::SCHEMA_UNINSTALLED);
  }
  
  /**
   * Updates the installed version information for a module.
   *
   * @param string $module
   *   A module name.
   * @param int $version
   *   The new schema version.
   *
   * @return self
   *   Returns self to support chained method calls.
   */
  public function setInstalledVersion(string $module, int $version) : self {
    $this->keyValue
      ->set($module, $version);
    $this->deleteEquivalentUpdate($module, $version);
    return $this;
  }
  
  /**
   * Deletes the installed version information for the module.
   *
   * @param string $module
   *   The module name to delete.
   */
  public function deleteInstalledVersion(string $module) : void {
    $this->keyValue
      ->delete($module);
    $this->equivalentUpdates
      ->delete($module);
  }
  
  /**
   * Returns the currently installed schema version for all modules.
   *
   * @return int[]
   *   Array of modules as the keys and values as the currently installed
   *   schema version of corresponding module, or self::SCHEMA_UNINSTALLED if the
   *   module is not installed.
   */
  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();
  }

}

Members

Title Sort descending Modifiers Object type Summary
UpdateHookRegistry::$allAvailableSchemaVersions protected property A static cache of schema currentVersions per module.
UpdateHookRegistry::$enabledModules protected property A list of enabled modules.
UpdateHookRegistry::$equivalentUpdates protected property The core.equivalent_updates key value storage.
UpdateHookRegistry::$keyValue protected property The system.schema key value storage.
UpdateHookRegistry::deleteEquivalentUpdate protected function Removes an equivalent update.
UpdateHookRegistry::deleteInstalledVersion public function Deletes the installed version information for the module.
UpdateHookRegistry::determineModuleAndVersion private function Determines the module and update number from the stack trace.
UpdateHookRegistry::FUNC_NAME_REGEXP private constant Regular expression to match all possible defined hook_update_N().
UpdateHookRegistry::getAllEquivalentUpdates public function Returns the equivalent update information for all modules.
UpdateHookRegistry::getAllInstalledVersions public function Returns the currently installed schema version for all modules.
UpdateHookRegistry::getAvailableUpdates public function Returns an array of available schema versions for a module.
UpdateHookRegistry::getEquivalentUpdate public function Gets the EquivalentUpdate object for an update.
UpdateHookRegistry::getInstalledVersion public function Returns the currently installed schema version for a module.
UpdateHookRegistry::markFutureUpdateEquivalent public function Marks a future update as equivalent to the current update running.
UpdateHookRegistry::SCHEMA_UNINSTALLED public constant Indicates that a module has not been installed yet.
UpdateHookRegistry::setInstalledVersion public function Updates the installed version information for a module.
UpdateHookRegistry::__construct public function Constructs a new UpdateHookRegistry.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.