Issue #2659940 by almaudoh, alexpott, phenaproxima, Jo Fitzgerald, markcarver, oriol_e9g, dawehner, dsnopek, jibran, larowlan: Extension System, Part III: ThemeExtensionList and ThemeEngineExtensionList

8.7.x
Nathaniel Catchpole 2018-11-29 12:15:17 +00:00
parent bab4f6d3f3
commit 10722cd872
36 changed files with 1015 additions and 751 deletions

View File

@ -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:
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']

View File

@ -10,12 +10,12 @@ use Drupal\Component\Utility\Html;
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) {
* 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) {
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);
} }
/** /**

View File

@ -14,41 +14,31 @@ use Drupal\Core\Extension\ExtensionDiscovery;
* 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;
}
else {
$lists = [
'theme' => [],
'filepaths' => [],
];
// 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']);
}
$lists = [
'theme' => \Drupal::service('theme_handler')->listInfo(),
'filepaths' => [],
];
foreach ($lists['theme'] as $name => $theme) {
$lists['filepaths'][] = [
'type' => 'theme',
'name' => $name,
'filepath' => $theme->getPathname(),
];
}
return $lists[$type]; return $lists[$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();
} }
/** /**

View File

@ -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 [

View File

@ -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]);
} }

View File

@ -169,8 +169,8 @@ class Extension implements \Serializable {
'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);

View File

@ -11,6 +11,12 @@ use Drupal\Core\State\StateInterface;
* 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 @@ abstract class ExtensionList {
// 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 @@ abstract class ExtensionList {
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;
}
} }

View File

@ -9,6 +9,12 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
/** /**
* 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 {

View File

@ -4,6 +4,12 @@ namespace Drupal\Core\Extension;
/** /**
* 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 {

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -5,28 +5,12 @@ namespace Drupal\Core\Extension;
use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\Exception\UninstalledExtensionException; use Drupal\Core\Extension\Exception\UninstalledExtensionException;
use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\State\StateInterface;
/** /**
* Default theme handler using the config system to store installation statuses. * Default theme handler using the config system to store installation statuses.
*/ */
class ThemeHandler implements ThemeHandlerInterface { 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. * A list of all currently available themes.
* *
@ -41,68 +25,12 @@ class ThemeHandler implements ThemeHandlerInterface {
*/ */
protected $configFactory; 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. * An extension discovery instance.
* *
* @var \Drupal\Core\Extension\ExtensionDiscovery * @var \Drupal\Core\Extension\ThemeExtensionList
*/ */
protected $extensionDiscovery; protected $themeList;
/**
* 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
*/
protected $configManager;
/** /**
* The app root. * The app root.
@ -118,22 +46,13 @@ class ThemeHandler implements ThemeHandlerInterface {
* The app root. * The app root.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory to get the installed themes. * The config factory to get the installed themes.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list
* The module handler to fire themes_installed/themes_uninstalled hooks. * A extension discovery instance.
* @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).
*/ */
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->root = $root;
$this->configFactory = $config_factory; $this->configFactory = $config_factory;
$this->moduleHandler = $module_handler; $this->themeList = $theme_list;
$this->state = $state;
$this->infoParser = $info_parser;
$this->extensionDiscovery = $extension_discovery;
} }
/** /**
@ -181,16 +100,10 @@ class ThemeHandler implements ThemeHandlerInterface {
public function listInfo() { public function listInfo() {
if (!isset($this->list)) { if (!isset($this->list)) {
$this->list = []; $this->list = [];
$themes = $this->systemThemeList(); $installed_themes = $this->configFactory->get('core.extension')->get('theme');
// @todo Ensure that systemThemeList() does not contain an empty list if (!empty($installed_themes)) {
// during the batch installer, see https://www.drupal.org/node/2322619. $installed_themes = array_intersect_key($this->themeList->getList(), $installed_themes);
if (empty($themes)) { array_map([$this, 'addTheme'], $installed_themes);
$this->refreshInfo();
$this->list = $this->list ?: [];
$themes = \Drupal::state()->get('system.theme.data', []);
}
foreach ($themes as $theme) {
$this->addTheme($theme);
} }
} }
return $this->list; return $this->list;
@ -200,6 +113,11 @@ class ThemeHandler implements ThemeHandlerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function addTheme(Extension $theme) { 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'])) { if (!empty($theme->info['libraries'])) {
foreach ($theme->info['libraries'] as $library => $name) { foreach ($theme->info['libraries'] as $library => $name) {
$theme->libraries[$library] = $name; $theme->libraries[$library] = $name;
@ -218,32 +136,21 @@ class ThemeHandler implements ThemeHandlerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function refreshInfo() { public function refreshInfo() {
$extension_config = $this->configFactory->get('core.extension'); $installed = $this->configFactory->get('core.extension')->get('theme');
$installed = $extension_config->get('theme');
// Only refresh the info if a theme has been installed. Modules are // Only refresh the info if a theme has been installed. Modules are
// installed before themes by the installer and this method is called during // installed before themes by the installer and this method is called during
// module installation. // module installation.
if (empty($installed) && empty($this->list)) { if (empty($installed) && empty($this->list)) {
return; return;
} }
$this->reset(); $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} * {@inheritdoc}
*/ */
public function reset() { public function reset() {
$this->systemListReset(); $this->themeList->reset();
$this->list = NULL; $this->list = NULL;
} }
@ -251,214 +158,21 @@ class ThemeHandler implements ThemeHandlerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function rebuildThemeData() { public function rebuildThemeData() {
$listing = $this->getExtensionDiscovery(); return $this->themeList->reset()->getList();
$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;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getBaseThemes(array $themes, $theme) { public function getBaseThemes(array $themes, $theme) {
return $this->doGetBaseThemes($themes, $theme); return $this->themeList->getBaseThemes($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;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getName($theme) { public function getName($theme) {
$themes = $this->listInfo(); return $this->themeList->getName($theme);
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');
} }
/** /**

View File

@ -56,8 +56,8 @@ interface ThemeHandlerInterface {
* *
* @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:
* - filename: The filepath and name of the .info.yml file. * - filename: The filepath and name of the .info.yml file.
* - name: The machine name of the theme. * - name: The machine name of the theme.
* - status: 1 for installed, 0 for uninstalled themes. * - status: 1 for installed, 0 for uninstalled themes.

View File

@ -174,22 +174,12 @@ class ThemeInstaller implements ThemeInstallerInterface {
->set("theme.$key", 0) ->set("theme.$key", 0)
->save(TRUE); ->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. // Reset theme settings.
$theme_settings = &drupal_static('theme_get_setting'); $theme_settings = &drupal_static('theme_get_setting');
unset($theme_settings[$key]); unset($theme_settings[$key]);
// @todo Remove system_list(). // Reset theme listing.
$this->systemListReset(); $this->themeHandler->reset();
// Only install default configuration if this theme has not been installed // Only install default configuration if this theme has not been installed
// already. // already.
@ -245,14 +235,10 @@ class ThemeInstaller implements ThemeInstallerInterface {
} }
$this->cssCollectionOptimizer->deleteAll(); $this->cssCollectionOptimizer->deleteAll();
$current_theme_data = $this->state->get('system.theme.data', []);
foreach ($theme_list as $key) { foreach ($theme_list as $key) {
// The value is not used; the weight is ignored for themes currently. // The value is not used; the weight is ignored for themes currently.
$extension_config->clear("theme.$key"); $extension_config->clear("theme.$key");
// Update the current theme data accordingly.
unset($current_theme_data[$key]);
// Reset theme settings. // Reset theme settings.
$theme_settings = &drupal_static('theme_get_setting'); $theme_settings = &drupal_static('theme_get_setting');
unset($theme_settings[$key]); unset($theme_settings[$key]);
@ -264,11 +250,10 @@ class ThemeInstaller implements ThemeInstallerInterface {
// Don't check schema when uninstalling a theme since we are only clearing // Don't check schema when uninstalling a theme since we are only clearing
// keys. // keys.
$extension_config->save(TRUE); $extension_config->save(TRUE);
$this->state->set('system.theme.data', $current_theme_data);
// @todo Remove system_list(). // Refresh theme info.
$this->themeHandler->refreshInfo();
$this->resetSystem(); $this->resetSystem();
$this->themeHandler->reset();
$this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]); $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
} }
@ -280,7 +265,6 @@ class ThemeInstaller implements ThemeInstallerInterface {
if ($this->routeBuilder) { if ($this->routeBuilder) {
$this->routeBuilder->setRebuildNeeded(); $this->routeBuilder->setRebuildNeeded();
} }
$this->systemListReset();
// @todo It feels wrong to have the requirement to clear the local tasks // @todo It feels wrong to have the requirement to clear the local tasks
// cache here. // cache here.
@ -295,11 +279,4 @@ class ThemeInstaller implements ThemeInstallerInterface {
drupal_theme_rebuild(); drupal_theme_rebuild();
} }
/**
* Wraps system_list_reset().
*/
protected function systemListReset() {
system_list_reset();
}
} }

View File

@ -129,10 +129,9 @@ function hook_module_implements_alter(&$implementations, $hook) {
/** /**
* Alter the information parsed from module and theme .info.yml files. * Alter the information parsed from module and theme .info.yml files.
* *
* This hook is invoked in _system_rebuild_module_data() and in * This hook is invoked in \Drupal\Core\Extension\ExtensionList::doList(). A
* \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). A module * module may implement this hook in order to add to or alter the data generated
* may implement this hook in order to add to or alter the data generated by * by reading the .info.yml file with \Drupal\Core\Extension\InfoParser.
* reading the .info.yml file with \Drupal\Core\Extension\InfoParser.
* *
* Using implementations of this hook to make modules required by setting the * Using implementations of this hook to make modules required by setting the
* $info['required'] key is discouraged. Doing so will slow down the module * $info['required'] key is discouraged. Doing so will slow down the module

View File

@ -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.");
}
}

View File

@ -8,51 +8,6 @@ use Drupal\Core\Extension\ModuleExtensionList;
* Overrides the module extension list to have a static cache. * Overrides the module extension list to have a static cache.
*/ */
class InstallerModuleExtensionList extends ModuleExtensionList { class InstallerModuleExtensionList extends ModuleExtensionList {
use ExtensionListTrait;
/**
* 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.");
}
} }

View File

@ -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;
}

View File

@ -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;
}

View File

@ -16,6 +16,8 @@ class NormalInstallerServiceProvider implements ServiceProviderInterface {
public function register(ContainerBuilder $container) { public function register(ContainerBuilder $container) {
// Use a performance optimised module extension list. // Use a performance optimised module extension list.
$container->getDefinition('extension.list.module')->setClass('Drupal\Core\Installer\InstallerModuleExtensionList'); $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');
} }
} }

View File

@ -161,7 +161,7 @@ class ActiveTheme {
/** /**
* Returns the path to the theme engine for root themes. * Returns the path to the theme engine for root themes.
* *
* @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData * @see \Drupal\Core\Extension\ThemeExtensionList::doList()
* *
* @return mixed * @return mixed
*/ */

View File

@ -1829,7 +1829,8 @@ function system_update_8014() {
$theme_handler->refreshInfo(); $theme_handler->refreshInfo();
foreach ($theme_handler->listInfo() as $theme) { foreach ($theme_handler->listInfo() as $theme) {
// We first check that a base theme is set because if it's set to false then // 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') { if (isset($theme->info['base theme']) && $theme->info['base theme'] == 'stable') {
$theme_handler->install(['stable']); $theme_handler->install(['stable']);
return; return;

View File

@ -11,7 +11,6 @@ use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Queue\QueueGarbageCollectionInterface; use Drupal\Core\Queue\QueueGarbageCollectionInterface;
use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\Extension;
@ -963,38 +962,20 @@ function system_check_directory($form_element, FormStateInterface $form_state) {
* array is returned. * array is returned.
* *
* @see system_rebuild_module_data() * @see system_rebuild_module_data()
* @see \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData() * @see \Drupal\Core\Extension\ThemeExtensionList
*/ */
function system_get_info($type, $name = NULL) { function system_get_info($type, $name = NULL) {
if ($type == 'module') { /** @var \Drupal\Core\Extension\ExtensionList $extension_list */
/** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */ $extension_list = \Drupal::service('extension.list.' . $type);
$module_list = \Drupal::service('extension.list.module'); if (isset($name)) {
if (isset($name)) { try {
try { return $extension_list->getExtensionInfo($name);
return $module_list->getExtensionInfo($name);
}
catch (UnknownExtensionException $e) {
return [];
}
} }
else { catch (\InvalidArgumentException $e) {
return $module_list->getAllInstalledInfo(); return [];
} }
} }
else { return $extension_list->getAllInstalledInfo();
// @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] : [];
}
return $info;
}
} }
/** /**

View File

@ -158,4 +158,14 @@ class ThemeTestController extends ControllerBase {
]; ];
} }
/**
* 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',
];
}
} }

View File

@ -0,0 +1 @@
<p>{{ message }}</p>

View File

@ -70,6 +70,11 @@ function theme_test_theme($existing, $type, $theme, $path) {
'render element' => 'content', 'render element' => 'content',
'base hook' => 'container', 'base hook' => 'container',
]; ];
$items['theme_test_theme_class'] = [
'variables' => [
'message' => '',
],
];
return $items; return $items;
} }

View File

@ -110,3 +110,10 @@ theme_test.preprocess_suggestions:
_controller: '\Drupal\theme_test\ThemeTestController::preprocessSuggestions' _controller: '\Drupal\theme_test\ThemeTestController::preprocessSuggestions'
requirements: requirements:
_access: 'TRUE' _access: 'TRUE'
theme_test.test_theme_class:
path: '/theme-test/test-theme-class'
defaults:
_controller: '\Drupal\theme_test\ThemeTestController::testThemeClass'
requirements:
_access: 'TRUE'

View File

@ -80,6 +80,20 @@ class ThemeTest extends BrowserTestBase {
$this->assertTrue(in_array('page__front', $suggestions), 'Front page template was suggested.'); $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. * Ensures a theme's .info.yml file is able to override a module CSS file from being added to the page.
* *

View File

@ -4,7 +4,6 @@ namespace Drupal\Tests\system\Kernel\Theme;
use Drupal\KernelTests\KernelTestBase; use Drupal\KernelTests\KernelTestBase;
use Drupal\Component\Render\MarkupInterface; use Drupal\Component\Render\MarkupInterface;
use Drupal\test_theme\ThemeClass;
/** /**
* Tests low-level theme functions. * Tests low-level theme functions.
@ -146,13 +145,6 @@ class ThemeTest extends KernelTestBase {
$this->assertThemeOutput('theme_test_render_element_children', $element, 'Foo', 'drupal_render() avoids #theme_wrappers recursion loop when rendering a render element.'); $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(). * Tests drupal_find_theme_templates().
*/ */

View File

@ -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) { function test_theme_preprocess_theme_test_preprocess_suggestions__kitten__meerkat__tarsier__moose(&$variables) {
$variables['bar'] = 'Moose'; $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.';
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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'));
}
}

View File

@ -44,7 +44,7 @@ class ThemeInstallerTest extends KernelTestBase {
$this->assertFalse($this->extensionConfig()->get('theme')); $this->assertFalse($this->extensionConfig()->get('theme'));
$this->assertFalse(array_keys($this->themeHandler()->listInfo())); $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. // Rebuilding available themes should always yield results though.
$this->assertTrue($this->themeHandler()->rebuildThemeData()['stark'], 'ThemeHandler::rebuildThemeData() yields all available themes.'); $this->assertTrue($this->themeHandler()->rebuildThemeData()['stark'], 'ThemeHandler::rebuildThemeData() yields all available themes.');
@ -70,8 +70,6 @@ class ThemeInstallerTest extends KernelTestBase {
$this->assertTrue(isset($themes[$name])); $this->assertTrue(isset($themes[$name]));
$this->assertEqual($themes[$name]->getName(), $name); $this->assertEqual($themes[$name]->getName(), $name);
$this->assertEqual(array_keys(system_list('theme')), array_keys($themes));
// Verify that test_basetheme.settings is active. // Verify that test_basetheme.settings is active.
$this->assertIdentical(theme_get_setting('features.favicon', $name), FALSE); $this->assertIdentical(theme_get_setting('features.favicon', $name), FALSE);
$this->assertEqual(theme_get_setting('base', $name), 'only'); $this->assertEqual(theme_get_setting('base', $name), 'only');
@ -272,7 +270,6 @@ class ThemeInstallerTest extends KernelTestBase {
$this->themeInstaller()->uninstall([$name]); $this->themeInstaller()->uninstall([$name]);
$this->assertFalse(array_keys($this->themeHandler()->listInfo())); $this->assertFalse(array_keys($this->themeHandler()->listInfo()));
$this->assertFalse(array_keys(system_list('theme')));
$this->assertFalse($this->config("$name.settings")->get()); $this->assertFalse($this->config("$name.settings")->get());
@ -281,7 +278,6 @@ class ThemeInstallerTest extends KernelTestBase {
$themes = $this->themeHandler()->listInfo(); $themes = $this->themeHandler()->listInfo();
$this->assertTrue(isset($themes[$name])); $this->assertTrue(isset($themes[$name]));
$this->assertEqual($themes[$name]->getName(), $name); $this->assertEqual($themes[$name]->getName(), $name);
$this->assertEqual(array_keys(system_list('theme')), array_keys($themes));
$this->assertTrue($this->config("$name.settings")->get()); $this->assertTrue($this->config("$name.settings")->get());
} }
@ -331,8 +327,8 @@ class ThemeInstallerTest extends KernelTestBase {
$this->assertTrue(isset($info['regions']['test_region'])); $this->assertTrue(isset($info['regions']['test_region']));
$regions = system_region_list($name); $regions = system_region_list($name);
$this->assertTrue(isset($regions['test_region'])); $this->assertTrue(isset($regions['test_region']));
$system_list = system_list('theme'); $theme_list = \Drupal::service('theme_handler')->listInfo();
$this->assertTrue(isset($system_list[$name]->info['regions']['test_region'])); $this->assertTrue(isset($theme_list[$name]->info['regions']['test_region']));
$this->moduleInstaller()->uninstall(['module_test']); $this->moduleInstaller()->uninstall(['module_test']);
$this->assertFalse($this->moduleHandler()->moduleExists('module_test')); $this->assertFalse($this->moduleHandler()->moduleExists('module_test'));
@ -347,8 +343,8 @@ class ThemeInstallerTest extends KernelTestBase {
$this->assertFalse(isset($info['regions']['test_region'])); $this->assertFalse(isset($info['regions']['test_region']));
$regions = system_region_list($name); $regions = system_region_list($name);
$this->assertFalse(isset($regions['test_region'])); $this->assertFalse(isset($regions['test_region']));
$system_list = system_list('theme'); $theme_list = \Drupal::service('theme_handler')->listInfo();
$this->assertFalse(isset($system_list[$name]->info['regions']['test_region'])); $this->assertFalse(isset($theme_list[$name]->info['regions']['test_region']));
} }
/** /**

View File

@ -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');
}

View File

@ -7,11 +7,10 @@
namespace Drupal\Tests\Core\Extension; namespace Drupal\Tests\Core\Extension;
use Composer\Autoload\ClassLoader;
use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\InfoParser; use Drupal\Core\Extension\ThemeExtensionList;
use Drupal\Core\Extension\ThemeHandler; use Drupal\Core\Extension\ThemeHandler;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\State\State;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
/** /**
@ -20,20 +19,6 @@ use Drupal\Tests\UnitTestCase;
*/ */
class ThemeHandlerTest extends UnitTestCase { 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. * The mocked config factory.
* *
@ -42,18 +27,11 @@ class ThemeHandlerTest extends UnitTestCase {
protected $configFactory; protected $configFactory;
/** /**
* The mocked module handler. * The theme listing service.
* *
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject * @var \Drupal\Core\Extension\ThemeExtensionList|\PHPUnit_Framework_MockObject_MockObject
*/ */
protected $moduleHandler; protected $themeList;
/**
* The extension discovery.
*
* @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject
*/
protected $extensionDiscovery;
/** /**
* The tested theme handler. * The tested theme handler.
@ -77,16 +55,17 @@ class ThemeHandlerTest extends UnitTestCase {
], ],
], ],
]); ]);
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); $this->themeList = $this->getMockBuilder(ThemeExtensionList::class)
$this->state = new State(new KeyValueMemoryFactory());
$this->infoParser = $this->getMock('Drupal\Core\Extension\InfoParserInterface');
$this->extensionDiscovery = $this->getMockBuilder('Drupal\Core\Extension\ExtensionDiscovery')
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->themeHandler = new StubThemeHandler($this->root, $this->configFactory, $this->moduleHandler, $this->state, $this->infoParser, $this->extensionDiscovery); $this->themeHandler = new StubThemeHandler($this->root, $this->configFactory, $this->themeList);
$cache_tags_invalidator = $this->getMock('Drupal\Core\Cache\CacheTagsInvalidatorInterface'); $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface');
$this->getContainerWithCacheTagsInvalidator($cache_tags_invalidator); $container->expects($this->any())
->method('get')
->with('class_loader')
->will($this->returnValue($this->createMock(ClassLoader::class)));
\Drupal::setContainer($container);
} }
/** /**
@ -95,31 +74,14 @@ class ThemeHandlerTest extends UnitTestCase {
* @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData() * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData()
*/ */
public function testRebuildThemeData() { public function testRebuildThemeData() {
$this->extensionDiscovery->expects($this->at(0)) $this->themeList->expects($this->at(0))
->method('scan') ->method('reset')
->with('theme') ->willReturnSelf();
$this->themeList->expects($this->at(1))
->method('getList')
->will($this->returnValue([ ->will($this->returnValue([
'seven' => new Extension($this->root, 'theme', $this->root . '/core/themes/seven/seven.info.yml', 'seven.theme'), '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(); $theme_data = $this->themeHandler->rebuildThemeData();
$this->assertCount(1, $theme_data); $this->assertCount(1, $theme_data);
@ -130,11 +92,7 @@ class ThemeHandlerTest extends UnitTestCase {
$this->assertEquals('seven', $info->getName()); $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.info.yml', $info->getPathname());
$this->assertEquals($this->root . '/core/themes/seven/seven.theme', $info->getExtensionPathname()); $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 @@ class ThemeHandlerTest extends UnitTestCase {
} }
} }
/**
* 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; protected $registryRebuild;
/**
* A list of themes keyed by name.
*
* @var array
*/
protected $systemList;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -345,27 +144,8 @@ class StubThemeHandler extends ThemeHandler {
$this->registryRebuild = TRUE; $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')) { if (!defined('DRUPAL_MINIMUM_PHP')) {
define('DRUPAL_MINIMUM_PHP', '5.3.10'); define('DRUPAL_MINIMUM_PHP', '5.5.9');
} }