1<?php 2 3namespace Drupal\Core\Update; 4 5use Drupal\Core\Extension\Extension; 6use Drupal\Core\Extension\ExtensionDiscovery; 7use Drupal\Core\KeyValueStore\KeyValueStoreInterface; 8 9/** 10 * Provides all and missing update implementations. 11 * 12 * Note: This registry is specific to a type of updates, like 'post_update' as 13 * example. 14 * 15 * It therefore scans for functions named like the type of updates, so it looks 16 * like MODULE_UPDATETYPE_NAME() with NAME being a machine name. 17 */ 18class UpdateRegistry { 19 20 /** 21 * The used update name. 22 * 23 * @var string 24 */ 25 protected $updateType = 'post_update'; 26 27 /** 28 * The app root. 29 * 30 * @var string 31 */ 32 protected $root; 33 34 /** 35 * The filename of the log file. 36 * 37 * @var string 38 */ 39 protected $logFilename; 40 41 /** 42 * @var string[] 43 */ 44 protected $enabledModules; 45 46 /** 47 * The key value storage. 48 * 49 * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface 50 */ 51 protected $keyValue; 52 53 /** 54 * Should we respect update functions in tests. 55 * 56 * @var bool|null 57 */ 58 protected $includeTests = NULL; 59 60 /** 61 * The site path. 62 * 63 * @var string 64 */ 65 protected $sitePath; 66 67 /** 68 * Constructs a new UpdateRegistry. 69 * 70 * @param string $root 71 * The app root. 72 * @param string $site_path 73 * The site path. 74 * @param string[] $enabled_modules 75 * A list of enabled modules. 76 * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value 77 * The key value store. 78 * @param bool|null $include_tests 79 * (optional) A flag whether to include tests in the scanning of modules. 80 */ 81 public function __construct($root, $site_path, array $enabled_modules, KeyValueStoreInterface $key_value, $include_tests = NULL) { 82 $this->root = $root; 83 $this->sitePath = $site_path; 84 $this->enabledModules = $enabled_modules; 85 $this->keyValue = $key_value; 86 $this->includeTests = $include_tests; 87 } 88 89 /** 90 * Gets removed hook_post_update_NAME() implementations for a module. 91 * 92 * @return string[] 93 * A list of post-update functions that have been removed. 94 */ 95 public function getRemovedPostUpdates($module) { 96 $this->scanExtensionsAndLoadUpdateFiles(); 97 $function = "{$module}_removed_post_updates"; 98 if (function_exists($function)) { 99 return $function(); 100 } 101 return []; 102 } 103 104 /** 105 * Gets all available update functions. 106 * 107 * @return callable[] 108 * A list of update functions. 109 */ 110 protected function getAvailableUpdateFunctions() { 111 $regexp = '/^(?<module>.+)_' . $this->updateType . '_(?<name>.+)$/'; 112 $functions = get_defined_functions(); 113 114 $updates = []; 115 foreach (preg_grep('/_' . $this->updateType . '_/', $functions['user']) as $function) { 116 // If this function is a module update function, add it to the list of 117 // module updates. 118 if (preg_match($regexp, $function, $matches)) { 119 if (in_array($matches['module'], $this->enabledModules)) { 120 $function_name = $matches['module'] . '_' . $this->updateType . '_' . $matches['name']; 121 if ($this->updateType === 'post_update') { 122 $removed = array_keys($this->getRemovedPostUpdates($matches['module'])); 123 if (array_search($function_name, $removed) !== FALSE) { 124 throw new RemovedPostUpdateNameException(sprintf('The following update is specified as removed in hook_removed_post_updates() but still exists in the code base: %s', $function_name)); 125 } 126 } 127 $updates[] = $function_name; 128 } 129 } 130 } 131 // Ensure that the update order is deterministic. 132 sort($updates); 133 return $updates; 134 } 135 136 /** 137 * Find all update functions that haven't been executed. 138 * 139 * @return callable[] 140 * A list of update functions. 141 */ 142 public function getPendingUpdateFunctions() { 143 // We need a) the list of active modules (we get that from the config 144 // bootstrap factory) and b) the path to the modules, we use the extension 145 // discovery for that. 146 147 $this->scanExtensionsAndLoadUpdateFiles(); 148 149 // First figure out which hook_{$this->updateType}_NAME got executed 150 // already. 151 $existing_update_functions = $this->keyValue->get('existing_updates', []); 152 153 $available_update_functions = $this->getAvailableUpdateFunctions(); 154 $not_executed_update_functions = array_diff($available_update_functions, $existing_update_functions); 155 156 return $not_executed_update_functions; 157 } 158 159 /** 160 * Loads all update files for a given list of extension. 161 * 162 * @param \Drupal\Core\Extension\Extension[] $module_extensions 163 * The extensions used for loading. 164 */ 165 protected function loadUpdateFiles(array $module_extensions) { 166 // Load all the {$this->updateType}.php files. 167 foreach ($this->enabledModules as $module) { 168 if (isset($module_extensions[$module])) { 169 $this->loadUpdateFile($module_extensions[$module]); 170 } 171 } 172 } 173 174 /** 175 * Loads the {$this->updateType}.php file for a given extension. 176 * 177 * @param \Drupal\Core\Extension\Extension $module 178 * The extension of the module to load its file. 179 */ 180 protected function loadUpdateFile(Extension $module) { 181 $filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateType}.php"; 182 if (file_exists($filename)) { 183 include_once $filename; 184 } 185 } 186 187 /** 188 * Returns a list of all the pending updates. 189 * 190 * @return array[] 191 * An associative array keyed by module name which contains all information 192 * about database updates that need to be run, and any updates that are not 193 * going to proceed due to missing requirements. 194 * 195 * The subarray for each module can contain the following keys: 196 * - start: The starting update that is to be processed. If this does not 197 * exist then do not process any updates for this module as there are 198 * other requirements that need to be resolved. 199 * - pending: An array of all the pending updates for the module including 200 * the description from source code comment for each update function. 201 * This array is keyed by the update name. 202 */ 203 public function getPendingUpdateInformation() { 204 $functions = $this->getPendingUpdateFunctions(); 205 206 $ret = []; 207 foreach ($functions as $function) { 208 list($module, $update) = explode("_{$this->updateType}_", $function); 209 // The description for an update comes from its Doxygen. 210 $func = new \ReflectionFunction($function); 211 $description = trim(str_replace(["\n", '*', '/'], '', $func->getDocComment()), ' '); 212 $ret[$module]['pending'][$update] = $description; 213 if (!isset($ret[$module]['start'])) { 214 $ret[$module]['start'] = $update; 215 } 216 } 217 return $ret; 218 } 219 220 /** 221 * Registers that update functions were executed. 222 * 223 * @param string[] $function_names 224 * The executed update functions. 225 * 226 * @return $this 227 */ 228 public function registerInvokedUpdates(array $function_names) { 229 $executed_updates = $this->keyValue->get('existing_updates', []); 230 $executed_updates = array_merge($executed_updates, $function_names); 231 $this->keyValue->set('existing_updates', $executed_updates); 232 233 return $this; 234 } 235 236 /** 237 * Returns all available updates for a given module. 238 * 239 * @param string $module_name 240 * The module name. 241 * 242 * @return callable[] 243 * A list of update functions. 244 */ 245 public function getModuleUpdateFunctions($module_name) { 246 $this->scanExtensionsAndLoadUpdateFiles(); 247 $all_functions = $this->getAvailableUpdateFunctions(); 248 249 return array_filter($all_functions, function ($function_name) use ($module_name) { 250 list($function_module_name,) = explode("_{$this->updateType}_", $function_name); 251 return $function_module_name === $module_name; 252 }); 253 } 254 255 /** 256 * Scans all module + profile extensions and load the update files. 257 */ 258 protected function scanExtensionsAndLoadUpdateFiles() { 259 // Scan the module list. 260 $extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath); 261 $module_extensions = $extension_discovery->scan('module'); 262 263 $profile_extensions = $extension_discovery->scan('profile'); 264 $extensions = array_merge($module_extensions, $profile_extensions); 265 266 $this->loadUpdateFiles($extensions); 267 } 268 269 /** 270 * Filters out already executed update functions by module. 271 * 272 * @param string $module 273 * The module name. 274 */ 275 public function filterOutInvokedUpdatesByModule($module) { 276 $existing_update_functions = $this->keyValue->get('existing_updates', []); 277 278 $remaining_update_functions = array_filter($existing_update_functions, function ($function_name) use ($module) { 279 return strpos($function_name, "{$module}_{$this->updateType}_") !== 0; 280 }); 281 282 $this->keyValue->set('existing_updates', array_values($remaining_update_functions)); 283 } 284 285} 286