Commit 10722cd8 authored by catch's avatar catch

Issue #2659940 by almaudoh, alexpott, phenaproxima, Jo Fitzgerald, markcarver,...

Issue #2659940 by almaudoh, alexpott, phenaproxima, Jo Fitzgerald, markcarver, oriol_e9g, dawehner, dsnopek, jibran, larowlan: Extension System, Part III: ThemeExtensionList and ThemeEngineExtensionList
parent bab4f6d3
...@@ -517,6 +517,12 @@ services: ...@@ -517,6 +517,12 @@ services:
extension.list.profile: extension.list.profile:
class: Drupal\Core\Extension\ProfileExtensionList class: Drupal\Core\Extension\ProfileExtensionList
arguments: ['@app.root', 'profile', '@cache.default', '@info_parser', '@module_handler', '@state', '%install_profile%'] arguments: ['@app.root', 'profile', '@cache.default', '@info_parser', '@module_handler', '@state', '%install_profile%']
extension.list.theme:
class: Drupal\Core\Extension\ThemeExtensionList
arguments: ['@app.root', 'theme', '@cache.default', '@info_parser', '@module_handler', '@state', '@config.factory', '@extension.list.theme_engine', '%install_profile%']
extension.list.theme_engine:
class: Drupal\Core\Extension\ThemeEngineExtensionList
arguments: ['@app.root', 'theme_engine', '@cache.default', '@info_parser', '@module_handler', '@state', '%install_profile%']
content_uninstall_validator: content_uninstall_validator:
class: Drupal\Core\Entity\ContentUninstallValidator class: Drupal\Core\Entity\ContentUninstallValidator
tags: tags:
...@@ -531,7 +537,7 @@ services: ...@@ -531,7 +537,7 @@ services:
lazy: true lazy: true
theme_handler: theme_handler:
class: Drupal\Core\Extension\ThemeHandler class: Drupal\Core\Extension\ThemeHandler
arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser'] arguments: ['@app.root', '@config.factory', '@extension.list.theme']
theme_installer: theme_installer:
class: Drupal\Core\Extension\ThemeInstaller class: Drupal\Core\Extension\ThemeInstaller
arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state'] arguments: ['@theme_handler', '@config.factory', '@config.installer', '@module_handler', '@config.manager', '@asset.css.collection_optimizer', '@router.builder', '@logger.channel.default', '@state']
......
...@@ -10,12 +10,12 @@ ...@@ -10,12 +10,12 @@
use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\Config\BootstrapConfigStorageFactory;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Test\TestDatabase; use Drupal\Core\Test\TestDatabase;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Error; use Drupal\Core\Utility\Error;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
/** /**
* Minimum supported version of PHP. * Minimum supported version of PHP.
...@@ -223,10 +223,6 @@ function config_get_config_directory($type) { ...@@ -223,10 +223,6 @@ function config_get_config_directory($type) {
* The filename of the requested item or NULL if the item is not found. * The filename of the requested item or NULL if the item is not found.
*/ */
function drupal_get_filename($type, $name, $filename = NULL) { function drupal_get_filename($type, $name, $filename = NULL) {
// The location of files will not change during the request, so do not use
// drupal_static().
static $files = [];
// Type 'core' only exists to simplify application-level logic; it always maps // Type 'core' only exists to simplify application-level logic; it always maps
// to the /core directory, whereas $name is ignored. It is only requested via // to the /core directory, whereas $name is ignored. It is only requested via
// drupal_get_path(). /core/core.info.yml does not exist, but is required // drupal_get_path(). /core/core.info.yml does not exist, but is required
...@@ -235,45 +231,31 @@ function drupal_get_filename($type, $name, $filename = NULL) { ...@@ -235,45 +231,31 @@ function drupal_get_filename($type, $name, $filename = NULL) {
return 'core/core.info.yml'; return 'core/core.info.yml';
} }
if ($type === 'module' || $type === 'profile') { try {
$service_id = 'extension.list.' . $type;
/** @var \Drupal\Core\Extension\ExtensionList $extension_list */ /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
$extension_list = \Drupal::service($service_id); $extension_list = \Drupal::service("extension.list.$type");
if (isset($filename)) { if (isset($filename)) {
// Manually add the info file path of an extension. // Manually add the info file path of an extension.
$extension_list->setPathname($name, $filename); $extension_list->setPathname($name, $filename);
} }
try { return $extension_list->getPathname($name);
return $extension_list->getPathname($name);
}
catch (UnknownExtensionException $e) {
// Catch the exception. This will result in triggering an error.
}
} }
else { catch (ServiceNotFoundException $e) {
// Catch the exception. This will result in triggering an error.
if (!isset($files[$type])) { // If the service is unknown, create a user-level error message.
$files[$type] = []; trigger_error(
} sprintf('Unknown type specified: "%s". Must be one of: "core", "profile", "module", "theme", or "theme_engine".', $type),
E_USER_WARNING
if (isset($filename)) { );
$files[$type][$name] = $filename; }
} catch (\InvalidArgumentException $e) {
elseif (!isset($files[$type][$name])) { // Catch the exception. This will result in triggering an error.
// If still unknown, retrieve the file list prepared in state by // If the filename is still unknown, create a user-level error message.
// \Drupal\Core\Extension\ExtensionList() and trigger_error(
// \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). sprintf('The following %s is missing from the file system: %s', $type, $name),
if (!isset($files[$type][$name]) && \Drupal::hasService('state')) { E_USER_WARNING
$files[$type] += \Drupal::state()->get('system.' . $type . '.files', []); );
}
}
if (isset($files[$type][$name])) {
return $files[$type][$name];
}
} }
// If the filename is still unknown, create a user-level error message.
trigger_error(new FormattableMarkup('The following @type is missing from the file system: @name', ['@type' => $type, '@name' => $name]), E_USER_WARNING);
} }
/** /**
......
...@@ -14,41 +14,31 @@ ...@@ -14,41 +14,31 @@
* The type of list to return: * The type of list to return:
* - theme: All installed themes. * - theme: All installed themes.
* *
* @return * @return array
* An associative array of themes, keyed by name. * An associative array of themes, keyed by name.
* For $type 'theme', the array values are objects representing the * For $type 'theme', the array values are objects representing the
* respective database row, with the 'info' property already unserialized. * respective database row, with the 'info' property already unserialized.
* *
* @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use
* \Drupal::service('theme_handler')->listInfo() instead.
*
* @see https://www.drupal.org/node/2709919
* @see \Drupal\Core\Extension\ThemeHandler::listInfo() * @see \Drupal\Core\Extension\ThemeHandler::listInfo()
*/ */
function system_list($type) { function system_list($type) {
$lists = &drupal_static(__FUNCTION__); @trigger_error('system_list() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Use \Drupal::service(\'theme_handler\')->listInfo() instead. See https://www.drupal.org/node/2709919', E_USER_DEPRECATED);
if ($cached = \Drupal::cache('bootstrap')->get('system_list')) {
$lists = $cached->data; $lists = [
} 'theme' => \Drupal::service('theme_handler')->listInfo(),
else { 'filepaths' => [],
$lists = [ ];
'theme' => [], foreach ($lists['theme'] as $name => $theme) {
'filepaths' => [], $lists['filepaths'][] = [
'type' => 'theme',
'name' => $name,
'filepath' => $theme->getPathname(),
]; ];
// ThemeHandler maintains the 'system.theme.data' state record.
$theme_data = \Drupal::state()->get('system.theme.data', []);
foreach ($theme_data as $name => $theme) {
$lists['theme'][$name] = $theme;
$lists['filepaths'][] = [
'type' => 'theme',
'name' => $name,
'filepath' => $theme->getPathname(),
];
}
\Drupal::cache('bootstrap')->set('system_list', $lists);
}
// To avoid a separate database lookup for the filepath, prime the
// drupal_get_filename() static cache with all enabled themes.
foreach ($lists['filepaths'] as $item) {
system_register($item['type'], $item['name'], $item['filepath']);
} }
return $lists[$type]; return $lists[$type];
} }
...@@ -56,9 +46,10 @@ function system_list($type) { ...@@ -56,9 +46,10 @@ function system_list($type) {
* Resets all system_list() caches. * Resets all system_list() caches.
*/ */
function system_list_reset() { function system_list_reset() {
drupal_static_reset('system_list'); \Drupal::service('extension.list.profile')->reset();
\Drupal::service('extension.list.module')->reset(); \Drupal::service('extension.list.module')->reset();
\Drupal::cache('bootstrap')->delete('system_list'); \Drupal::service('extension.list.theme_engine')->reset();
\Drupal::service('extension.list.theme')->reset();
} }
/** /**
......
...@@ -99,7 +99,7 @@ function theme_get_registry($complete = TRUE) { ...@@ -99,7 +99,7 @@ function theme_get_registry($complete = TRUE) {
/** /**
* Returns an array of default theme features. * Returns an array of default theme features.
* *
* @see \Drupal\Core\Extension\ThemeHandler::$defaultFeatures * @see \Drupal\Core\Extension\ThemeExtensionList::$defaults
*/ */
function _system_default_theme_features() { function _system_default_theme_features() {
return [ return [
......
...@@ -68,7 +68,7 @@ function _drupal_maintenance_theme() { ...@@ -68,7 +68,7 @@ function _drupal_maintenance_theme() {
$theme_init = \Drupal::service('theme.initialization'); $theme_init = \Drupal::service('theme.initialization');
$theme_handler = \Drupal::service('theme_handler'); $theme_handler = \Drupal::service('theme_handler');
if (empty($themes) || !isset($themes[$custom_theme])) { if (empty($themes) || !isset($themes[$custom_theme])) {
$themes = $theme_handler->rebuildThemeData(); $themes = \Drupal::service('extension.list.theme')->getList();
$theme_handler->addTheme($themes[$custom_theme]); $theme_handler->addTheme($themes[$custom_theme]);
} }
......
...@@ -169,8 +169,8 @@ public function serialize() { ...@@ -169,8 +169,8 @@ public function serialize() {
'filename' => $this->filename, 'filename' => $this->filename,
]; ];
// @todo ThemeHandler::listInfo(), ThemeHandler::rebuildThemeData(), and // @todo \Drupal\Core\Extension\ThemeExtensionList is adding custom
// system_list() are adding custom properties to the Extension object. // properties to the Extension object.
$info = new \ReflectionObject($this); $info = new \ReflectionObject($this);
foreach ($info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { foreach ($info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$data[$property->getName()] = $property->getValue($this); $data[$property->getName()] = $property->getValue($this);
......
...@@ -11,6 +11,12 @@ ...@@ -11,6 +11,12 @@
* Provides available extensions. * Provides available extensions.
* *
* The extension list is per extension type, like module, theme and profile. * The extension list is per extension type, like module, theme and profile.
*
* @internal
* This class is not yet stable and therefore there are no guarantees that the
* internal implementations including constructor signature and protected
* properties / methods will not change over time. This will be reviewed after
* https://www.drupal.org/project/drupal/issues/2940481
*/ */
abstract class ExtensionList { abstract class ExtensionList {
...@@ -305,15 +311,7 @@ protected function doList() { ...@@ -305,15 +311,7 @@ protected function doList() {
// Read info files for each extension. // Read info files for each extension.
foreach ($extensions as $extension_name => $extension) { foreach ($extensions as $extension_name => $extension) {
// Look for the info file. $extension->info = $this->createExtensionInfo($extension);
$extension->info = $this->infoParser->parse($extension->getPathname());
// Add the info file modification time, so it becomes available for
// contributed extensions to use for ordering extension lists.
$extension->info['mtime'] = $extension->getMTime();
// Merge extension type-specific defaults.
$extension->info += $this->defaults;
// Invoke hook_system_info_alter() to give installed modules a chance to // Invoke hook_system_info_alter() to give installed modules a chance to
// modify the data in the .info.yml files if necessary. // modify the data in the .info.yml files if necessary.
...@@ -541,4 +539,26 @@ public function getPath($extension_name) { ...@@ -541,4 +539,26 @@ public function getPath($extension_name) {
return dirname($this->getPathname($extension_name)); return dirname($this->getPathname($extension_name));
} }
/**
* Creates the info value for an extension object.
*
* @param \Drupal\Core\Extension\Extension $extension
* The extension whose info is to be altered.
*
* @return array
* The extension info array.
*/
protected function createExtensionInfo(Extension $extension) {
$info = $this->infoParser->parse($extension->getPathname());
// Add the info file modification time, so it becomes available for
// contributed extensions to use for ordering extension lists.
$info['mtime'] = $extension->getMTime();
// Merge extension type-specific defaults.
$info += $this->defaults;
return $info;
}
} }
...@@ -9,6 +9,12 @@ ...@@ -9,6 +9,12 @@
/** /**
* Provides a list of available modules. * Provides a list of available modules.
*
* @internal
* This class is not yet stable and therefore there are no guarantees that the
* internal implementations including constructor signature and protected
* properties / methods will not change over time. This will be reviewed after
* https://www.drupal.org/project/drupal/issues/2940481
*/ */
class ModuleExtensionList extends ExtensionList { class ModuleExtensionList extends ExtensionList {
......
...@@ -4,6 +4,12 @@ ...@@ -4,6 +4,12 @@
/** /**
* Provides a list of installation profiles. * Provides a list of installation profiles.
*
* @internal
* This class is not yet stable and therefore there are no guarantees that the
* internal implementations including constructor signature and protected
* properties / methods will not change over time. This will be reviewed after
* https://www.drupal.org/project/drupal/issues/2940481
*/ */
class ProfileExtensionList extends ExtensionList { class ProfileExtensionList extends ExtensionList {
......
<?php
namespace Drupal\Core\Extension;
/**
* Provides a list of available theme engines.
*
* @internal
* This class is not yet stable and therefore there are no guarantees that the
* internal implementations including constructor signature and protected
* properties / methods will not change over time. This will be reviewed after
* https://www.drupal.org/project/drupal/issues/2940481
*/
class ThemeEngineExtensionList extends ExtensionList {
/**
* {@inheritdoc}
*/
protected $defaults = [
'dependencies' => [],
'description' => '',
'package' => 'Other',
'version' => NULL,
'php' => DRUPAL_MINIMUM_PHP,
];
/**
* {@inheritdoc}
*/
protected function getInstalledExtensionNames() {
// Theme engines do not have an 'install' state, so return names of all
// discovered theme engines.
return array_keys($this->extensions);
}
}
<?php
namespace Drupal\Core\Extension;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\State\StateInterface;
/**
* Provides a list of available themes.
*
* @internal
* This class is not yet stable and therefore there are no guarantees that the
* internal implementations including constructor signature and protected
* properties / methods will not change over time. This will be reviewed after
* https://www.drupal.org/project/drupal/issues/2940481
*/
class ThemeExtensionList extends ExtensionList {
/**
* {@inheritdoc}
*/
protected $defaults = [
'engine' => 'twig',
'base theme' => 'stable',
'regions' => [
'sidebar_first' => 'Left sidebar',
'sidebar_second' => 'Right sidebar',
'content' => 'Content',
'header' => 'Header',
'primary_menu' => 'Primary menu',
'secondary_menu' => 'Secondary menu',
'footer' => 'Footer',
'highlighted' => 'Highlighted',
'help' => 'Help',
'page_top' => 'Page top',
'page_bottom' => 'Page bottom',
'breadcrumb' => 'Breadcrumb',
],
'description' => '',
// The following array should be kept inline with
// _system_default_theme_features().
'features' => [
'favicon',
'logo',
'node_user_picture',
'comment_user_picture',
'comment_user_verification',
],
'screenshot' => 'screenshot.png',
'php' => DRUPAL_MINIMUM_PHP,
'libraries' => [],
'libraries_extend' => [],
'libraries_override' => [],
];
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The theme engine list needed by this theme list.
*
* @var \Drupal\Core\Extension\ThemeEngineExtensionList
*/
protected $engineList;
/**
* The list of installed themes.
*
* @var string[]
*/
protected $installedThemes;
/**
* Constructs a new ThemeExtensionList instance.
*
* @param string $root
* The app root.
* @param string $type
* The extension type.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache.
* @param \Drupal\Core\Extension\InfoParserInterface $info_parser
* The info parser.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ThemeEngineExtensionList $engine_list
* The theme engine extension listing.
* @param string $install_profile
* The install profile used by the site.
*/
public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, StateInterface $state, ConfigFactoryInterface $config_factory, ThemeEngineExtensionList $engine_list, $install_profile) {
parent::__construct($root, $type, $cache, $info_parser, $module_handler, $state, $install_profile);
$this->configFactory = $config_factory;
$this->engineList = $engine_list;
}
/**
* {@inheritdoc}
*/
protected function doList() {
// Find themes.
$themes = parent::doList();
$engines = $this->engineList->getList();
// Always get the freshest list of themes (rather than the already cached
// list in $this->installedThemes) when building the theme listing because a
// theme could have just been installed or uninstalled.
$this->installedThemes = $this->configFactory->get('core.extension')->get('theme') ?: [];
$sub_themes = [];
// Read info files for each theme.
foreach ($themes as $name => $theme) {
// Defaults to 'twig' (see self::defaults above).
$engine = $theme->info['engine'];
if (isset($engines[$engine])) {
$theme->owner = $engines[$engine]->getExtensionPathname();
$theme->prefix = $engines[$engine]->getName();
}
// Add this theme as a sub-theme if it has a base theme.
if (!empty($theme->info['base theme'])) {
$sub_themes[] = $name;
}
// Add weight and status.
$theme->status = (int) isset($this->installedThemes[$name]);
$theme->weight = isset($this->installedThemes[$name]) ? $this->installedThemes[$name] : 0;
}
// Build dependencies.
$themes = $this->moduleHandler->buildModuleDependencies($themes);
// After establishing the full list of available themes, fill in data for
// sub-themes.
$this->fillInSubThemeData($themes, $sub_themes);
return $themes;
}
/**
* Fills in data for themes that are also sub-themes.
*
* @param array $themes
* The array of partly processed theme information.
* @param array $sub_themes
* A list of themes from the $theme array that are also sub-themes.
*/
protected function fillInSubThemeData(array &$themes, array $sub_themes) {
foreach ($sub_themes as $name) {
$sub_theme = $themes[$name];
// The $base_themes property is optional; only set for sub themes.
// @see ThemeHandlerInterface::listInfo()
$sub_theme->base_themes = $this->doGetBaseThemes($themes, $name);
// empty() cannot be used here, since static::doGetBaseThemes() adds
// the key of a base theme with a value of NULL in case it is not found,
// in order to prevent needless iterations.
if (!current($sub_theme->base_themes)) {
continue;
}
// Determine the root base theme.
$root_key = key($sub_theme->base_themes);
// Build the list of sub-themes for each of the theme's base themes.
foreach (array_keys($sub_theme->base_themes) as $base_theme) {
$themes[$base_theme]->sub_themes[$name] = $sub_theme->info['name'];
}
// Add the theme engine info from the root base theme.
if (isset($themes[$root_key]->owner)) {
$sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
$sub_theme->owner = $themes[$root_key]->owner;
$sub_theme->prefix = $themes[$root_key]->prefix;
}
}
}
/**
* Finds all the base themes for the specified theme.
*
* Themes can inherit templates and function implementations from earlier
* themes.
*
* @param \Drupal\Core\Extension\Extension[] $themes
* An array of available themes.
* @param string $theme
* The name of the theme whose base we are looking for.
*
* @return array
* Returns an array of all of the theme's ancestors; the first element's
* value will be NULL if an error occurred.
*/
public function getBaseThemes(array $themes, $theme) {
return $this->doGetBaseThemes($themes, $theme);
}
/**
* Finds the base themes for the specific theme.
*
* @param array $themes
* An array of available themes.
* @param string $theme
* The name of the theme whose base we are looking for.
* @param array $used_themes
* (optional) A recursion parameter preventing endless loops. Defaults to
* an empty array.
*
* @return array
* An array of base themes.
*/
protected function doGetBaseThemes(array $themes, $theme, array $used_themes = []) {
if (!isset($themes[$theme]->info['base theme'])) {
return [];
}
$base_key = $themes[$theme]->info['base theme'];
// Does the base theme exist?
if (!isset($themes[$base_key])) {
return [$base_key => NULL];
}
$current_base_theme = [$base_key => $themes[$base_key]->info['name']];
// Is the base theme itself a child of another theme?
if (isset($themes[$base_key]->info['base theme'])) {
// Do we already know the base themes of this theme?
if (isset($themes[$base_key]->base_themes)) {
return $themes[$base_key]->base_themes + $current_base_theme;
}
// Prevent loops.
if (!empty($used_themes[$base_key])) {
return [$base_key => NULL];
}
$used_themes[$base_key] = TRUE;
return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
}
// If we get here, then this is our parent theme.
return $current_base_theme;
}
/**
* {@inheritdoc}
*/
protected function createExtensionInfo(Extension $extension) {
$info = parent::createExtensionInfo($extension);
// Remove the default Stable base theme when 'base theme: false' is set in
// a theme .info.yml file.
if ($info['base theme'] === FALSE) {
unset($info['base theme']);
}
if (!empty($info['base theme'])) {
// Add the base theme as a proper dependency.
$info['dependencies'][] = $info['base theme'];
}
// Prefix screenshot with theme path.
if (!empty($info['screenshot'])) {
$info['screenshot'] = $extension->getPath() . '/' . $info['screenshot'];
}
return $info;
}
/**
* {@inheritdoc}
*/
protected function getInstalledExtensionNames() {
// Cache the installed themes to avoid multiple calls to the config system.
if (!isset($this->installedThemes)) {
$this->installedThemes = $this->configFactory->get('core.extension')->get('theme') ?: [];
}
return array_keys($this->installedThemes);
}
/**
* {@inheritdoc}
*/
public function reset() {
parent::reset();
$this->installedThemes = NULL;
return $this;
}
}
...@@ -56,8 +56,8 @@ public function uninstall(array $theme_list); ...@@ -56,8 +56,8 @@ public function uninstall(array $theme_list);
* *
* @return \Drupal\Core\Extension\Extension[] * @return \Drupal\Core\Extension\Extension[]
* An associative array of the currently installed themes. The keys are the * An associative array of the currently installed themes. The keys are the
* themes' machine names and the values are objects having the following * themes' machine names and the values are Extension objects having the
* properties: * following properties: