Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\Extension;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Component\Graph\Graph;
|
Chris@0
|
6 use Drupal\Component\Utility\NestedArray;
|
Chris@0
|
7 use Drupal\Core\Cache\CacheBackendInterface;
|
Chris@0
|
8
|
Chris@0
|
9 /**
|
Chris@0
|
10 * Class that manages modules in a Drupal installation.
|
Chris@0
|
11 */
|
Chris@0
|
12 class ModuleHandler implements ModuleHandlerInterface {
|
Chris@0
|
13
|
Chris@0
|
14 /**
|
Chris@0
|
15 * List of loaded files.
|
Chris@0
|
16 *
|
Chris@0
|
17 * @var array
|
Chris@0
|
18 * An associative array whose keys are file paths of loaded files, relative
|
Chris@0
|
19 * to the application's root directory.
|
Chris@0
|
20 */
|
Chris@0
|
21 protected $loadedFiles;
|
Chris@0
|
22
|
Chris@0
|
23 /**
|
Chris@0
|
24 * List of installed modules.
|
Chris@0
|
25 *
|
Chris@0
|
26 * @var \Drupal\Core\Extension\Extension[]
|
Chris@0
|
27 */
|
Chris@0
|
28 protected $moduleList;
|
Chris@0
|
29
|
Chris@0
|
30 /**
|
Chris@0
|
31 * Boolean indicating whether modules have been loaded.
|
Chris@0
|
32 *
|
Chris@0
|
33 * @var bool
|
Chris@0
|
34 */
|
Chris@0
|
35 protected $loaded = FALSE;
|
Chris@0
|
36
|
Chris@0
|
37 /**
|
Chris@0
|
38 * List of hook implementations keyed by hook name.
|
Chris@0
|
39 *
|
Chris@0
|
40 * @var array
|
Chris@0
|
41 */
|
Chris@0
|
42 protected $implementations;
|
Chris@0
|
43
|
Chris@0
|
44 /**
|
Chris@0
|
45 * List of hooks where the implementations have been "verified".
|
Chris@0
|
46 *
|
Chris@0
|
47 * @var true[]
|
Chris@0
|
48 * Associative array where keys are hook names.
|
Chris@0
|
49 */
|
Chris@0
|
50 protected $verified;
|
Chris@0
|
51
|
Chris@0
|
52 /**
|
Chris@0
|
53 * Information returned by hook_hook_info() implementations.
|
Chris@0
|
54 *
|
Chris@0
|
55 * @var array
|
Chris@0
|
56 */
|
Chris@0
|
57 protected $hookInfo;
|
Chris@0
|
58
|
Chris@0
|
59 /**
|
Chris@0
|
60 * Cache backend for storing module hook implementation information.
|
Chris@0
|
61 *
|
Chris@0
|
62 * @var \Drupal\Core\Cache\CacheBackendInterface
|
Chris@0
|
63 */
|
Chris@0
|
64 protected $cacheBackend;
|
Chris@0
|
65
|
Chris@0
|
66 /**
|
Chris@0
|
67 * Whether the cache needs to be written.
|
Chris@0
|
68 *
|
Chris@0
|
69 * @var bool
|
Chris@0
|
70 */
|
Chris@0
|
71 protected $cacheNeedsWriting = FALSE;
|
Chris@0
|
72
|
Chris@0
|
73 /**
|
Chris@0
|
74 * List of alter hook implementations keyed by hook name(s).
|
Chris@0
|
75 *
|
Chris@0
|
76 * @var array
|
Chris@0
|
77 */
|
Chris@0
|
78 protected $alterFunctions;
|
Chris@0
|
79
|
Chris@0
|
80 /**
|
Chris@0
|
81 * The app root.
|
Chris@0
|
82 *
|
Chris@0
|
83 * @var string
|
Chris@0
|
84 */
|
Chris@0
|
85 protected $root;
|
Chris@0
|
86
|
Chris@0
|
87 /**
|
Chris@0
|
88 * A list of module include file keys.
|
Chris@0
|
89 *
|
Chris@0
|
90 * @var array
|
Chris@0
|
91 */
|
Chris@0
|
92 protected $includeFileKeys = [];
|
Chris@0
|
93
|
Chris@0
|
94 /**
|
Chris@0
|
95 * Constructs a ModuleHandler object.
|
Chris@0
|
96 *
|
Chris@0
|
97 * @param string $root
|
Chris@0
|
98 * The app root.
|
Chris@0
|
99 * @param array $module_list
|
Chris@0
|
100 * An associative array whose keys are the names of installed modules and
|
Chris@0
|
101 * whose values are Extension class parameters. This is normally the
|
Chris@0
|
102 * %container.modules% parameter being set up by DrupalKernel.
|
Chris@0
|
103 * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
Chris@0
|
104 * Cache backend for storing module hook implementation information.
|
Chris@0
|
105 *
|
Chris@0
|
106 * @see \Drupal\Core\DrupalKernel
|
Chris@0
|
107 * @see \Drupal\Core\CoreServiceProvider
|
Chris@0
|
108 */
|
Chris@0
|
109 public function __construct($root, array $module_list, CacheBackendInterface $cache_backend) {
|
Chris@0
|
110 $this->root = $root;
|
Chris@0
|
111 $this->moduleList = [];
|
Chris@0
|
112 foreach ($module_list as $name => $module) {
|
Chris@0
|
113 $this->moduleList[$name] = new Extension($this->root, $module['type'], $module['pathname'], $module['filename']);
|
Chris@0
|
114 }
|
Chris@0
|
115 $this->cacheBackend = $cache_backend;
|
Chris@0
|
116 }
|
Chris@0
|
117
|
Chris@0
|
118 /**
|
Chris@0
|
119 * {@inheritdoc}
|
Chris@0
|
120 */
|
Chris@0
|
121 public function load($name) {
|
Chris@0
|
122 if (isset($this->loadedFiles[$name])) {
|
Chris@0
|
123 return TRUE;
|
Chris@0
|
124 }
|
Chris@0
|
125
|
Chris@0
|
126 if (isset($this->moduleList[$name])) {
|
Chris@0
|
127 $this->moduleList[$name]->load();
|
Chris@0
|
128 $this->loadedFiles[$name] = TRUE;
|
Chris@0
|
129 return TRUE;
|
Chris@0
|
130 }
|
Chris@0
|
131 return FALSE;
|
Chris@0
|
132 }
|
Chris@0
|
133
|
Chris@0
|
134 /**
|
Chris@0
|
135 * {@inheritdoc}
|
Chris@0
|
136 */
|
Chris@0
|
137 public function loadAll() {
|
Chris@0
|
138 if (!$this->loaded) {
|
Chris@0
|
139 foreach ($this->moduleList as $name => $module) {
|
Chris@0
|
140 $this->load($name);
|
Chris@0
|
141 }
|
Chris@0
|
142 $this->loaded = TRUE;
|
Chris@0
|
143 }
|
Chris@0
|
144 }
|
Chris@0
|
145
|
Chris@0
|
146 /**
|
Chris@0
|
147 * {@inheritdoc}
|
Chris@0
|
148 */
|
Chris@0
|
149 public function reload() {
|
Chris@0
|
150 $this->loaded = FALSE;
|
Chris@0
|
151 $this->loadAll();
|
Chris@0
|
152 }
|
Chris@0
|
153
|
Chris@0
|
154 /**
|
Chris@0
|
155 * {@inheritdoc}
|
Chris@0
|
156 */
|
Chris@0
|
157 public function isLoaded() {
|
Chris@0
|
158 return $this->loaded;
|
Chris@0
|
159 }
|
Chris@0
|
160
|
Chris@0
|
161 /**
|
Chris@0
|
162 * {@inheritdoc}
|
Chris@0
|
163 */
|
Chris@0
|
164 public function getModuleList() {
|
Chris@0
|
165 return $this->moduleList;
|
Chris@0
|
166 }
|
Chris@0
|
167
|
Chris@0
|
168 /**
|
Chris@0
|
169 * {@inheritdoc}
|
Chris@0
|
170 */
|
Chris@0
|
171 public function getModule($name) {
|
Chris@0
|
172 if (isset($this->moduleList[$name])) {
|
Chris@0
|
173 return $this->moduleList[$name];
|
Chris@0
|
174 }
|
Chris@0
|
175 throw new \InvalidArgumentException(sprintf('The module %s does not exist.', $name));
|
Chris@0
|
176 }
|
Chris@0
|
177
|
Chris@0
|
178 /**
|
Chris@0
|
179 * {@inheritdoc}
|
Chris@0
|
180 */
|
Chris@0
|
181 public function setModuleList(array $module_list = []) {
|
Chris@0
|
182 $this->moduleList = $module_list;
|
Chris@0
|
183 // Reset the implementations, so a new call triggers a reloading of the
|
Chris@0
|
184 // available hooks.
|
Chris@0
|
185 $this->resetImplementations();
|
Chris@0
|
186 }
|
Chris@0
|
187
|
Chris@0
|
188 /**
|
Chris@0
|
189 * {@inheritdoc}
|
Chris@0
|
190 */
|
Chris@0
|
191 public function addModule($name, $path) {
|
Chris@0
|
192 $this->add('module', $name, $path);
|
Chris@0
|
193 }
|
Chris@0
|
194
|
Chris@0
|
195 /**
|
Chris@0
|
196 * {@inheritdoc}
|
Chris@0
|
197 */
|
Chris@0
|
198 public function addProfile($name, $path) {
|
Chris@0
|
199 $this->add('profile', $name, $path);
|
Chris@0
|
200 }
|
Chris@0
|
201
|
Chris@0
|
202 /**
|
Chris@0
|
203 * Adds a module or profile to the list of currently active modules.
|
Chris@0
|
204 *
|
Chris@0
|
205 * @param string $type
|
Chris@0
|
206 * The extension type; either 'module' or 'profile'.
|
Chris@0
|
207 * @param string $name
|
Chris@0
|
208 * The module name; e.g., 'node'.
|
Chris@0
|
209 * @param string $path
|
Chris@0
|
210 * The module path; e.g., 'core/modules/node'.
|
Chris@0
|
211 */
|
Chris@0
|
212 protected function add($type, $name, $path) {
|
Chris@0
|
213 $pathname = "$path/$name.info.yml";
|
Chris@0
|
214 $filename = file_exists($this->root . "/$path/$name.$type") ? "$name.$type" : NULL;
|
Chris@0
|
215 $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename);
|
Chris@0
|
216 $this->resetImplementations();
|
Chris@0
|
217 }
|
Chris@0
|
218
|
Chris@0
|
219 /**
|
Chris@0
|
220 * {@inheritdoc}
|
Chris@0
|
221 */
|
Chris@0
|
222 public function buildModuleDependencies(array $modules) {
|
Chris@0
|
223 foreach ($modules as $module) {
|
Chris@0
|
224 $graph[$module->getName()]['edges'] = [];
|
Chris@0
|
225 if (isset($module->info['dependencies']) && is_array($module->info['dependencies'])) {
|
Chris@0
|
226 foreach ($module->info['dependencies'] as $dependency) {
|
Chris@0
|
227 $dependency_data = static::parseDependency($dependency);
|
Chris@0
|
228 $graph[$module->getName()]['edges'][$dependency_data['name']] = $dependency_data;
|
Chris@0
|
229 }
|
Chris@0
|
230 }
|
Chris@0
|
231 }
|
Chris@0
|
232 $graph_object = new Graph($graph);
|
Chris@0
|
233 $graph = $graph_object->searchAndSort();
|
Chris@0
|
234 foreach ($graph as $module_name => $data) {
|
Chris@0
|
235 $modules[$module_name]->required_by = isset($data['reverse_paths']) ? $data['reverse_paths'] : [];
|
Chris@0
|
236 $modules[$module_name]->requires = isset($data['paths']) ? $data['paths'] : [];
|
Chris@0
|
237 $modules[$module_name]->sort = $data['weight'];
|
Chris@0
|
238 }
|
Chris@0
|
239 return $modules;
|
Chris@0
|
240 }
|
Chris@0
|
241
|
Chris@0
|
242 /**
|
Chris@0
|
243 * {@inheritdoc}
|
Chris@0
|
244 */
|
Chris@0
|
245 public function moduleExists($module) {
|
Chris@0
|
246 return isset($this->moduleList[$module]);
|
Chris@0
|
247 }
|
Chris@0
|
248
|
Chris@0
|
249 /**
|
Chris@0
|
250 * {@inheritdoc}
|
Chris@0
|
251 */
|
Chris@0
|
252 public function loadAllIncludes($type, $name = NULL) {
|
Chris@0
|
253 foreach ($this->moduleList as $module => $filename) {
|
Chris@0
|
254 $this->loadInclude($module, $type, $name);
|
Chris@0
|
255 }
|
Chris@0
|
256 }
|
Chris@0
|
257
|
Chris@0
|
258 /**
|
Chris@0
|
259 * {@inheritdoc}
|
Chris@0
|
260 */
|
Chris@0
|
261 public function loadInclude($module, $type, $name = NULL) {
|
Chris@0
|
262 if ($type == 'install') {
|
Chris@0
|
263 // Make sure the installation API is available
|
Chris@0
|
264 include_once $this->root . '/core/includes/install.inc';
|
Chris@0
|
265 }
|
Chris@0
|
266
|
Chris@0
|
267 $name = $name ?: $module;
|
Chris@0
|
268 $key = $type . ':' . $module . ':' . $name;
|
Chris@0
|
269 if (isset($this->includeFileKeys[$key])) {
|
Chris@0
|
270 return $this->includeFileKeys[$key];
|
Chris@0
|
271 }
|
Chris@0
|
272 if (isset($this->moduleList[$module])) {
|
Chris@0
|
273 $file = $this->root . '/' . $this->moduleList[$module]->getPath() . "/$name.$type";
|
Chris@0
|
274 if (is_file($file)) {
|
Chris@0
|
275 require_once $file;
|
Chris@0
|
276 $this->includeFileKeys[$key] = $file;
|
Chris@0
|
277 return $file;
|
Chris@0
|
278 }
|
Chris@0
|
279 else {
|
Chris@0
|
280 $this->includeFileKeys[$key] = FALSE;
|
Chris@0
|
281 }
|
Chris@0
|
282 }
|
Chris@0
|
283 return FALSE;
|
Chris@0
|
284 }
|
Chris@0
|
285
|
Chris@0
|
286 /**
|
Chris@0
|
287 * {@inheritdoc}
|
Chris@0
|
288 */
|
Chris@0
|
289 public function getHookInfo() {
|
Chris@0
|
290 if (!isset($this->hookInfo)) {
|
Chris@0
|
291 if ($cache = $this->cacheBackend->get('hook_info')) {
|
Chris@0
|
292 $this->hookInfo = $cache->data;
|
Chris@0
|
293 }
|
Chris@0
|
294 else {
|
Chris@0
|
295 $this->buildHookInfo();
|
Chris@0
|
296 $this->cacheBackend->set('hook_info', $this->hookInfo);
|
Chris@0
|
297 }
|
Chris@0
|
298 }
|
Chris@0
|
299 return $this->hookInfo;
|
Chris@0
|
300 }
|
Chris@0
|
301
|
Chris@0
|
302 /**
|
Chris@0
|
303 * Builds hook_hook_info() information.
|
Chris@0
|
304 *
|
Chris@0
|
305 * @see \Drupal\Core\Extension\ModuleHandler::getHookInfo()
|
Chris@0
|
306 */
|
Chris@0
|
307 protected function buildHookInfo() {
|
Chris@0
|
308 $this->hookInfo = [];
|
Chris@0
|
309 // Make sure that the modules are loaded before checking.
|
Chris@0
|
310 $this->reload();
|
Chris@0
|
311 // $this->invokeAll() would cause an infinite recursion.
|
Chris@0
|
312 foreach ($this->moduleList as $module => $filename) {
|
Chris@0
|
313 $function = $module . '_hook_info';
|
Chris@0
|
314 if (function_exists($function)) {
|
Chris@0
|
315 $result = $function();
|
Chris@0
|
316 if (isset($result) && is_array($result)) {
|
Chris@0
|
317 $this->hookInfo = NestedArray::mergeDeep($this->hookInfo, $result);
|
Chris@0
|
318 }
|
Chris@0
|
319 }
|
Chris@0
|
320 }
|
Chris@0
|
321 }
|
Chris@0
|
322
|
Chris@0
|
323 /**
|
Chris@0
|
324 * {@inheritdoc}
|
Chris@0
|
325 */
|
Chris@0
|
326 public function getImplementations($hook) {
|
Chris@0
|
327 $implementations = $this->getImplementationInfo($hook);
|
Chris@0
|
328 return array_keys($implementations);
|
Chris@0
|
329 }
|
Chris@0
|
330
|
Chris@0
|
331 /**
|
Chris@0
|
332 * {@inheritdoc}
|
Chris@0
|
333 */
|
Chris@0
|
334 public function writeCache() {
|
Chris@0
|
335 if ($this->cacheNeedsWriting) {
|
Chris@0
|
336 $this->cacheBackend->set('module_implements', $this->implementations);
|
Chris@0
|
337 $this->cacheNeedsWriting = FALSE;
|
Chris@0
|
338 }
|
Chris@0
|
339 }
|
Chris@0
|
340
|
Chris@0
|
341 /**
|
Chris@0
|
342 * {@inheritdoc}
|
Chris@0
|
343 */
|
Chris@0
|
344 public function resetImplementations() {
|
Chris@0
|
345 $this->implementations = NULL;
|
Chris@0
|
346 $this->hookInfo = NULL;
|
Chris@0
|
347 $this->alterFunctions = NULL;
|
Chris@0
|
348 // We maintain a persistent cache of hook implementations in addition to the
|
Chris@0
|
349 // static cache to avoid looping through every module and every hook on each
|
Chris@0
|
350 // request. Benchmarks show that the benefit of this caching outweighs the
|
Chris@0
|
351 // additional database hit even when using the default database caching
|
Chris@0
|
352 // backend and only a small number of modules are enabled. The cost of the
|
Chris@0
|
353 // $this->cacheBackend->get() is more or less constant and reduced further
|
Chris@0
|
354 // when non-database caching backends are used, so there will be more
|
Chris@0
|
355 // significant gains when a large number of modules are installed or hooks
|
Chris@0
|
356 // invoked, since this can quickly lead to
|
Chris@0
|
357 // \Drupal::moduleHandler()->implementsHook() being called several thousand
|
Chris@0
|
358 // times per request.
|
Chris@0
|
359 $this->cacheBackend->set('module_implements', []);
|
Chris@0
|
360 $this->cacheBackend->delete('hook_info');
|
Chris@0
|
361 }
|
Chris@0
|
362
|
Chris@0
|
363 /**
|
Chris@0
|
364 * {@inheritdoc}
|
Chris@0
|
365 */
|
Chris@0
|
366 public function implementsHook($module, $hook) {
|
Chris@0
|
367 $function = $module . '_' . $hook;
|
Chris@0
|
368 if (function_exists($function)) {
|
Chris@0
|
369 return TRUE;
|
Chris@0
|
370 }
|
Chris@0
|
371 // If the hook implementation does not exist, check whether it lives in an
|
Chris@0
|
372 // optional include file registered via hook_hook_info().
|
Chris@0
|
373 $hook_info = $this->getHookInfo();
|
Chris@0
|
374 if (isset($hook_info[$hook]['group'])) {
|
Chris@0
|
375 $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
|
Chris@0
|
376 if (function_exists($function)) {
|
Chris@0
|
377 return TRUE;
|
Chris@0
|
378 }
|
Chris@0
|
379 }
|
Chris@0
|
380 return FALSE;
|
Chris@0
|
381 }
|
Chris@0
|
382
|
Chris@0
|
383 /**
|
Chris@0
|
384 * {@inheritdoc}
|
Chris@0
|
385 */
|
Chris@0
|
386 public function invoke($module, $hook, array $args = []) {
|
Chris@0
|
387 if (!$this->implementsHook($module, $hook)) {
|
Chris@0
|
388 return;
|
Chris@0
|
389 }
|
Chris@0
|
390 $function = $module . '_' . $hook;
|
Chris@0
|
391 return call_user_func_array($function, $args);
|
Chris@0
|
392 }
|
Chris@0
|
393
|
Chris@0
|
394 /**
|
Chris@0
|
395 * {@inheritdoc}
|
Chris@0
|
396 */
|
Chris@0
|
397 public function invokeAll($hook, array $args = []) {
|
Chris@0
|
398 $return = [];
|
Chris@0
|
399 $implementations = $this->getImplementations($hook);
|
Chris@0
|
400 foreach ($implementations as $module) {
|
Chris@0
|
401 $function = $module . '_' . $hook;
|
Chris@0
|
402 $result = call_user_func_array($function, $args);
|
Chris@0
|
403 if (isset($result) && is_array($result)) {
|
Chris@0
|
404 $return = NestedArray::mergeDeep($return, $result);
|
Chris@0
|
405 }
|
Chris@0
|
406 elseif (isset($result)) {
|
Chris@0
|
407 $return[] = $result;
|
Chris@0
|
408 }
|
Chris@0
|
409 }
|
Chris@0
|
410
|
Chris@0
|
411 return $return;
|
Chris@0
|
412 }
|
Chris@0
|
413
|
Chris@0
|
414 /**
|
Chris@0
|
415 * {@inheritdoc}
|
Chris@0
|
416 */
|
Chris@14
|
417 public function invokeDeprecated($description, $module, $hook, array $args = []) {
|
Chris@14
|
418 $result = $this->invoke($module, $hook, $args);
|
Chris@14
|
419 $this->triggerDeprecationError($description, $hook);
|
Chris@14
|
420 return $result;
|
Chris@14
|
421 }
|
Chris@14
|
422
|
Chris@14
|
423 /**
|
Chris@14
|
424 * {@inheritdoc}
|
Chris@14
|
425 */
|
Chris@14
|
426 public function invokeAllDeprecated($description, $hook, array $args = []) {
|
Chris@14
|
427 $result = $this->invokeAll($hook, $args);
|
Chris@14
|
428 $this->triggerDeprecationError($description, $hook);
|
Chris@14
|
429 return $result;
|
Chris@14
|
430 }
|
Chris@14
|
431
|
Chris@14
|
432
|
Chris@14
|
433 /**
|
Chris@14
|
434 * Triggers an E_USER_DEPRECATED error if any module implements the hook.
|
Chris@14
|
435 *
|
Chris@14
|
436 * @param string $description
|
Chris@14
|
437 * Helpful text describing what to do instead of implementing this hook.
|
Chris@14
|
438 * @param string $hook
|
Chris@14
|
439 * The name of the hook.
|
Chris@14
|
440 */
|
Chris@14
|
441 private function triggerDeprecationError($description, $hook) {
|
Chris@14
|
442 $modules = array_keys($this->getImplementationInfo($hook));
|
Chris@14
|
443 if (!empty($modules)) {
|
Chris@14
|
444 $message = 'The deprecated hook hook_' . $hook . '() is implemented in these functions: ';
|
Chris@14
|
445 $implementations = array_map(function ($module) use ($hook) {
|
Chris@14
|
446 return $module . '_' . $hook . '()';
|
Chris@14
|
447 }, $modules);
|
Chris@14
|
448 @trigger_error($message . implode(', ', $implementations) . '. ' . $description, E_USER_DEPRECATED);
|
Chris@14
|
449 }
|
Chris@14
|
450 }
|
Chris@14
|
451
|
Chris@14
|
452 /**
|
Chris@14
|
453 * {@inheritdoc}
|
Chris@14
|
454 */
|
Chris@0
|
455 public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
|
Chris@0
|
456 // Most of the time, $type is passed as a string, so for performance,
|
Chris@0
|
457 // normalize it to that. When passed as an array, usually the first item in
|
Chris@0
|
458 // the array is a generic type, and additional items in the array are more
|
Chris@0
|
459 // specific variants of it, as in the case of array('form', 'form_FORM_ID').
|
Chris@0
|
460 if (is_array($type)) {
|
Chris@0
|
461 $cid = implode(',', $type);
|
Chris@0
|
462 $extra_types = $type;
|
Chris@0
|
463 $type = array_shift($extra_types);
|
Chris@0
|
464 // Allow if statements in this function to use the faster isset() rather
|
Chris@0
|
465 // than !empty() both when $type is passed as a string, or as an array
|
Chris@0
|
466 // with one item.
|
Chris@0
|
467 if (empty($extra_types)) {
|
Chris@0
|
468 unset($extra_types);
|
Chris@0
|
469 }
|
Chris@0
|
470 }
|
Chris@0
|
471 else {
|
Chris@0
|
472 $cid = $type;
|
Chris@0
|
473 }
|
Chris@0
|
474
|
Chris@0
|
475 // Some alter hooks are invoked many times per page request, so store the
|
Chris@0
|
476 // list of functions to call, and on subsequent calls, iterate through them
|
Chris@0
|
477 // quickly.
|
Chris@0
|
478 if (!isset($this->alterFunctions[$cid])) {
|
Chris@0
|
479 $this->alterFunctions[$cid] = [];
|
Chris@0
|
480 $hook = $type . '_alter';
|
Chris@0
|
481 $modules = $this->getImplementations($hook);
|
Chris@0
|
482 if (!isset($extra_types)) {
|
Chris@0
|
483 // For the more common case of a single hook, we do not need to call
|
Chris@0
|
484 // function_exists(), since $this->getImplementations() returns only
|
Chris@0
|
485 // modules with implementations.
|
Chris@0
|
486 foreach ($modules as $module) {
|
Chris@0
|
487 $this->alterFunctions[$cid][] = $module . '_' . $hook;
|
Chris@0
|
488 }
|
Chris@0
|
489 }
|
Chris@0
|
490 else {
|
Chris@0
|
491 // For multiple hooks, we need $modules to contain every module that
|
Chris@0
|
492 // implements at least one of them.
|
Chris@0
|
493 $extra_modules = [];
|
Chris@0
|
494 foreach ($extra_types as $extra_type) {
|
Chris@0
|
495 $extra_modules = array_merge($extra_modules, $this->getImplementations($extra_type . '_alter'));
|
Chris@0
|
496 }
|
Chris@0
|
497 // If any modules implement one of the extra hooks that do not implement
|
Chris@0
|
498 // the primary hook, we need to add them to the $modules array in their
|
Chris@0
|
499 // appropriate order. $this->getImplementations() can only return
|
Chris@0
|
500 // ordered implementations of a single hook. To get the ordered
|
Chris@0
|
501 // implementations of multiple hooks, we mimic the
|
Chris@0
|
502 // $this->getImplementations() logic of first ordering by
|
Chris@0
|
503 // $this->getModuleList(), and then calling
|
Chris@0
|
504 // $this->alter('module_implements').
|
Chris@0
|
505 if (array_diff($extra_modules, $modules)) {
|
Chris@0
|
506 // Merge the arrays and order by getModuleList().
|
Chris@0
|
507 $modules = array_intersect(array_keys($this->moduleList), array_merge($modules, $extra_modules));
|
Chris@0
|
508 // Since $this->getImplementations() already took care of loading the
|
Chris@0
|
509 // necessary include files, we can safely pass FALSE for the array
|
Chris@0
|
510 // values.
|
Chris@0
|
511 $implementations = array_fill_keys($modules, FALSE);
|
Chris@0
|
512 // Let modules adjust the order solely based on the primary hook. This
|
Chris@0
|
513 // ensures the same module order regardless of whether this if block
|
Chris@0
|
514 // runs. Calling $this->alter() recursively in this way does not
|
Chris@0
|
515 // result in an infinite loop, because this call is for a single
|
Chris@0
|
516 // $type, so we won't end up in this code block again.
|
Chris@0
|
517 $this->alter('module_implements', $implementations, $hook);
|
Chris@0
|
518 $modules = array_keys($implementations);
|
Chris@0
|
519 }
|
Chris@0
|
520 foreach ($modules as $module) {
|
Chris@0
|
521 // Since $modules is a merged array, for any given module, we do not
|
Chris@0
|
522 // know whether it has any particular implementation, so we need a
|
Chris@0
|
523 // function_exists().
|
Chris@0
|
524 $function = $module . '_' . $hook;
|
Chris@0
|
525 if (function_exists($function)) {
|
Chris@0
|
526 $this->alterFunctions[$cid][] = $function;
|
Chris@0
|
527 }
|
Chris@0
|
528 foreach ($extra_types as $extra_type) {
|
Chris@0
|
529 $function = $module . '_' . $extra_type . '_alter';
|
Chris@0
|
530 if (function_exists($function)) {
|
Chris@0
|
531 $this->alterFunctions[$cid][] = $function;
|
Chris@0
|
532 }
|
Chris@0
|
533 }
|
Chris@0
|
534 }
|
Chris@0
|
535 }
|
Chris@0
|
536 }
|
Chris@0
|
537
|
Chris@0
|
538 foreach ($this->alterFunctions[$cid] as $function) {
|
Chris@0
|
539 $function($data, $context1, $context2);
|
Chris@0
|
540 }
|
Chris@0
|
541 }
|
Chris@0
|
542
|
Chris@0
|
543 /**
|
Chris@14
|
544 * {@inheritdoc}
|
Chris@14
|
545 */
|
Chris@14
|
546 public function alterDeprecated($description, $type, &$data, &$context1 = NULL, &$context2 = NULL) {
|
Chris@14
|
547 // Invoke the alter hook. This has the side effect of populating
|
Chris@14
|
548 // $this->alterFunctions.
|
Chris@14
|
549 $this->alter($type, $data, $context1, $context2);
|
Chris@14
|
550 // The $type parameter can be an array. alter() will deal with this
|
Chris@14
|
551 // internally, but we have to extract the proper $cid in order to discover
|
Chris@14
|
552 // implementations.
|
Chris@14
|
553 $cid = $type;
|
Chris@14
|
554 if (is_array($type)) {
|
Chris@14
|
555 $cid = implode(',', $type);
|
Chris@14
|
556 $extra_types = $type;
|
Chris@14
|
557 $type = array_shift($extra_types);
|
Chris@14
|
558 }
|
Chris@14
|
559 if (!empty($this->alterFunctions[$cid])) {
|
Chris@14
|
560 $message = 'The deprecated alter hook hook_' . $type . '_alter() is implemented in these functions: ' . implode(', ', $this->alterFunctions[$cid]) . '.';
|
Chris@14
|
561 @trigger_error($message . ' ' . $description, E_USER_DEPRECATED);
|
Chris@14
|
562 }
|
Chris@14
|
563 }
|
Chris@14
|
564
|
Chris@14
|
565 /**
|
Chris@0
|
566 * Provides information about modules' implementations of a hook.
|
Chris@0
|
567 *
|
Chris@0
|
568 * @param string $hook
|
Chris@0
|
569 * The name of the hook (e.g. "help" or "menu").
|
Chris@0
|
570 *
|
Chris@0
|
571 * @return mixed[]
|
Chris@0
|
572 * An array whose keys are the names of the modules which are implementing
|
Chris@0
|
573 * this hook and whose values are either a string identifying a file in
|
Chris@0
|
574 * which the implementation is to be found, or FALSE, if the implementation
|
Chris@0
|
575 * is in the module file.
|
Chris@0
|
576 */
|
Chris@0
|
577 protected function getImplementationInfo($hook) {
|
Chris@0
|
578 if (!isset($this->implementations)) {
|
Chris@0
|
579 $this->implementations = [];
|
Chris@0
|
580 $this->verified = [];
|
Chris@0
|
581 if ($cache = $this->cacheBackend->get('module_implements')) {
|
Chris@0
|
582 $this->implementations = $cache->data;
|
Chris@0
|
583 }
|
Chris@0
|
584 }
|
Chris@0
|
585 if (!isset($this->implementations[$hook])) {
|
Chris@0
|
586 // The hook is not cached, so ensure that whether or not it has
|
Chris@0
|
587 // implementations, the cache is updated at the end of the request.
|
Chris@0
|
588 $this->cacheNeedsWriting = TRUE;
|
Chris@0
|
589 // Discover implementations.
|
Chris@0
|
590 $this->implementations[$hook] = $this->buildImplementationInfo($hook);
|
Chris@0
|
591 // Implementations are always "verified" as part of the discovery.
|
Chris@0
|
592 $this->verified[$hook] = TRUE;
|
Chris@0
|
593 }
|
Chris@0
|
594 elseif (!isset($this->verified[$hook])) {
|
Chris@0
|
595 if (!$this->verifyImplementations($this->implementations[$hook], $hook)) {
|
Chris@0
|
596 // One or more of the implementations did not exist and need to be
|
Chris@0
|
597 // removed in the cache.
|
Chris@0
|
598 $this->cacheNeedsWriting = TRUE;
|
Chris@0
|
599 }
|
Chris@0
|
600 $this->verified[$hook] = TRUE;
|
Chris@0
|
601 }
|
Chris@0
|
602 return $this->implementations[$hook];
|
Chris@0
|
603 }
|
Chris@0
|
604
|
Chris@0
|
605 /**
|
Chris@0
|
606 * Builds hook implementation information for a given hook name.
|
Chris@0
|
607 *
|
Chris@0
|
608 * @param string $hook
|
Chris@0
|
609 * The name of the hook (e.g. "help" or "menu").
|
Chris@0
|
610 *
|
Chris@0
|
611 * @return mixed[]
|
Chris@0
|
612 * An array whose keys are the names of the modules which are implementing
|
Chris@0
|
613 * this hook and whose values are either a string identifying a file in
|
Chris@0
|
614 * which the implementation is to be found, or FALSE, if the implementation
|
Chris@0
|
615 * is in the module file.
|
Chris@0
|
616 *
|
Chris@0
|
617 * @throws \RuntimeException
|
Chris@0
|
618 * Exception thrown when an invalid implementation is added by
|
Chris@0
|
619 * hook_module_implements_alter().
|
Chris@0
|
620 *
|
Chris@0
|
621 * @see \Drupal\Core\Extension\ModuleHandler::getImplementationInfo()
|
Chris@0
|
622 */
|
Chris@0
|
623 protected function buildImplementationInfo($hook) {
|
Chris@0
|
624 $implementations = [];
|
Chris@0
|
625 $hook_info = $this->getHookInfo();
|
Chris@0
|
626 foreach ($this->moduleList as $module => $extension) {
|
Chris@0
|
627 $include_file = isset($hook_info[$hook]['group']) && $this->loadInclude($module, 'inc', $module . '.' . $hook_info[$hook]['group']);
|
Chris@0
|
628 // Since $this->implementsHook() may needlessly try to load the include
|
Chris@0
|
629 // file again, function_exists() is used directly here.
|
Chris@0
|
630 if (function_exists($module . '_' . $hook)) {
|
Chris@0
|
631 $implementations[$module] = $include_file ? $hook_info[$hook]['group'] : FALSE;
|
Chris@0
|
632 }
|
Chris@0
|
633 }
|
Chris@0
|
634 // Allow modules to change the weight of specific implementations, but avoid
|
Chris@0
|
635 // an infinite loop.
|
Chris@0
|
636 if ($hook != 'module_implements_alter') {
|
Chris@0
|
637 // Remember the original implementations, before they are modified with
|
Chris@0
|
638 // hook_module_implements_alter().
|
Chris@0
|
639 $implementations_before = $implementations;
|
Chris@0
|
640 // Verify implementations that were added or modified.
|
Chris@0
|
641 $this->alter('module_implements', $implementations, $hook);
|
Chris@0
|
642 // Verify new or modified implementations.
|
Chris@0
|
643 foreach (array_diff_assoc($implementations, $implementations_before) as $module => $group) {
|
Chris@0
|
644 // If an implementation of hook_module_implements_alter() changed or
|
Chris@0
|
645 // added a group, the respective file needs to be included.
|
Chris@0
|
646 if ($group) {
|
Chris@0
|
647 $this->loadInclude($module, 'inc', "$module.$group");
|
Chris@0
|
648 }
|
Chris@0
|
649 // If a new implementation was added, verify that the function exists.
|
Chris@0
|
650 if (!function_exists($module . '_' . $hook)) {
|
Chris@0
|
651 throw new \RuntimeException("An invalid implementation {$module}_{$hook} was added by hook_module_implements_alter()");
|
Chris@0
|
652 }
|
Chris@0
|
653 }
|
Chris@0
|
654 }
|
Chris@0
|
655 return $implementations;
|
Chris@0
|
656 }
|
Chris@0
|
657
|
Chris@0
|
658 /**
|
Chris@0
|
659 * Verifies an array of implementations loaded from the cache, by including
|
Chris@0
|
660 * the lazy-loaded $module.$group.inc, and checking function_exists().
|
Chris@0
|
661 *
|
Chris@0
|
662 * @param string[] $implementations
|
Chris@0
|
663 * Implementation "group" by module name.
|
Chris@0
|
664 * @param string $hook
|
Chris@0
|
665 * The hook name.
|
Chris@0
|
666 *
|
Chris@0
|
667 * @return bool
|
Chris@0
|
668 * TRUE, if all implementations exist.
|
Chris@0
|
669 * FALSE, if one or more implementations don't exist and need to be removed
|
Chris@0
|
670 * from the cache.
|
Chris@0
|
671 */
|
Chris@0
|
672 protected function verifyImplementations(&$implementations, $hook) {
|
Chris@0
|
673 $all_valid = TRUE;
|
Chris@0
|
674 foreach ($implementations as $module => $group) {
|
Chris@0
|
675 // If this hook implementation is stored in a lazy-loaded file, include
|
Chris@0
|
676 // that file first.
|
Chris@0
|
677 if ($group) {
|
Chris@0
|
678 $this->loadInclude($module, 'inc', "$module.$group");
|
Chris@0
|
679 }
|
Chris@0
|
680 // It is possible that a module removed a hook implementation without
|
Chris@0
|
681 // the implementations cache being rebuilt yet, so we check whether the
|
Chris@0
|
682 // function exists on each request to avoid undefined function errors.
|
Chris@0
|
683 // Since ModuleHandler::implementsHook() may needlessly try to
|
Chris@0
|
684 // load the include file again, function_exists() is used directly here.
|
Chris@0
|
685 if (!function_exists($module . '_' . $hook)) {
|
Chris@0
|
686 // Clear out the stale implementation from the cache and force a cache
|
Chris@0
|
687 // refresh to forget about no longer existing hook implementations.
|
Chris@0
|
688 unset($implementations[$module]);
|
Chris@0
|
689 // One of the implementations did not exist and needs to be removed in
|
Chris@0
|
690 // the cache.
|
Chris@0
|
691 $all_valid = FALSE;
|
Chris@0
|
692 }
|
Chris@0
|
693 }
|
Chris@0
|
694 return $all_valid;
|
Chris@0
|
695 }
|
Chris@0
|
696
|
Chris@0
|
697 /**
|
Chris@0
|
698 * Parses a dependency for comparison by drupal_check_incompatibility().
|
Chris@0
|
699 *
|
Chris@0
|
700 * @param $dependency
|
Chris@0
|
701 * A dependency string, which specifies a module dependency, and optionally
|
Chris@0
|
702 * the project it comes from and versions that are supported. Supported
|
Chris@0
|
703 * formats include:
|
Chris@0
|
704 * - 'module'
|
Chris@0
|
705 * - 'project:module'
|
Chris@0
|
706 * - 'project:module (>=version, version)'
|
Chris@0
|
707 *
|
Chris@0
|
708 * @return
|
Chris@0
|
709 * An associative array with three keys:
|
Chris@0
|
710 * - 'name' includes the name of the thing to depend on (e.g. 'foo').
|
Chris@0
|
711 * - 'original_version' contains the original version string (which can be
|
Chris@0
|
712 * used in the UI for reporting incompatibilities).
|
Chris@0
|
713 * - 'versions' is a list of associative arrays, each containing the keys
|
Chris@0
|
714 * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
|
Chris@0
|
715 * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
|
Chris@0
|
716 * Callers should pass this structure to drupal_check_incompatibility().
|
Chris@0
|
717 *
|
Chris@0
|
718 * @see drupal_check_incompatibility()
|
Chris@0
|
719 */
|
Chris@0
|
720 public static function parseDependency($dependency) {
|
Chris@0
|
721 $value = [];
|
Chris@0
|
722 // Split out the optional project name.
|
Chris@0
|
723 if (strpos($dependency, ':') !== FALSE) {
|
Chris@0
|
724 list($project_name, $dependency) = explode(':', $dependency);
|
Chris@0
|
725 $value['project'] = $project_name;
|
Chris@0
|
726 }
|
Chris@0
|
727 // We use named subpatterns and support every op that version_compare
|
Chris@0
|
728 // supports. Also, op is optional and defaults to equals.
|
Chris@0
|
729 $p_op = '(?<operation>!=|==|=|<|<=|>|>=|<>)?';
|
Chris@0
|
730 // Core version is always optional: 8.x-2.x and 2.x is treated the same.
|
Chris@0
|
731 $p_core = '(?:' . preg_quote(\Drupal::CORE_COMPATIBILITY) . '-)?';
|
Chris@0
|
732 $p_major = '(?<major>\d+)';
|
Chris@0
|
733 // By setting the minor version to x, branches can be matched.
|
Chris@0
|
734 $p_minor = '(?<minor>(?:\d+|x)(?:-[A-Za-z]+\d+)?)';
|
Chris@0
|
735 $parts = explode('(', $dependency, 2);
|
Chris@0
|
736 $value['name'] = trim($parts[0]);
|
Chris@0
|
737 if (isset($parts[1])) {
|
Chris@0
|
738 $value['original_version'] = ' (' . $parts[1];
|
Chris@0
|
739 foreach (explode(',', $parts[1]) as $version) {
|
Chris@0
|
740 if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) {
|
Chris@0
|
741 $op = !empty($matches['operation']) ? $matches['operation'] : '=';
|
Chris@0
|
742 if ($matches['minor'] == 'x') {
|
Chris@0
|
743 // Drupal considers "2.x" to mean any version that begins with
|
Chris@0
|
744 // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(),
|
Chris@0
|
745 // on the other hand, treats "x" as a string; so to
|
Chris@0
|
746 // version_compare(), "2.x" is considered less than 2.0. This
|
Chris@0
|
747 // means that >=2.x and <2.x are handled by version_compare()
|
Chris@0
|
748 // as we need, but > and <= are not.
|
Chris@0
|
749 if ($op == '>' || $op == '<=') {
|
Chris@0
|
750 $matches['major']++;
|
Chris@0
|
751 }
|
Chris@0
|
752 // Equivalence can be checked by adding two restrictions.
|
Chris@0
|
753 if ($op == '=' || $op == '==') {
|
Chris@0
|
754 $value['versions'][] = ['op' => '<', 'version' => ($matches['major'] + 1) . '.x'];
|
Chris@0
|
755 $op = '>=';
|
Chris@0
|
756 }
|
Chris@0
|
757 }
|
Chris@0
|
758 $value['versions'][] = ['op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']];
|
Chris@0
|
759 }
|
Chris@0
|
760 }
|
Chris@0
|
761 }
|
Chris@0
|
762 return $value;
|
Chris@0
|
763 }
|
Chris@0
|
764
|
Chris@0
|
765 /**
|
Chris@0
|
766 * {@inheritdoc}
|
Chris@0
|
767 */
|
Chris@0
|
768 public function getModuleDirectories() {
|
Chris@0
|
769 $dirs = [];
|
Chris@0
|
770 foreach ($this->getModuleList() as $name => $module) {
|
Chris@0
|
771 $dirs[$name] = $this->root . '/' . $module->getPath();
|
Chris@0
|
772 }
|
Chris@0
|
773 return $dirs;
|
Chris@0
|
774 }
|
Chris@0
|
775
|
Chris@0
|
776 /**
|
Chris@0
|
777 * {@inheritdoc}
|
Chris@0
|
778 */
|
Chris@0
|
779 public function getName($module) {
|
Chris@0
|
780 $info = system_get_info('module', $module);
|
Chris@0
|
781 return isset($info['name']) ? $info['name'] : $module;
|
Chris@0
|
782 }
|
Chris@0
|
783
|
Chris@0
|
784 }
|