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