3 namespace Drupal\Core\Extension;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Database\DatabaseExceptionWrapper;
7 use Drupal\Core\Extension\Exception\UnknownExtensionException;
8 use Drupal\Core\State\StateInterface;
11 * Provides available extensions.
13 * The extension list is per extension type, like module, theme and profile.
15 abstract class ExtensionList {
18 * The type of the extension: "module", "theme" or "profile".
34 * @var \Drupal\Core\Cache\CacheBackendInterface
39 * Default values to be merged into *.info.yml file arrays.
43 protected $defaults = [];
48 * @var \Drupal\Core\Extension\InfoParserInterface
50 protected $infoParser;
55 * @var \Drupal\Core\Extension\ModuleHandlerInterface
57 protected $moduleHandler;
60 * The cached extensions.
62 * @var \Drupal\Core\Extension\Extension[]|null
64 protected $extensions;
67 * Static caching for extension info.
69 * Access this property's value through static::getAllInfo().
72 * Keys are extension names, and values their info arrays (mixed[]).
74 * @see \Drupal\Core\Extension\ExtensionList::getAllAvailableInfo
76 protected $extensionInfo;
79 * A list of extension folder names keyed by extension name.
86 * A list of extension folder names directly added in code (not discovered).
88 * It is important to keep a separate list to ensure that it takes priority
89 * over the discovered extension folders.
95 protected $addedPathNames = [];
100 * @var \Drupal\Core\State\StateInterface
105 * The install profile used by the site.
109 protected $installProfile;
112 * Constructs a new instance.
114 * @param string $root
116 * @param string $type
117 * The extension type.
118 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
120 * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
122 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
123 * The module handler.
124 * @param \Drupal\Core\State\StateInterface $state
126 * @param string $install_profile
127 * The install profile used by the site.
129 public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, $install_profile) {
132 $this->cache = $cache;
133 $this->infoParser = $info_parser;
134 $this->moduleHandler = $module_handler;
135 $this->state = $state;
136 $this->installProfile = $install_profile;
140 * Returns the extension discovery.
142 * @return \Drupal\Core\Extension\ExtensionDiscovery
144 protected function getExtensionDiscovery() {
145 return new ExtensionDiscovery($this->root);
149 * Resets the stored extension list.
151 * We don't reset statically added filenames, as it is a static cache which
152 * logically can't change. This is done for performance reasons of the
155 public function reset() {
156 $this->extensions = NULL;
157 $this->cache->delete($this->getListCacheId());
158 $this->extensionInfo = NULL;
159 $this->cache->delete($this->getInfoCacheId());
160 $this->pathNames = NULL;
163 $this->state->delete($this->getPathnamesCacheId());
165 catch (DatabaseExceptionWrapper $e) {
166 // Ignore exceptions caused by a non existing {key_value} table in the
170 $this->cache->delete($this->getPathnamesCacheId());
171 // @todo In the long run it would be great to add the reset, but the early
172 // installer fails due to that. https://www.drupal.org/node/2719315 could
173 // help to resolve with that.
178 * Returns the extension list cache ID.
183 protected function getListCacheId() {
184 return 'core.extension.list.' . $this->type;
188 * Returns the extension info cache ID.
193 protected function getInfoCacheId() {
194 return "system.{$this->type}.info";
198 * Returns the extension filenames cache ID.
201 * The filename cache ID.
203 protected function getPathnamesCacheId() {
204 return "system.{$this->type}.files";
208 * Determines if an extension exists in the filesystem.
210 * @param string $extension_name
211 * The machine name of the extension.
214 * TRUE if the extension exists (regardless installed or not) and FALSE if
217 public function exists($extension_name) {
218 $extensions = $this->getList();
219 return isset($extensions[$extension_name]);
223 * Returns the human-readable name of the extension.
225 * @param string $extension_name
226 * The machine name of the extension.
229 * The human-readable name of the extension.
231 * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
232 * If there is no extension with the supplied machine name.
234 public function getName($extension_name) {
235 return $this->get($extension_name)->info['name'];
239 * Returns a single extension.
241 * @param string $extension_name
242 * The machine name of the extension.
244 * @return \Drupal\Core\Extension\Extension
245 * A processed extension object for the extension with the specified machine
248 * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
249 * If there is no extension with the supplied name.
251 public function get($extension_name) {
252 $extensions = $this->getList();
253 if (isset($extensions[$extension_name])) {
254 return $extensions[$extension_name];
257 throw new UnknownExtensionException("The {$this->type} $extension_name does not exist.");
261 * Returns all available extensions.
263 * @return \Drupal\Core\Extension\Extension[]
264 * Processed extension objects, keyed by machine name.
266 public function getList() {
267 if ($this->extensions !== NULL) {
268 return $this->extensions;
270 if ($cache = $this->cache->get($this->getListCacheId())) {
271 $this->extensions = $cache->data;
272 return $this->extensions;
274 $extensions = $this->doList();
275 $this->cache->set($this->getListCacheId(), $extensions);
276 $this->extensions = $extensions;
277 return $this->extensions;
281 * Scans the available extensions.
283 * Overriding this method gives other code the chance to add additional
284 * extensions to this raw listing.
286 * @return \Drupal\Core\Extension\Extension[]
287 * Unprocessed extension objects, keyed by machine name.
289 protected function doScanExtensions() {
290 return $this->getExtensionDiscovery()->scan($this->type);
294 * Builds the list of extensions.
296 * @return \Drupal\Core\Extension\Extension[]
297 * Processed extension objects, keyed by machine name.
299 * @throws \Drupal\Core\Extension\InfoParserException
300 * If one of the .info.yml files is incomplete, or causes a parsing error.
302 protected function doList() {
304 $extensions = $this->doScanExtensions();
306 // Read info files for each extension.
307 foreach ($extensions as $extension_name => $extension) {
308 // Look for the info file.
309 $extension->info = $this->infoParser->parse($extension->getPathname());
311 // Add the info file modification time, so it becomes available for
312 // contributed extensions to use for ordering extension lists.
313 $extension->info['mtime'] = $extension->getMTime();
315 // Merge extension type-specific defaults.
316 $extension->info += $this->defaults;
318 // Invoke hook_system_info_alter() to give installed modules a chance to
319 // modify the data in the .info.yml files if necessary.
320 $this->moduleHandler->alter('system_info', $extension->info, $extension, $this->type);
327 * Returns information about a specified extension.
329 * This function returns the contents of the .info.yml file for the specified
332 * @param string $extension_name
333 * The name of an extension whose information shall be returned.
336 * An associative array of extension information.
338 * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
339 * If there is no extension with the supplied name.
341 public function getExtensionInfo($extension_name) {
342 $all_info = $this->getAllInstalledInfo();
343 if (isset($all_info[$extension_name])) {
344 return $all_info[$extension_name];
346 throw new UnknownExtensionException("The {$this->type} $extension_name does not exist or is not installed.");
350 * Returns an array of info files information of available extensions.
352 * This function returns the processed contents (with added defaults) of the
356 * An associative array of extension information arrays, keyed by extension
359 public function getAllAvailableInfo() {
360 if ($this->extensionInfo === NULL) {
361 $cache_id = $this->getInfoCacheId();
362 if ($cache = $this->cache->get($cache_id)) {
363 $info = $cache->data;
366 $info = $this->recalculateInfo();
367 $this->cache->set($cache_id, $info);
369 $this->extensionInfo = $info;
372 return $this->extensionInfo;
376 * Returns a list of machine names of installed extensions.
379 * The machine names of all installed extensions of this type.
381 abstract protected function getInstalledExtensionNames();
384 * Returns an array of info files information of installed extensions.
386 * This function returns the processed contents (with added defaults) of the
390 * An associative array of extension information arrays, keyed by extension
393 public function getAllInstalledInfo() {
394 return array_intersect_key($this->getAllAvailableInfo(), array_flip($this->getInstalledExtensionNames()));
398 * Generates the information from .info.yml files for extensions of this type.
401 * An array of arrays of .info.yml entries keyed by the machine name.
403 protected function recalculateInfo() {
404 return array_map(function (Extension $extension) {
405 return $extension->info;
406 }, $this->getList());
410 * Returns a list of extension file paths keyed by machine name.
414 public function getPathnames() {
415 if ($this->pathNames === NULL) {
416 $cache_id = $this->getPathnamesCacheId();
417 if ($cache = $this->cache->get($cache_id)) {
418 $path_names = $cache->data;
420 // We use $file_names below.
421 elseif (!$path_names = $this->state->get($cache_id)) {
422 $path_names = $this->recalculatePathnames();
423 // Store filenames to allow static::getPathname() to retrieve them
424 // without having to rebuild or scan the filesystem.
425 $this->state->set($cache_id, $path_names);
426 $this->cache->set($cache_id, $path_names);
428 $this->pathNames = $path_names;
430 return $this->pathNames;
434 * Generates a sorted list of .info.yml file locations for all extensions.
437 * An array of .info.yml file locations keyed by the extension machine name.
439 protected function recalculatePathnames() {
440 $extensions = $this->getList();
443 return array_map(function (Extension $extension) {
444 return $extension->getPathname();
449 * Sets the pathname for an extension.
451 * This method is used in the Drupal bootstrapping phase, when the extension
452 * system is not fully initialized, to manually set locations of modules and
453 * profiles needed to complete bootstrapping.
455 * It is not recommended to call this method except in those rare cases.
457 * @param string $extension_name
458 * The machine name of the extension.
459 * @param string $pathname
460 * The pathname of the extension which is to be set explicitly rather
461 * than by consulting the dynamic extension listing.
467 public function setPathname($extension_name, $pathname) {
468 $this->addedPathNames[$extension_name] = $pathname;
472 * Gets the info file path for an extension.
474 * The info path, whether provided, cached, or retrieved from the database, is
475 * only returned if the file exists.
477 * This function plays a key role in allowing Drupal's extensions (modules,
478 * themes, profiles, theme_engines, etc.) to be located in different places
479 * depending on a site's configuration. For example, a module 'foo' may
480 * legally be located in any of these four places:
482 * - core/modules/foo/foo.info.yml
483 * - modules/foo/foo.info.yml
484 * - sites/all/modules/foo/foo.info.yml
485 * - sites/example.com/modules/foo/foo.info.yml
487 * while a theme 'bar' may be located in any of the following four places:
489 * - core/themes/bar/bar.info.yml
490 * - themes/bar/bar.info.yml
491 * - sites/all/themes/bar/bar.info.yml
492 * - sites/example.com/themes/bar/bar.info.yml
494 * An installation profile maybe be located in any of the following places:
496 * - core/profiles/baz/baz.info.yml
497 * - profiles/baz/baz.info.yml
499 * Calling ExtensionList::getPathname('foo') will give you one of the above,
500 * depending on where the extension is located and what type it is.
502 * @param string $extension_name
503 * The machine name of the extension for which the pathname is requested.
506 * The drupal-root relative filename and path of the requested extension's
509 * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
510 * If there is no extension with the supplied machine name.
512 public function getPathname($extension_name) {
513 if (isset($this->addedPathNames[$extension_name])) {
514 return $this->addedPathNames[$extension_name];
516 elseif (isset($this->pathNames[$extension_name])) {
517 return $this->pathNames[$extension_name];
519 elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) {
520 return $path_names[$extension_name];
522 throw new UnknownExtensionException("The {$this->type} $extension_name does not exist.");
526 * Gets the path to an extension of a specific type (module, theme, etc.).
528 * The path is the directory in which the .info file is located. This name is
529 * coming from \SplFileInfo.
531 * @param string $extension_name
532 * The machine name of the extension for which the path is requested.
535 * The Drupal-root-relative path to the specified extension.
537 * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException
538 * If there is no extension with the supplied name.
540 public function getPath($extension_name) {
541 return dirname($this->getPathname($extension_name));