3 namespace Drupal\Core\Extension;
5 use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\Config\ConfigInstallerInterface;
9 use Drupal\Core\Config\ConfigManagerInterface;
10 use Drupal\Core\Extension\Exception\UnknownExtensionException;
11 use Drupal\Core\Routing\RouteBuilderInterface;
12 use Drupal\Core\State\StateInterface;
13 use Psr\Log\LoggerInterface;
16 * Manages theme installation/uninstallation.
18 class ThemeInstaller implements ThemeInstallerInterface {
21 * @var \Drupal\Core\Extension\ThemeHandlerInterface
23 protected $themeHandler;
26 * @var \Drupal\Core\Config\ConfigFactoryInterface
28 protected $configFactory;
31 * @var \Drupal\Core\Config\ConfigInstallerInterface
33 protected $configInstaller;
36 * @var \Drupal\Core\Extension\ModuleHandlerInterface
38 protected $moduleHandler;
41 * @var \Drupal\Core\State\StateInterface
46 * @var \Drupal\Core\Config\ConfigManagerInterface
48 protected $configManager;
51 * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
53 protected $cssCollectionOptimizer;
56 * @var \Drupal\Core\Routing\RouteBuilderInterface
58 protected $routeBuilder;
61 * @var \Psr\Log\LoggerInterface
66 * Constructs a new ThemeInstaller.
68 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
70 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
71 * The config factory to get the installed themes.
72 * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer
73 * (optional) The config installer to install configuration. This optional
74 * to allow the theme handler to work before Drupal is installed and has a
76 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
77 * The module handler to fire themes_installed/themes_uninstalled hooks.
78 * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
79 * The config manager used to uninstall a theme.
80 * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
81 * The CSS asset collection optimizer service.
82 * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
83 * (optional) The route builder service to rebuild the routes if a theme is
85 * @param \Psr\Log\LoggerInterface $logger
87 * @param \Drupal\Core\State\StateInterface $state
90 public function __construct(ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory, ConfigInstallerInterface $config_installer, ModuleHandlerInterface $module_handler, ConfigManagerInterface $config_manager, AssetCollectionOptimizerInterface $css_collection_optimizer, RouteBuilderInterface $route_builder, LoggerInterface $logger, StateInterface $state) {
91 $this->themeHandler = $theme_handler;
92 $this->configFactory = $config_factory;
93 $this->configInstaller = $config_installer;
94 $this->moduleHandler = $module_handler;
95 $this->configManager = $config_manager;
96 $this->cssCollectionOptimizer = $css_collection_optimizer;
97 $this->routeBuilder = $route_builder;
98 $this->logger = $logger;
99 $this->state = $state;
105 public function install(array $theme_list, $install_dependencies = TRUE) {
106 $extension_config = $this->configFactory->getEditable('core.extension');
108 $theme_data = $this->themeHandler->rebuildThemeData();
110 if ($install_dependencies) {
111 $theme_list = array_combine($theme_list, $theme_list);
113 if ($missing = array_diff_key($theme_list, $theme_data)) {
114 // One or more of the given themes doesn't exist.
115 throw new UnknownExtensionException('Unknown themes: ' . implode(', ', $missing) . '.');
118 // Only process themes that are not installed currently.
119 $installed_themes = $extension_config->get('theme') ?: [];
120 if (!$theme_list = array_diff_key($theme_list, $installed_themes)) {
121 // Nothing to do. All themes already installed.
125 foreach ($theme_list as $theme => $value) {
126 // Add dependencies to the list. The new themes will be processed as
127 // the parent foreach loop continues.
128 foreach (array_keys($theme_data[$theme]->requires) as $dependency) {
129 if (!isset($theme_data[$dependency])) {
130 // The dependency does not exist.
134 // Skip already installed themes.
135 if (!isset($theme_list[$dependency]) && !isset($installed_themes[$dependency])) {
136 $theme_list[$dependency] = $dependency;
141 // Set the actual theme weights.
142 $theme_list = array_map(function ($theme) use ($theme_data) {
143 return $theme_data[$theme]->sort;
146 // Sort the theme list by their weights (reverse).
148 $theme_list = array_keys($theme_list);
151 $installed_themes = $extension_config->get('theme') ?: [];
154 $themes_installed = [];
155 foreach ($theme_list as $key) {
156 // Only process themes that are not already installed.
157 $installed = $extension_config->get("theme.$key") !== NULL;
162 // Throw an exception if the theme name is too long.
163 if (strlen($key) > DRUPAL_EXTENSION_NAME_MAX_LENGTH) {
164 throw new ExtensionNameLengthException("Theme name $key is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters.');
167 // Validate default configuration of the theme. If there is existing
168 // configuration then stop installing.
169 $this->configInstaller->checkConfigurationToInstall('theme', $key);
171 // The value is not used; the weight is ignored for themes currently. Do
172 // not check schema when saving the configuration.
174 ->set("theme.$key", 0)
177 // Add the theme to the current list.
178 // @todo Remove all code that relies on $status property.
179 $theme_data[$key]->status = 1;
180 $this->themeHandler->addTheme($theme_data[$key]);
182 // Update the current theme data accordingly.
183 $current_theme_data = $this->state->get('system.theme.data', []);
184 $current_theme_data[$key] = $theme_data[$key];
185 $this->state->set('system.theme.data', $current_theme_data);
187 // Reset theme settings.
188 $theme_settings = &drupal_static('theme_get_setting');
189 unset($theme_settings[$key]);
191 // @todo Remove system_list().
192 $this->systemListReset();
194 // Only install default configuration if this theme has not been installed
196 if (!isset($installed_themes[$key])) {
197 // Install default configuration of the theme.
198 $this->configInstaller->installDefaultConfig('theme', $key);
201 $themes_installed[] = $key;
203 // Record the fact that it was installed.
204 $this->logger->info('%theme theme installed.', ['%theme' => $key]);
207 $this->cssCollectionOptimizer->deleteAll();
208 $this->resetSystem();
210 // Invoke hook_themes_installed() after the themes have been installed.
211 $this->moduleHandler->invokeAll('themes_installed', [$themes_installed]);
213 return !empty($themes_installed);
219 public function uninstall(array $theme_list) {
220 $extension_config = $this->configFactory->getEditable('core.extension');
221 $theme_config = $this->configFactory->getEditable('system.theme');
222 $list = $this->themeHandler->listInfo();
223 foreach ($theme_list as $key) {
224 if (!isset($list[$key])) {
225 throw new UnknownExtensionException("Unknown theme: $key.");
227 if ($key === $theme_config->get('default')) {
228 throw new \InvalidArgumentException("The current default theme $key cannot be uninstalled.");
230 if ($key === $theme_config->get('admin')) {
231 throw new \InvalidArgumentException("The current administration theme $key cannot be uninstalled.");
233 // Base themes cannot be uninstalled if sub themes are installed, and if
234 // they are not uninstalled at the same time.
235 // @todo https://www.drupal.org/node/474684 and
236 // https://www.drupal.org/node/1297856 themes should leverage the module
237 // dependency system.
238 if (!empty($list[$key]->sub_themes)) {
239 foreach ($list[$key]->sub_themes as $sub_key => $sub_label) {
240 if (isset($list[$sub_key]) && !in_array($sub_key, $theme_list, TRUE)) {
241 throw new \InvalidArgumentException("The base theme $key cannot be uninstalled, because theme $sub_key depends on it.");
247 $this->cssCollectionOptimizer->deleteAll();
248 $current_theme_data = $this->state->get('system.theme.data', []);
249 foreach ($theme_list as $key) {
250 // The value is not used; the weight is ignored for themes currently.
251 $extension_config->clear("theme.$key");
253 // Update the current theme data accordingly.
254 unset($current_theme_data[$key]);
256 // Reset theme settings.
257 $theme_settings = &drupal_static('theme_get_setting');
258 unset($theme_settings[$key]);
260 // Remove all configuration belonging to the theme.
261 $this->configManager->uninstall('theme', $key);
264 // Don't check schema when uninstalling a theme since we are only clearing
266 $extension_config->save(TRUE);
267 $this->state->set('system.theme.data', $current_theme_data);
269 // @todo Remove system_list().
270 $this->themeHandler->refreshInfo();
271 $this->resetSystem();
273 $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]);
277 * Resets some other systems like rebuilding the route information or caches.
279 protected function resetSystem() {
280 if ($this->routeBuilder) {
281 $this->routeBuilder->setRebuildNeeded();
283 $this->systemListReset();
285 // @todo It feels wrong to have the requirement to clear the local tasks
287 Cache::invalidateTags(['local_task']);
288 $this->themeRegistryRebuild();
292 * Wraps drupal_theme_rebuild().
294 protected function themeRegistryRebuild() {
295 drupal_theme_rebuild();
299 * Wraps system_list_reset().
301 protected function systemListReset() {