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