diff --git a/core/core.services.yml b/core/core.services.yml index 074474874f6fbddb29c5eb78045d91dcf31799ff..66a46c66036c22c9004b8a39806296bda83adcfa 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -517,6 +517,12 @@ services: extension.list.profile: class: Drupal\Core\Extension\ProfileExtensionList 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: class: Drupal\Core\Entity\ContentUninstallValidator tags: @@ -531,7 +537,7 @@ services: lazy: true theme_handler: 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: 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'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index d58779d5374ec92465814f6f13039dc873392d51..afd267adcf7306c357ea250ff8bbe7bf060775f4 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -10,12 +10,12 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\BootstrapConfigStorageFactory; -use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Logger\RfcLogLevel; use Drupal\Core\Test\TestDatabase; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Utility\Error; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; /** * Minimum supported version of PHP. @@ -223,10 +223,6 @@ function config_get_config_directory($type) { * The filename of the requested item or NULL if the item is not found. */ 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 // 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 @@ -235,45 +231,31 @@ function drupal_get_filename($type, $name, $filename = NULL) { return 'core/core.info.yml'; } - if ($type === 'module' || $type === 'profile') { - $service_id = 'extension.list.' . $type; + try { /** @var \Drupal\Core\Extension\ExtensionList $extension_list */ - $extension_list = \Drupal::service($service_id); + $extension_list = \Drupal::service("extension.list.$type"); if (isset($filename)) { // Manually add the info file path of an extension. $extension_list->setPathname($name, $filename); } - try { - return $extension_list->getPathname($name); - } - catch (UnknownExtensionException $e) { - // Catch the exception. This will result in triggering an error. - } + return $extension_list->getPathname($name); } - else { - - if (!isset($files[$type])) { - $files[$type] = []; - } - - if (isset($filename)) { - $files[$type][$name] = $filename; - } - elseif (!isset($files[$type][$name])) { - // If still unknown, retrieve the file list prepared in state by - // \Drupal\Core\Extension\ExtensionList() and - // \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). - if (!isset($files[$type][$name]) && \Drupal::hasService('state')) { - $files[$type] += \Drupal::state()->get('system.' . $type . '.files', []); - } - } - - if (isset($files[$type][$name])) { - return $files[$type][$name]; - } + catch (ServiceNotFoundException $e) { + // Catch the exception. This will result in triggering an error. + // If the service is unknown, create a user-level error message. + trigger_error( + sprintf('Unknown type specified: "%s". Must be one of: "core", "profile", "module", "theme", or "theme_engine".', $type), + E_USER_WARNING + ); + } + catch (\InvalidArgumentException $e) { + // Catch the exception. This will result in triggering an error. + // If the filename is still unknown, create a user-level error message. + trigger_error( + sprintf('The following %s is missing from the file system: %s', $type, $name), + E_USER_WARNING + ); } - // 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); } /** diff --git a/core/includes/module.inc b/core/includes/module.inc index 63b3abc5ef14a4eae1109f5219900f2fcba68d89..aee832949c1f5b7fd686b70099e6ac41b69f2b5a 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -14,41 +14,31 @@ * The type of list to return: * - theme: All installed themes. * - * @return + * @return array * An associative array of themes, keyed by name. * For $type 'theme', the array values are objects representing the * 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() */ function system_list($type) { - $lists = &drupal_static(__FUNCTION__); - if ($cached = \Drupal::cache('bootstrap')->get('system_list')) { - $lists = $cached->data; - } - else { - $lists = [ - 'theme' => [], - 'filepaths' => [], + @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); + + $lists = [ + 'theme' => \Drupal::service('theme_handler')->listInfo(), + 'filepaths' => [], + ]; + foreach ($lists['theme'] as $name => $theme) { + $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]; } @@ -56,9 +46,10 @@ function system_list($type) { * Resets all system_list() caches. */ function system_list_reset() { - drupal_static_reset('system_list'); + \Drupal::service('extension.list.profile')->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(); } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 79ceaccfe62c25acae0a77fc0241f5266e59b2ac..f5f553ddf45c0fced66084f6270b2c86a7f36e06 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -99,7 +99,7 @@ function theme_get_registry($complete = TRUE) { /** * Returns an array of default theme features. * - * @see \Drupal\Core\Extension\ThemeHandler::$defaultFeatures + * @see \Drupal\Core\Extension\ThemeExtensionList::$defaults */ function _system_default_theme_features() { return [ diff --git a/core/includes/theme.maintenance.inc b/core/includes/theme.maintenance.inc index 310c9cd858a3fca0a8fea067dcebd75165425863..2982f845421f69d6833cc91f9722f87e3cfececf 100644 --- a/core/includes/theme.maintenance.inc +++ b/core/includes/theme.maintenance.inc @@ -68,7 +68,7 @@ function _drupal_maintenance_theme() { $theme_init = \Drupal::service('theme.initialization'); $theme_handler = \Drupal::service('theme_handler'); if (empty($themes) || !isset($themes[$custom_theme])) { - $themes = $theme_handler->rebuildThemeData(); + $themes = \Drupal::service('extension.list.theme')->getList(); $theme_handler->addTheme($themes[$custom_theme]); } diff --git a/core/lib/Drupal/Core/Extension/Extension.php b/core/lib/Drupal/Core/Extension/Extension.php index 830c395b820b3e4bf3e4fd6de8e8b8afa955c106..3b8edbe32cb81b85df3386974e336b05dd5f291e 100644 --- a/core/lib/Drupal/Core/Extension/Extension.php +++ b/core/lib/Drupal/Core/Extension/Extension.php @@ -169,8 +169,8 @@ public function serialize() { 'filename' => $this->filename, ]; - // @todo ThemeHandler::listInfo(), ThemeHandler::rebuildThemeData(), and - // system_list() are adding custom properties to the Extension object. + // @todo \Drupal\Core\Extension\ThemeExtensionList is adding custom + // properties to the Extension object. $info = new \ReflectionObject($this); foreach ($info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { $data[$property->getName()] = $property->getValue($this); diff --git a/core/lib/Drupal/Core/Extension/ExtensionList.php b/core/lib/Drupal/Core/Extension/ExtensionList.php index 6b5dfec45b08b1a49128bd7c8cb1ced437fcf66b..dc20679cd2bcf76cbf3093f89cb5336551cb32d0 100644 --- a/core/lib/Drupal/Core/Extension/ExtensionList.php +++ b/core/lib/Drupal/Core/Extension/ExtensionList.php @@ -11,6 +11,12 @@ * Provides available extensions. * * 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 { @@ -305,15 +311,7 @@ protected function doList() { // Read info files for each extension. foreach ($extensions as $extension_name => $extension) { - // Look for the info file. - $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; + $extension->info = $this->createExtensionInfo($extension); // Invoke hook_system_info_alter() to give installed modules a chance to // modify the data in the .info.yml files if necessary. @@ -541,4 +539,26 @@ public function getPath($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; + } + } diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php index 7db535b350ed888692b3ee8b263cbc9f11d88451..c8f492bd4fe9ad5cdf4fc401bb0845fc7ac3e0af 100644 --- a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php +++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php @@ -9,6 +9,12 @@ /** * 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 { diff --git a/core/lib/Drupal/Core/Extension/ProfileExtensionList.php b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php index 4f73f9c9cbf3ced36889a358a32cf0c99d4c32cb..7c415168d8adf5a37858ed3a1280128bd5958472 100644 --- a/core/lib/Drupal/Core/Extension/ProfileExtensionList.php +++ b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php @@ -4,6 +4,12 @@ /** * 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 { diff --git a/core/lib/Drupal/Core/Extension/ThemeEngineExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeEngineExtensionList.php new file mode 100644 index 0000000000000000000000000000000000000000..c4d39ef6008bf385a0de3e082dc01b1dff9df977 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ThemeEngineExtensionList.php @@ -0,0 +1,36 @@ +<?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); + } + +} diff --git a/core/lib/Drupal/Core/Extension/ThemeExtensionList.php b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php new file mode 100644 index 0000000000000000000000000000000000000000..5397ba2fc5c58453a8cd87a56d199c9e235c0b12 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ThemeExtensionList.php @@ -0,0 +1,289 @@ +<?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; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index 4258fda9fa4f137e13f42de2bf93c3b01fe51d62..a5dd850d88ee6e8fbbbdf2b0cc26b3117a3da495 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -5,28 +5,12 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\Exception\UninstalledExtensionException; use Drupal\Core\Extension\Exception\UnknownExtensionException; -use Drupal\Core\State\StateInterface; /** * Default theme handler using the config system to store installation statuses. */ class ThemeHandler implements ThemeHandlerInterface { - /** - * Contains the features enabled for themes by default. - * - * @var array - * - * @see _system_default_theme_features() - */ - protected $defaultFeatures = [ - 'favicon', - 'logo', - 'node_user_picture', - 'comment_user_picture', - 'comment_user_verification', - ]; - /** * A list of all currently available themes. * @@ -41,68 +25,12 @@ class ThemeHandler implements ThemeHandlerInterface { */ protected $configFactory; - /** - * The module handler to fire themes_installed/themes_uninstalled hooks. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface - */ - protected $moduleHandler; - - /** - * The state backend. - * - * @var \Drupal\Core\State\StateInterface - */ - protected $state; - - /** - * The config installer to install configuration. - * - * @var \Drupal\Core\Config\ConfigInstallerInterface - */ - protected $configInstaller; - - /** - * The info parser to parse the theme.info.yml files. - * - * @var \Drupal\Core\Extension\InfoParserInterface - */ - protected $infoParser; - - /** - * A logger instance. - * - * @var \Psr\Log\LoggerInterface - */ - protected $logger; - - /** - * The route builder to rebuild the routes if a theme is installed. - * - * @var \Drupal\Core\Routing\RouteBuilderInterface - */ - protected $routeBuilder; - /** * An extension discovery instance. * - * @var \Drupal\Core\Extension\ExtensionDiscovery - */ - protected $extensionDiscovery; - - /** - * The CSS asset collection optimizer service. - * - * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface - */ - protected $cssCollectionOptimizer; - - /** - * The config manager used to uninstall a theme. - * - * @var \Drupal\Core\Config\ConfigManagerInterface + * @var \Drupal\Core\Extension\ThemeExtensionList */ - protected $configManager; + protected $themeList; /** * The app root. @@ -118,22 +46,13 @@ class ThemeHandler implements ThemeHandlerInterface { * The app root. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory to get the installed themes. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler to fire themes_installed/themes_uninstalled hooks. - * @param \Drupal\Core\State\StateInterface $state - * The state store. - * @param \Drupal\Core\Extension\InfoParserInterface $info_parser - * The info parser to parse the theme.info.yml files. - * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery - * (optional) A extension discovery instance (for unit tests). + * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list + * A extension discovery instance. */ - public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) { + public function __construct($root, ConfigFactoryInterface $config_factory, ThemeExtensionList $theme_list) { $this->root = $root; $this->configFactory = $config_factory; - $this->moduleHandler = $module_handler; - $this->state = $state; - $this->infoParser = $info_parser; - $this->extensionDiscovery = $extension_discovery; + $this->themeList = $theme_list; } /** @@ -181,16 +100,10 @@ public function uninstall(array $theme_list) { public function listInfo() { if (!isset($this->list)) { $this->list = []; - $themes = $this->systemThemeList(); - // @todo Ensure that systemThemeList() does not contain an empty list - // during the batch installer, see https://www.drupal.org/node/2322619. - if (empty($themes)) { - $this->refreshInfo(); - $this->list = $this->list ?: []; - $themes = \Drupal::state()->get('system.theme.data', []); - } - foreach ($themes as $theme) { - $this->addTheme($theme); + $installed_themes = $this->configFactory->get('core.extension')->get('theme'); + if (!empty($installed_themes)) { + $installed_themes = array_intersect_key($this->themeList->getList(), $installed_themes); + array_map([$this, 'addTheme'], $installed_themes); } } return $this->list; @@ -200,6 +113,11 @@ public function listInfo() { * {@inheritdoc} */ public function addTheme(Extension $theme) { + // Register the namespaces of installed themes. + // @todo Implement proper theme registration + // https://www.drupal.org/project/drupal/issues/2941757 + \Drupal::service('class_loader')->addPsr4('Drupal\\' . $theme->getName() . '\\', $this->root . '/' . $theme->getPath() . '/src'); + if (!empty($theme->info['libraries'])) { foreach ($theme->info['libraries'] as $library => $name) { $theme->libraries[$library] = $name; @@ -218,32 +136,21 @@ public function addTheme(Extension $theme) { * {@inheritdoc} */ public function refreshInfo() { - $extension_config = $this->configFactory->get('core.extension'); - $installed = $extension_config->get('theme'); + $installed = $this->configFactory->get('core.extension')->get('theme'); // Only refresh the info if a theme has been installed. Modules are // installed before themes by the installer and this method is called during // module installation. if (empty($installed) && empty($this->list)) { return; } - $this->reset(); - // @todo Avoid re-scanning all themes by retaining the original (unaltered) - // theme info somewhere. - $list = $this->rebuildThemeData(); - foreach ($list as $name => $theme) { - if (isset($installed[$name])) { - $this->addTheme($theme); - } - } - $this->state->set('system.theme.data', $this->list); } /** * {@inheritdoc} */ public function reset() { - $this->systemListReset(); + $this->themeList->reset(); $this->list = NULL; } @@ -251,214 +158,21 @@ public function reset() { * {@inheritdoc} */ public function rebuildThemeData() { - $listing = $this->getExtensionDiscovery(); - $themes = $listing->scan('theme'); - $engines = $listing->scan('theme_engine'); - $extension_config = $this->configFactory->get('core.extension'); - $installed = $extension_config->get('theme') ?: []; - - // Set defaults for theme info. - $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' => '', - 'features' => $this->defaultFeatures, - 'screenshot' => 'screenshot.png', - 'php' => DRUPAL_MINIMUM_PHP, - 'libraries' => [], - ]; - - $sub_themes = []; - $files_theme = []; - $files_theme_engine = []; - // Read info files for each theme. - foreach ($themes as $key => $theme) { - // @todo Remove all code that relies on the $status property. - $theme->status = (int) isset($installed[$key]); - - $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults; - // Remove the default Stable base theme when 'base theme: false' is set in - // a theme .info.yml file. - if ($theme->info['base theme'] === FALSE) { - unset($theme->info['base theme']); - } - - // Add the info file modification time, so it becomes available for - // contributed modules to use for ordering theme lists. - $theme->info['mtime'] = $theme->getMTime(); - - // Invoke hook_system_info_alter() to give installed modules a chance to - // modify the data in the .info.yml files if necessary. - // @todo Remove $type argument, obsolete with $theme->getType(). - $type = 'theme'; - $this->moduleHandler->alter('system_info', $theme->info, $theme, $type); - - if (!empty($theme->info['base theme'])) { - $sub_themes[] = $key; - // Add the base theme as a proper dependency. - $themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme']; - } - - // Defaults to 'twig' (see $defaults above). - $engine = $theme->info['engine']; - if (isset($engines[$engine])) { - $theme->owner = $engines[$engine]->getExtensionPathname(); - $theme->prefix = $engines[$engine]->getName(); - $files_theme_engine[$engine] = $engines[$engine]->getPathname(); - } - - // Prefix screenshot with theme path. - if (!empty($theme->info['screenshot'])) { - $theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot']; - } - - $files_theme[$key] = $theme->getPathname(); - } - // Build dependencies. - // @todo Move into a generic ExtensionHandler base class. - // @see https://www.drupal.org/node/2208429 - $themes = $this->moduleHandler->buildModuleDependencies($themes); - - // Store filenames to allow system_list() and drupal_get_filename() to - // retrieve them for themes and theme engines without having to scan the - // filesystem. - $this->state->set('system.theme.files', $files_theme); - $this->state->set('system.theme_engine.files', $files_theme_engine); - - // After establishing the full list of available themes, fill in data for - // sub-themes. - foreach ($sub_themes as $key) { - $sub_theme = $themes[$key]; - // The $base_themes property is optional; only set for sub themes. - // @see ThemeHandlerInterface::listInfo() - $sub_theme->base_themes = $this->getBaseThemes($themes, $key); - // empty() cannot be used here, since ThemeHandler::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[$key] = $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; - } - } - - return $themes; + return $this->themeList->reset()->getList(); } /** * {@inheritdoc} */ 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, $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; - } - - /** - * Returns an extension discovery object. - * - * @return \Drupal\Core\Extension\ExtensionDiscovery - * The extension discovery object. - */ - protected function getExtensionDiscovery() { - if (!isset($this->extensionDiscovery)) { - $this->extensionDiscovery = new ExtensionDiscovery($this->root); - } - return $this->extensionDiscovery; + return $this->themeList->getBaseThemes($themes, $theme); } /** * {@inheritdoc} */ public function getName($theme) { - $themes = $this->listInfo(); - if (!isset($themes[$theme])) { - throw new UnknownExtensionException("Requested the name of a non-existing theme $theme"); - } - return $themes[$theme]->info['name']; - } - - /** - * Wraps system_list_reset(). - */ - protected function systemListReset() { - system_list_reset(); - } - - /** - * Wraps system_list(). - * - * @return array - * A list of themes keyed by name. - */ - protected function systemThemeList() { - return system_list('theme'); + return $this->themeList->getName($theme); } /** diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php index 56d9e9a8a86c4577d30900f343ac477d8b17d8c4..97f103ad704ac41d840dd7627cd6b6dc268ac305 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -56,8 +56,8 @@ public function uninstall(array $theme_list); * * @return \Drupal\Core\Extension\Extension[] * An associative array of the currently installed themes. The keys are the - * themes' machine names and the values are objects having the following - * properties: + * themes' machine names and the values are Extension objects having the + * following properties: * - filename: The filepath and name of the .info.yml file. * - name: The machine name of the theme. * - status: 1 for installed, 0 for uninstalled themes. diff --git a/core/lib/Drupal/Core/Extension/ThemeInstaller.php b/core/lib/Drupal/Core/Extension/ThemeInstaller.php index 2d6567fa188665cf411c92c73979aa9777a78d77..e1149e7b668d409af42cc321e9f8d8e3ffbb1275 100644 --- a/core/lib/Drupal/Core/Extension/ThemeInstaller.php +++ b/core/lib/Drupal/Core/Extension/ThemeInstaller.php @@ -174,22 +174,12 @@ public function install(array $theme_list, $install_dependencies = TRUE) { ->set("theme.$key", 0) ->save(TRUE); - // Add the theme to the current list. - // @todo Remove all code that relies on $status property. - $theme_data[$key]->status = 1; - $this->themeHandler->addTheme($theme_data[$key]); - - // Update the current theme data accordingly. - $current_theme_data = $this->state->get('system.theme.data', []); - $current_theme_data[$key] = $theme_data[$key]; - $this->state->set('system.theme.data', $current_theme_data); - // Reset theme settings. $theme_settings = &drupal_static('theme_get_setting'); unset($theme_settings[$key]); - // @todo Remove system_list(). - $this->systemListReset(); + // Reset theme listing. + $this->themeHandler->reset(); // Only install default configuration if this theme has not been installed // already. @@ -245,14 +235,10 @@ public function uninstall(array $theme_list) { } $this->cssCollectionOptimizer->deleteAll(); - $current_theme_data = $this->state->get('system.theme.data', []); foreach ($theme_list as $key) { // The value is not used; the weight is ignored for themes currently. $extension_config->clear("theme.$key"); - // Update the current theme data accordingly. - unset($current_theme_data[$key]); - // Reset theme settings. $theme_settings = &drupal_static('theme_get_setting'); unset($theme_settings[$key]); @@ -264,11 +250,10 @@ public function uninstall(array $theme_list) { // Don't check schema when uninstalling a theme since we are only clearing // keys. $extension_config->save(TRUE); - $this->state->set('system.theme.data', $current_theme_data); - // @todo Remove system_list(). - $this->themeHandler->refreshInfo(); + // Refresh theme info. $this->resetSystem(); + $this->themeHandler->reset(); $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]); } @@ -280,7 +265,6 @@ protected function resetSystem() { if ($this->routeBuilder) { $this->routeBuilder->setRebuildNeeded(); } - $this->systemListReset(); // @todo It feels wrong to have the requirement to clear the local tasks // cache here. @@ -295,11 +279,4 @@ protected function themeRegistryRebuild() { drupal_theme_rebuild(); } - /** - * Wraps system_list_reset(). - */ - protected function systemListReset() { - system_list_reset(); - } - } diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 5685f19d41989fd8f8b5da0dfae8e6bde16f2ce9..fe63381dc3609061960df0c35b6e5dcaf4338499 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -129,10 +129,9 @@ function hook_module_implements_alter(&$implementations, $hook) { /** * Alter the information parsed from module and theme .info.yml files. * - * This hook is invoked in _system_rebuild_module_data() and in - * \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). A module - * may implement this hook in order to add to or alter the data generated by - * reading the .info.yml file with \Drupal\Core\Extension\InfoParser. + * This hook is invoked in \Drupal\Core\Extension\ExtensionList::doList(). A + * module may implement this hook in order to add to or alter the data generated + * by reading the .info.yml file with \Drupal\Core\Extension\InfoParser. * * Using implementations of this hook to make modules required by setting the * $info['required'] key is discouraged. Doing so will slow down the module diff --git a/core/lib/Drupal/Core/Installer/ExtensionListTrait.php b/core/lib/Drupal/Core/Installer/ExtensionListTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..bbfae1fe09c6c4678c6cdd602560e49ea9818537 --- /dev/null +++ b/core/lib/Drupal/Core/Installer/ExtensionListTrait.php @@ -0,0 +1,56 @@ +<?php + +namespace Drupal\Core\Installer; + +/** + * Provides common functionality for the extension list classes. + */ +trait ExtensionListTrait { + + /** + * Static version of the added file names during the installer. + * + * @var string[] + * + * @internal + */ + protected static $staticAddedPathNames; + + /** + * @see \Drupal\Core\Extension\ExtensionList::setPathname() + */ + public function setPathname($extension_name, $pathname) { + parent::setPathname($extension_name, $pathname); + + // In the early installer the container is rebuilt multiple times. Therefore + // we have to keep the added filenames across those rebuilds. This is not a + // final design, but rather just a workaround resolved at some point, + // hopefully. + // @todo Remove as part of https://drupal.org/project/drupal/issues/2934063 + static::$staticAddedPathNames[$extension_name] = $pathname; + } + + /** + * @see \Drupal\Core\Extension\ExtensionList::getPathname() + */ + public function getPathname($extension_name) { + if (isset($this->addedPathNames[$extension_name])) { + return $this->addedPathNames[$extension_name]; + } + elseif (isset($this->pathNames[$extension_name])) { + return $this->pathNames[$extension_name]; + } + elseif (isset(static::$staticAddedPathNames[$extension_name])) { + return static::$staticAddedPathNames[$extension_name]; + } + elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) { + // Ensure we don't have to do path scanning more than really needed. + foreach ($path_names as $extension => $path_name) { + static::$staticAddedPathNames[$extension] = $path_name; + } + return $path_names[$extension_name]; + } + throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist."); + } + +} diff --git a/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php b/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php index 2ffac735312d2e8b3c1be806e731f5de72059ef2..a235423a4e1dcaf9b09f772a959a330e12099926 100644 --- a/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php +++ b/core/lib/Drupal/Core/Installer/InstallerModuleExtensionList.php @@ -8,51 +8,6 @@ * Overrides the module extension list to have a static cache. */ class InstallerModuleExtensionList extends ModuleExtensionList { - - /** - * Static version of the added file names during the installer. - * - * @var string[] - * - * @internal - */ - protected static $staticAddedPathNames; - - /** - * {@inheritdoc} - */ - public function setPathname($extension_name, $pathname) { - parent::setPathname($extension_name, $pathname); - - // In the early installer the container is rebuilt multiple times. Therefore - // we have to keep the added filenames across those rebuilds. This is not a - // final design, but rather just a workaround resolved at some point, - // hopefully. - // @todo Remove as part of https://drupal.org/project/drupal/issues/2934063 - static::$staticAddedPathNames[$extension_name] = $pathname; - } - - /** - * {@inheritdoc} - */ - public function getPathname($extension_name) { - if (isset($this->addedPathNames[$extension_name])) { - return $this->addedPathNames[$extension_name]; - } - elseif (isset($this->pathNames[$extension_name])) { - return $this->pathNames[$extension_name]; - } - elseif (isset(static::$staticAddedPathNames[$extension_name])) { - return static::$staticAddedPathNames[$extension_name]; - } - elseif (($path_names = $this->getPathnames()) && isset($path_names[$extension_name])) { - // Ensure we don't have to do path scanning more than really needed. - foreach ($path_names as $extension => $path_name) { - static::$staticAddedPathNames[$extension] = $path_name; - } - return $path_names[$extension_name]; - } - throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist."); - } + use ExtensionListTrait; } diff --git a/core/lib/Drupal/Core/Installer/InstallerThemeEngineExtensionList.php b/core/lib/Drupal/Core/Installer/InstallerThemeEngineExtensionList.php new file mode 100644 index 0000000000000000000000000000000000000000..ff775380b08c91319f3e1c860915f2a349a13b46 --- /dev/null +++ b/core/lib/Drupal/Core/Installer/InstallerThemeEngineExtensionList.php @@ -0,0 +1,13 @@ +<?php + +namespace Drupal\Core\Installer; + +use Drupal\Core\Extension\ThemeEngineExtensionList; + +/** + * Overrides the theme engine extension list to have a static cache. + */ +class InstallerThemeEngineExtensionList extends ThemeEngineExtensionList { + use ExtensionListTrait; + +} diff --git a/core/lib/Drupal/Core/Installer/InstallerThemeExtensionList.php b/core/lib/Drupal/Core/Installer/InstallerThemeExtensionList.php new file mode 100644 index 0000000000000000000000000000000000000000..46d99cde0a4be03d49a0e72a8a7b5247be70f2ee --- /dev/null +++ b/core/lib/Drupal/Core/Installer/InstallerThemeExtensionList.php @@ -0,0 +1,13 @@ +<?php + +namespace Drupal\Core\Installer; + +use Drupal\Core\Extension\ThemeExtensionList; + +/** + * Overrides the theme extension list to have a static cache. + */ +class InstallerThemeExtensionList extends ThemeExtensionList { + use ExtensionListTrait; + +} diff --git a/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php b/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php index c63b2d864f4f1b2716ee2565adb6fb0e482fb3f7..472d984b22efbc85e0acff32737281322ddf36fd 100644 --- a/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php +++ b/core/lib/Drupal/Core/Installer/NormalInstallerServiceProvider.php @@ -16,6 +16,8 @@ class NormalInstallerServiceProvider implements ServiceProviderInterface { public function register(ContainerBuilder $container) { // Use a performance optimised module extension list. $container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList'); + $container->getDefinition('extension.list.theme')->setClass('Drupal\Core\Installer\InstallerThemeExtensionList'); + $container->getDefinition('extension.list.theme_engine')->setClass('Drupal\Core\Installer\InstallerThemeEngineExtensionList'); } } diff --git a/core/lib/Drupal/Core/Theme/ActiveTheme.php b/core/lib/Drupal/Core/Theme/ActiveTheme.php index b7076fb6bdefc61dfbaa1530c6be24f92a50acf9..57c2165d89d5c90da944a50fda9537f3ccb1d988 100644 --- a/core/lib/Drupal/Core/Theme/ActiveTheme.php +++ b/core/lib/Drupal/Core/Theme/ActiveTheme.php @@ -161,7 +161,7 @@ public function getEngine() { /** * Returns the path to the theme engine for root themes. * - * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData + * @see \Drupal\Core\Extension\ThemeExtensionList::doList() * * @return mixed */ diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 4309c756ea8bbb0f06cf2219cc14467809f29ee9..e54797bb45f0d05466a817add0b19bed6ce4f38b 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1829,7 +1829,8 @@ function system_update_8014() { $theme_handler->refreshInfo(); foreach ($theme_handler->listInfo() as $theme) { // We first check that a base theme is set because if it's set to false then - // it's unset in \Drupal\Core\Extension\ThemeHandler::rebuildThemeData(). + // it's unset in + // \Drupal\Core\Extension\ThemeExtensionList::createExtensionInfo(). if (isset($theme->info['base theme']) && $theme->info['base theme'] == 'stable') { $theme_handler->install(['stable']); return; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c038de68ef1f897c5e3cf8dcda4b1b061b522c1c..13456c762dc36c225f93049b8aa5c6fda014c774 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -11,7 +11,6 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Cache\Cache; -use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Queue\QueueGarbageCollectionInterface; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Extension\Extension; @@ -963,38 +962,20 @@ function system_check_directory($form_element, FormStateInterface $form_state) { * array is returned. * * @see system_rebuild_module_data() - * @see \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData() + * @see \Drupal\Core\Extension\ThemeExtensionList */ function system_get_info($type, $name = NULL) { - if ($type == 'module') { - /** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */ - $module_list = \Drupal::service('extension.list.module'); - if (isset($name)) { - try { - return $module_list->getExtensionInfo($name); - } - catch (UnknownExtensionException $e) { - return []; - } + /** @var \Drupal\Core\Extension\ExtensionList $extension_list */ + $extension_list = \Drupal::service('extension.list.' . $type); + if (isset($name)) { + try { + return $extension_list->getExtensionInfo($name); } - else { - return $module_list->getAllInstalledInfo(); - } - } - else { - // @todo move into ThemeExtensionList https://www.drupal.org/node/2659940 - $info = []; - $list = system_list($type); - foreach ($list as $shortname => $item) { - if (!empty($item->status)) { - $info[$shortname] = $item->info; - } - } - if (isset($name)) { - return isset($info[$name]) ? $info[$name] : []; + catch (\InvalidArgumentException $e) { + return []; } - return $info; } + return $extension_list->getAllInstalledInfo(); } /** diff --git a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php index c9a939000a71637c60e277aa48fc43ffaee2f7bf..bd46aeaed9007a3d0af1091a0ee81d47cbc16bbd 100644 --- a/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php +++ b/core/modules/system/tests/modules/theme_test/src/ThemeTestController.php @@ -158,4 +158,14 @@ public function preprocessSuggestions() { ]; } + /** + * Controller for testing a namespaced class in a theme. + */ + public function testThemeClass() { + return [ + '#theme' => 'theme_test_theme_class', + '#title' => 'Testing loading a class from a .theme file', + ]; + } + } diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-theme-class.html.twig b/core/modules/system/tests/modules/theme_test/templates/theme-test-theme-class.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..810872f7c8db7911d74fbd4c20d4044b757c883c --- /dev/null +++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-theme-class.html.twig @@ -0,0 +1 @@ +<p>{{ message }}</p> diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index 701f033666dd1565b0f1fb4939eed180892de349..113676cbe9e5fe18f8a8e6d43f348f4daed141fc 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -70,6 +70,11 @@ function theme_test_theme($existing, $type, $theme, $path) { 'render element' => 'content', 'base hook' => 'container', ]; + $items['theme_test_theme_class'] = [ + 'variables' => [ + 'message' => '', + ], + ]; return $items; } diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml index 1ff61cf4c56852e50ab49f738f52f985dbdd9e23..6f880918f2fd8d72673441ddcd9190ff55edc719 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml +++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml @@ -110,3 +110,10 @@ theme_test.preprocess_suggestions: _controller: '\Drupal\theme_test\ThemeTestController::preprocessSuggestions' requirements: _access: 'TRUE' + +theme_test.test_theme_class: + path: '/theme-test/test-theme-class' + defaults: + _controller: '\Drupal\theme_test\ThemeTestController::testThemeClass' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/src/Functional/Theme/ThemeTest.php b/core/modules/system/tests/src/Functional/Theme/ThemeTest.php index b70ee859c65044e6bf32b010a4d245d06c55ed4c..545fc587aa541fbc0e5df97c6bc998fadacfeb32 100644 --- a/core/modules/system/tests/src/Functional/Theme/ThemeTest.php +++ b/core/modules/system/tests/src/Functional/Theme/ThemeTest.php @@ -80,6 +80,20 @@ public function testFrontPageThemeSuggestion() { $this->assertTrue(in_array('page__front', $suggestions), 'Front page template was suggested.'); } + /** + * Tests theme can provide classes. + */ + public function testClassLoading() { + // Install test theme and set it as default. + $this->config('system.theme') + ->set('default', 'test_theme') + ->save(); + $this->resetAll(); + // Visit page controller and confirm that the theme class is loaded. + $this->drupalGet('/theme-test/test-theme-class'); + $this->assertText('Loading ThemeClass was successful.'); + } + /** * Ensures a theme's .info.yml file is able to override a module CSS file from being added to the page. * diff --git a/core/modules/system/tests/src/Kernel/Theme/ThemeTest.php b/core/modules/system/tests/src/Kernel/Theme/ThemeTest.php index b9f09ec034bcfd087df0ed0c370eefe1c62fbfbc..fb54cd71e9b18451b573e5e2be7abed276c64e97 100644 --- a/core/modules/system/tests/src/Kernel/Theme/ThemeTest.php +++ b/core/modules/system/tests/src/Kernel/Theme/ThemeTest.php @@ -4,7 +4,6 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\Component\Render\MarkupInterface; -use Drupal\test_theme\ThemeClass; /** * Tests low-level theme functions. @@ -146,13 +145,6 @@ public function testDrupalRenderChildren() { $this->assertThemeOutput('theme_test_render_element_children', $element, 'Foo', 'drupal_render() avoids #theme_wrappers recursion loop when rendering a render element.'); } - /** - * Tests theme can provide classes. - */ - public function testClassLoading() { - new ThemeClass(); - } - /** * Tests drupal_find_theme_templates(). */ diff --git a/core/modules/system/tests/themes/test_theme/test_theme.theme b/core/modules/system/tests/themes/test_theme/test_theme.theme index 423390d2225c08a855e392951fdf314a4b52fce7..a5af83bc8774be1baa95be0e37e943ab20bb2f72 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.theme +++ b/core/modules/system/tests/themes/test_theme/test_theme.theme @@ -154,3 +154,15 @@ function test_theme_preprocess_theme_test_preprocess_suggestions__kitten__flamin function test_theme_preprocess_theme_test_preprocess_suggestions__kitten__meerkat__tarsier__moose(&$variables) { $variables['bar'] = 'Moose'; } + +/** + * Tests that a class can be loaded within a .theme file. + */ +function test_theme_preprocess_theme_test_theme_class(&$variables) { + if (class_exists('\Drupal\test_theme\ThemeClass')) { + $variables['message'] = 'Loading ThemeClass was successful.'; + } + else { + $variables['message'] = 'Loading ThemeClass failed.'; + } +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ThemeEngineExtensionListTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ThemeEngineExtensionListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3f59ff0e976adca3087a87b5ac9ee9de1a775474 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ThemeEngineExtensionListTest.php @@ -0,0 +1,29 @@ +<?php + +namespace Drupal\KernelTests\Core\Extension; + +use Drupal\Core\Site\Settings; +use Drupal\KernelTests\KernelTestBase; + +/** + * @coversDefaultClass \Drupal\Core\Extension\ThemeEngineExtensionList + * @group Extension + */ +class ThemeEngineExtensionListTest extends KernelTestBase { + + /** + * @covers ::getList + */ + public function testGetlist() { + $settings = Settings::getAll(); + $settings['install_profile'] = 'testing'; + new Settings($settings); + + // Confirm that all theme engines are available. + $theme_engines = \Drupal::service('extension.list.theme_engine')->getList(); + $this->assertArrayHasKey('twig', $theme_engines); + $this->assertArrayHasKey('nyan_cat', $theme_engines); + $this->assertCount(2, $theme_engines); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ThemeExtensionListTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ThemeExtensionListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..99e4cf7357667441c09b6d28ba93a75baf23f1ef --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/ThemeExtensionListTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\KernelTests\Core\Extension; + +use Drupal\Core\Site\Settings; +use Drupal\KernelTests\KernelTestBase; + +/** + * @coversDefaultClass \Drupal\Core\Extension\ThemeExtensionList + * @group Extension + */ +class ThemeExtensionListTest extends KernelTestBase { + + /** + * @covers ::getList + */ + public function testGetlist() { + $settings = Settings::getAll(); + $settings['install_profile'] = 'testing'; + new Settings($settings); + + \Drupal::configFactory()->getEditable('core.extension') + ->set('module.testing', 1000) + ->set('theme.test_theme', 0) + ->save(); + + // The installation profile is provided by a container parameter. + // Saving the configuration doesn't automatically trigger invalidation + $this->container->get('kernel')->rebuildContainer(); + + /** @var \Drupal\Core\Extension\ThemeExtensionList $theme_extension_list */ + $theme_extension_list = \Drupal::service('extension.list.theme'); + $extensions = $theme_extension_list->getList(); + + $this->assertArrayHasKey('test_theme', $extensions); + $this->assertEquals(0, $extensions['test_theme']->weight); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/SystemListTest.php b/core/tests/Drupal/KernelTests/Core/Theme/SystemListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..530d2ac9a5f0d13955ebb634e06fcc1e119322eb --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/SystemListTest.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\KernelTests\Core\Theme; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Routing\NullMatcherDumper; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests system_list() function. + * + * In Drupal 8 the system_list() function only lists themes. + * + * @group Extension + * @group legacy + */ +class SystemListTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['system']; + + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + // Some test methods involve ModuleHandler operations, which attempt to + // rebuild and dump routes. + $container->register('router.dumper', NullMatcherDumper::class); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->installConfig(['system']); + } + + /** + * Tests installing a theme. + * + * @expectedDeprecation 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 + */ + public function testSystemList() { + // Verifies that no themes are listed by default. + $this->assertEmpty(system_list('theme')); + $this->assertEmpty(system_list('filepaths')); + + $this->container->get('theme_installer')->install(['test_basetheme']); + + $themes = system_list('theme'); + $this->assertTrue(isset($themes['test_basetheme'])); + $this->assertEqual($themes['test_basetheme']->getName(), 'test_basetheme'); + $filepaths = system_list('filepaths'); + $this->assertEquals('test_basetheme', $filepaths[0]['name']); + $this->assertEquals('core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml', $filepaths[0]['filepath']); + $this->assertCount(1, $filepaths); + + $this->container->get('theme_installer')->uninstall(['test_basetheme']); + + $this->assertEmpty(system_list('theme')); + $this->assertEmpty(system_list('filepaths')); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php index dc606def6658bee3907bf3bb1da599f8bdc0c5e6..5c650d4f4684a0520d8fe5df16b852833a20f9f0 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php @@ -44,7 +44,7 @@ public function testEmpty() { $this->assertFalse($this->extensionConfig()->get('theme')); $this->assertFalse(array_keys($this->themeHandler()->listInfo())); - $this->assertFalse(array_keys(system_list('theme'))); + $this->assertFalse(array_keys(\Drupal::service('theme_handler')->listInfo())); // Rebuilding available themes should always yield results though. $this->assertTrue($this->themeHandler()->rebuildThemeData()['stark'], 'ThemeHandler::rebuildThemeData() yields all available themes.'); @@ -70,8 +70,6 @@ public function testInstall() { $this->assertTrue(isset($themes[$name])); $this->assertEqual($themes[$name]->getName(), $name); - $this->assertEqual(array_keys(system_list('theme')), array_keys($themes)); - // Verify that test_basetheme.settings is active. $this->assertIdentical(theme_get_setting('features.favicon', $name), FALSE); $this->assertEqual(theme_get_setting('base', $name), 'only'); @@ -272,7 +270,6 @@ public function testUninstall() { $this->themeInstaller()->uninstall([$name]); $this->assertFalse(array_keys($this->themeHandler()->listInfo())); - $this->assertFalse(array_keys(system_list('theme'))); $this->assertFalse($this->config("$name.settings")->get()); @@ -281,7 +278,6 @@ public function testUninstall() { $themes = $this->themeHandler()->listInfo(); $this->assertTrue(isset($themes[$name])); $this->assertEqual($themes[$name]->getName(), $name); - $this->assertEqual(array_keys(system_list('theme')), array_keys($themes)); $this->assertTrue($this->config("$name.settings")->get()); } @@ -331,8 +327,8 @@ public function testThemeInfoAlter() { $this->assertTrue(isset($info['regions']['test_region'])); $regions = system_region_list($name); $this->assertTrue(isset($regions['test_region'])); - $system_list = system_list('theme'); - $this->assertTrue(isset($system_list[$name]->info['regions']['test_region'])); + $theme_list = \Drupal::service('theme_handler')->listInfo(); + $this->assertTrue(isset($theme_list[$name]->info['regions']['test_region'])); $this->moduleInstaller()->uninstall(['module_test']); $this->assertFalse($this->moduleHandler()->moduleExists('module_test')); @@ -347,8 +343,8 @@ public function testThemeInfoAlter() { $this->assertFalse(isset($info['regions']['test_region'])); $regions = system_region_list($name); $this->assertFalse(isset($regions['test_region'])); - $system_list = system_list('theme'); - $this->assertFalse(isset($system_list[$name]->info['regions']['test_region'])); + $theme_list = \Drupal::service('theme_handler')->listInfo(); + $this->assertFalse(isset($theme_list[$name]->info['regions']['test_region'])); } /** diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9c5fd58fadc04f5e65790967890c1fd500053123 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeExtensionListTest.php @@ -0,0 +1,264 @@ +<?php + +namespace Drupal\Tests\Core\Extension; + +use Drupal\Core\Cache\MemoryBackend; +use Drupal\Core\Cache\NullBackend; +use Drupal\Core\Extension\Extension; +use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\InfoParser; +use Drupal\Core\Extension\InfoParserInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeEngineExtensionList; +use Drupal\Core\Extension\ThemeExtensionList; +use Drupal\Core\KeyValueStore\KeyValueMemoryFactory; +use Drupal\Core\Lock\NullLockBackend; +use Drupal\Core\State\State; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; + +/** + * @coversDefaultClass \Drupal\Core\Extension\ThemeExtensionList + * @group Extension + */ +class ThemeExtensionListTest extends UnitTestCase { + + /** + * Tests rebuild the theme data with theme parents. + */ + public function testRebuildThemeDataWithThemeParents() { + $extension_discovery = $this->prophesize(ExtensionDiscovery::class); + $extension_discovery + ->scan('theme') + ->willReturn([ + 'test_subtheme' => new Extension($this->root, 'theme', $this->root . '/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml', 'test_subtheme.info.yml'), + 'test_basetheme' => new Extension($this->root, 'theme', $this->root . '/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml', 'test_basetheme.info.yml'), + ]); + $extension_discovery + ->scan('theme_engine') + ->willReturn([ + 'twig' => new Extension($this->root, 'theme_engine', $this->root . '/core/themes/engines/twig/twig.info.yml', 'twig.engine'), + ]); + + // Verify that info parser is called with the specified paths. + $argument_condition = function ($path) { + return in_array($path, [ + $this->root . '/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml', + $this->root . '/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml', + $this->root . '/core/themes/engines/twig/twig.info.yml', + ], TRUE); + }; + $info_parser = $this->prophesize(InfoParserInterface::class); + $info_parser->parse(Argument::that($argument_condition)) + ->shouldBeCalled() + ->will(function ($file) { + $info_parser = new InfoParser(); + return $info_parser->parse($file[0]); + }); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $module_handler + ->buildModuleDependencies(Argument::type('array')) + ->willReturnArgument(0); + $module_handler + ->alter('system_info', Argument::type('array'), Argument::type(Extension::class), Argument::any()) + ->shouldBeCalled(); + + $state = new State(new KeyValueMemoryFactory(), new MemoryBackend(), new NullLockBackend()); + + $config_factory = $this->getConfigFactoryStub([ + 'core.extension' => [ + 'module' => [], + 'theme' => [], + 'disabled' => [ + 'theme' => [], + ], + 'theme_engine' => '', + ], + ]); + + $theme_engine_list = new TestThemeEngineExtensionList($this->root, 'theme_engine', new NullBackend('test'), $info_parser->reveal(), $module_handler->reveal(), $state, $config_factory, 'testing'); + $theme_engine_list->setExtensionDiscovery($extension_discovery->reveal()); + + $theme_list = new TestThemeExtensionList($this->root, 'theme', new NullBackend('test'), $info_parser->reveal(), $module_handler->reveal(), $state, $config_factory, $theme_engine_list, 'testing'); + $theme_list->setExtensionDiscovery($extension_discovery->reveal()); + + $theme_data = $theme_list->reset()->getList(); + $this->assertCount(2, $theme_data); + + $info_basetheme = $theme_data['test_basetheme']; + $info_subtheme = $theme_data['test_subtheme']; + + // Ensure some basic properties. + $this->assertInstanceOf('Drupal\Core\Extension\Extension', $info_basetheme); + $this->assertEquals('test_basetheme', $info_basetheme->getName()); + $this->assertInstanceOf('Drupal\Core\Extension\Extension', $info_subtheme); + $this->assertEquals('test_subtheme', $info_subtheme->getName()); + + // Test the parent/child-theme properties. + $info_subtheme->info['base theme'] = 'test_basetheme'; + $info_basetheme->sub_themes = ['test_subtheme']; + + $this->assertEquals($this->root . '/core/themes/engines/twig/twig.engine', $info_basetheme->owner); + $this->assertEquals('twig', $info_basetheme->prefix); + $this->assertEquals($this->root . '/core/themes/engines/twig/twig.engine', $info_subtheme->owner); + $this->assertEquals('twig', $info_subtheme->prefix); + } + + /** + * Tests getting the base themes for a set a defines themes. + * + * @param array $themes + * An array of available themes, keyed by the theme name. + * @param string $theme + * The theme name to find all its base themes. + * @param array $expected + * The expected base themes. + * + * @dataProvider providerTestGetBaseThemes + */ + public function testGetBaseThemes(array $themes, $theme, array $expected) { + // Mocks and stubs. + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $state = new State(new KeyValueMemoryFactory(), new MemoryBackend(), new NullLockBackend()); + $config_factory = $this->getConfigFactoryStub([]); + $theme_engine_list = $this->prophesize(ThemeEngineExtensionList::class); + $theme_listing = new ThemeExtensionList($this->root, 'theme', new NullBackend('test'), new InfoParser(), $module_handler->reveal(), $state, $config_factory, $theme_engine_list->reveal(), 'test'); + + $base_themes = $theme_listing->getBaseThemes($themes, $theme); + + $this->assertEquals($expected, $base_themes); + } + + /** + * Provides test data for testGetBaseThemes. + * + * @return array + * An array of theme test data. + */ + public function providerTestGetBaseThemes() { + $data = []; + + // Tests a theme without any base theme. + $themes = []; + $themes['test_1'] = (object) [ + 'name' => 'test_1', + 'info' => [ + 'name' => 'test_1', + ], + ]; + $data[] = [$themes, 'test_1', []]; + + // Tests a theme with a non existing base theme. + $themes = []; + $themes['test_1'] = (object) [ + 'name' => 'test_1', + 'info' => [ + 'name' => 'test_1', + 'base theme' => 'test_2', + ], + ]; + $data[] = [$themes, 'test_1', ['test_2' => NULL]]; + + // Tests a theme with a single existing base theme. + $themes = []; + $themes['test_1'] = (object) [ + 'name' => 'test_1', + 'info' => [ + 'name' => 'test_1', + 'base theme' => 'test_2', + ], + ]; + $themes['test_2'] = (object) [ + 'name' => 'test_2', + 'info' => [ + 'name' => 'test_2', + ], + ]; + $data[] = [$themes, 'test_1', ['test_2' => 'test_2']]; + + // Tests a theme with multiple base themes. + $themes = []; + $themes['test_1'] = (object) [ + 'name' => 'test_1', + 'info' => [ + 'name' => 'test_1', + 'base theme' => 'test_2', + ], + ]; + $themes['test_2'] = (object) [ + 'name' => 'test_2', + 'info' => [ + 'name' => 'test_2', + 'base theme' => 'test_3', + ], + ]; + $themes['test_3'] = (object) [ + 'name' => 'test_3', + 'info' => [ + 'name' => 'test_3', + ], + ]; + $data[] = [ + $themes, + 'test_1', + ['test_2' => 'test_2', 'test_3' => 'test_3'], + ]; + + return $data; + } + +} + +/** + * Trait that allows extension discovery to be set. + */ +trait SettableDiscoveryExtensionListTrait { + + /** + * The extension discovery for this extension list. + * + * @var \Drupal\Core\Extension\ExtensionDiscovery + */ + protected $extensionDiscovery; + + /** + * Sets the extension discovery. + * + * @param \Drupal\Core\Extension\ExtensionDiscovery $discovery + * The extension discovery. + */ + public function setExtensionDiscovery(ExtensionDiscovery $discovery) { + $this->extensionDiscovery = $discovery; + } + + /** + * {@inheritdoc} + */ + public function getExtensionDiscovery() { + return $this->extensionDiscovery; + } + +} + +/** + * Test theme extension list class. + */ +class TestThemeExtensionList extends ThemeExtensionList { + + use SettableDiscoveryExtensionListTrait; + +} + +/** + * Test theme engine extension list class. + */ +class TestThemeEngineExtensionList extends ThemeEngineExtensionList { + + use SettableDiscoveryExtensionListTrait; + +} + +if (!defined('DRUPAL_MINIMUM_PHP')) { + define('DRUPAL_MINIMUM_PHP', '5.5.9'); +} diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php index f22419f985c046df770f794f1d4a4f683743ae36..97a5e937e9f8a821585fe67f2965ca80a454feac 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php @@ -7,11 +7,10 @@ namespace Drupal\Tests\Core\Extension; +use Composer\Autoload\ClassLoader; use Drupal\Core\Extension\Extension; -use Drupal\Core\Extension\InfoParser; +use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Core\Extension\ThemeHandler; -use Drupal\Core\KeyValueStore\KeyValueMemoryFactory; -use Drupal\Core\State\State; use Drupal\Tests\UnitTestCase; /** @@ -20,20 +19,6 @@ */ class ThemeHandlerTest extends UnitTestCase { - /** - * The mocked info parser. - * - * @var \Drupal\Core\Extension\InfoParserInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $infoParser; - - /** - * The mocked state backend. - * - * @var \Drupal\Core\State\StateInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $state; - /** * The mocked config factory. * @@ -42,18 +27,11 @@ class ThemeHandlerTest extends UnitTestCase { protected $configFactory; /** - * The mocked module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $moduleHandler; - - /** - * The extension discovery. + * The theme listing service. * - * @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Extension\ThemeExtensionList|\PHPUnit_Framework_MockObject_MockObject */ - protected $extensionDiscovery; + protected $themeList; /** * The tested theme handler. @@ -77,16 +55,17 @@ protected function setUp() { ], ], ]); - $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - $this->state = new State(new KeyValueMemoryFactory()); - $this->infoParser = $this->getMock('Drupal\Core\Extension\InfoParserInterface'); - $this->extensionDiscovery = $this->getMockBuilder('Drupal\Core\Extension\ExtensionDiscovery') + $this->themeList = $this->getMockBuilder(ThemeExtensionList::class) ->disableOriginalConstructor() ->getMock(); - $this->themeHandler = new StubThemeHandler($this->root, $this->configFactory, $this->moduleHandler, $this->state, $this->infoParser, $this->extensionDiscovery); - - $cache_tags_invalidator = $this->getMock('Drupal\Core\Cache\CacheTagsInvalidatorInterface'); - $this->getContainerWithCacheTagsInvalidator($cache_tags_invalidator); + $this->themeHandler = new StubThemeHandler($this->root, $this->configFactory, $this->themeList); + + $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container->expects($this->any()) + ->method('get') + ->with('class_loader') + ->will($this->returnValue($this->createMock(ClassLoader::class))); + \Drupal::setContainer($container); } /** @@ -95,31 +74,14 @@ protected function setUp() { * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData() */ public function testRebuildThemeData() { - $this->extensionDiscovery->expects($this->at(0)) - ->method('scan') - ->with('theme') + $this->themeList->expects($this->at(0)) + ->method('reset') + ->willReturnSelf(); + $this->themeList->expects($this->at(1)) + ->method('getList') ->will($this->returnValue([ 'seven' => new Extension($this->root, 'theme', $this->root . '/core/themes/seven/seven.info.yml', 'seven.theme'), ])); - $this->extensionDiscovery->expects($this->at(1)) - ->method('scan') - ->with('theme_engine') - ->will($this->returnValue([ - 'twig' => new Extension($this->root, 'theme_engine', $this->root . '/core/themes/engines/twig/twig.info.yml', 'twig.engine'), - ])); - $this->infoParser->expects($this->once()) - ->method('parse') - ->with($this->root . '/core/themes/seven/seven.info.yml') - ->will($this->returnCallback(function ($file) { - $info_parser = new InfoParser(); - return $info_parser->parse($file); - })); - $this->moduleHandler->expects($this->once()) - ->method('buildModuleDependencies') - ->will($this->returnArgument(0)); - - $this->moduleHandler->expects($this->once()) - ->method('alter'); $theme_data = $this->themeHandler->rebuildThemeData(); $this->assertCount(1, $theme_data); @@ -130,11 +92,7 @@ public function testRebuildThemeData() { $this->assertEquals('seven', $info->getName()); $this->assertEquals($this->root . '/core/themes/seven/seven.info.yml', $info->getPathname()); $this->assertEquals($this->root . '/core/themes/seven/seven.theme', $info->getExtensionPathname()); - $this->assertEquals($this->root . '/core/themes/engines/twig/twig.engine', $info->owner); - $this->assertEquals('twig', $info->prefix); - $this->assertEquals('twig', $info->info['engine']); - $this->assertEquals(['seven/global-styling'], $info->info['libraries']); } /** @@ -151,158 +109,6 @@ public function testThemeLibrariesEmpty() { } } - /** - * Tests rebuild the theme data with theme parents. - */ - public function testRebuildThemeDataWithThemeParents() { - $this->extensionDiscovery->expects($this->at(0)) - ->method('scan') - ->with('theme') - ->will($this->returnValue([ - 'test_subtheme' => new Extension($this->root, 'theme', $this->root . '/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml', 'test_subtheme.info.yml'), - 'test_basetheme' => new Extension($this->root, 'theme', $this->root . '/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml', 'test_basetheme.info.yml'), - ])); - $this->extensionDiscovery->expects($this->at(1)) - ->method('scan') - ->with('theme_engine') - ->will($this->returnValue([ - 'twig' => new Extension($this->root, 'theme_engine', $this->root . '/core/themes/engines/twig/twig.info.yml', 'twig.engine'), - ])); - $this->infoParser->expects($this->at(0)) - ->method('parse') - ->with($this->root . '/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml') - ->will($this->returnCallback(function ($file) { - $info_parser = new InfoParser(); - return $info_parser->parse($file); - })); - $this->infoParser->expects($this->at(1)) - ->method('parse') - ->with($this->root . '/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml') - ->will($this->returnCallback(function ($file) { - $info_parser = new InfoParser(); - return $info_parser->parse($file); - })); - $this->moduleHandler->expects($this->once()) - ->method('buildModuleDependencies') - ->will($this->returnArgument(0)); - - $theme_data = $this->themeHandler->rebuildThemeData(); - $this->assertCount(2, $theme_data); - - $info_basetheme = $theme_data['test_basetheme']; - $info_subtheme = $theme_data['test_subtheme']; - - // Ensure some basic properties. - $this->assertInstanceOf('Drupal\Core\Extension\Extension', $info_basetheme); - $this->assertEquals('test_basetheme', $info_basetheme->getName()); - $this->assertInstanceOf('Drupal\Core\Extension\Extension', $info_subtheme); - $this->assertEquals('test_subtheme', $info_subtheme->getName()); - - // Test the parent/child-theme properties. - $info_subtheme->info['base theme'] = 'test_basetheme'; - $info_basetheme->sub_themes = ['test_subtheme']; - - $this->assertEquals($this->root . '/core/themes/engines/twig/twig.engine', $info_basetheme->owner); - $this->assertEquals('twig', $info_basetheme->prefix); - $this->assertEquals($this->root . '/core/themes/engines/twig/twig.engine', $info_subtheme->owner); - $this->assertEquals('twig', $info_subtheme->prefix); - } - - /** - * Tests getting the base themes for a set a defines themes. - * - * @param array $themes - * An array of available themes, keyed by the theme name. - * @param string $theme - * The theme name to find all its base themes. - * @param array $expected - * The expected base themes. - * - * @dataProvider providerTestGetBaseThemes - */ - public function testGetBaseThemes(array $themes, $theme, array $expected) { - $base_themes = $this->themeHandler->getBaseThemes($themes, $theme); - $this->assertEquals($expected, $base_themes); - } - - /** - * Provides test data for testGetBaseThemes. - * - * @return array - * An array of theme test data. - */ - public function providerTestGetBaseThemes() { - $data = []; - - // Tests a theme without any base theme. - $themes = []; - $themes['test_1'] = (object) [ - 'name' => 'test_1', - 'info' => [ - 'name' => 'test_1', - ], - ]; - $data[] = [$themes, 'test_1', []]; - - // Tests a theme with a non existing base theme. - $themes = []; - $themes['test_1'] = (object) [ - 'name' => 'test_1', - 'info' => [ - 'name' => 'test_1', - 'base theme' => 'test_2', - ], - ]; - $data[] = [$themes, 'test_1', ['test_2' => NULL]]; - - // Tests a theme with a single existing base theme. - $themes = []; - $themes['test_1'] = (object) [ - 'name' => 'test_1', - 'info' => [ - 'name' => 'test_1', - 'base theme' => 'test_2', - ], - ]; - $themes['test_2'] = (object) [ - 'name' => 'test_2', - 'info' => [ - 'name' => 'test_2', - ], - ]; - $data[] = [$themes, 'test_1', ['test_2' => 'test_2']]; - - // Tests a theme with multiple base themes. - $themes = []; - $themes['test_1'] = (object) [ - 'name' => 'test_1', - 'info' => [ - 'name' => 'test_1', - 'base theme' => 'test_2', - ], - ]; - $themes['test_2'] = (object) [ - 'name' => 'test_2', - 'info' => [ - 'name' => 'test_2', - 'base theme' => 'test_3', - ], - ]; - $themes['test_3'] = (object) [ - 'name' => 'test_3', - 'info' => [ - 'name' => 'test_3', - ], - ]; - $data[] = [ - $themes, - 'test_1', - ['test_2' => 'test_2', 'test_3' => 'test_3'], - ]; - - return $data; - } - } /** @@ -324,13 +130,6 @@ class StubThemeHandler extends ThemeHandler { */ protected $registryRebuild; - /** - * A list of themes keyed by name. - * - * @var array - */ - protected $systemList; - /** * {@inheritdoc} */ @@ -345,27 +144,8 @@ protected function themeRegistryRebuild() { $this->registryRebuild = TRUE; } - /** - * {@inheritdoc} - */ - protected function systemThemeList() { - return $this->systemList; - } - - /** - * {@inheritdoc} - */ - protected function systemListReset() { - } - } -if (!defined('DRUPAL_EXTENSION_NAME_MAX_LENGTH')) { - define('DRUPAL_EXTENSION_NAME_MAX_LENGTH', 50); -} -if (!defined('DRUPAL_PHP_FUNCTION_PATTERN')) { - define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'); -} if (!defined('DRUPAL_MINIMUM_PHP')) { - define('DRUPAL_MINIMUM_PHP', '5.3.10'); + define('DRUPAL_MINIMUM_PHP', '5.5.9'); }