comparison core/lib/Drupal/Core/Extension/ThemeHandler.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents a9cd425dd02b
children
comparison
equal deleted inserted replaced
4:a9cd425dd02b 5:12f9dff5fda9
3 namespace Drupal\Core\Extension; 3 namespace Drupal\Core\Extension;
4 4
5 use Drupal\Core\Config\ConfigFactoryInterface; 5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Extension\Exception\UninstalledExtensionException; 6 use Drupal\Core\Extension\Exception\UninstalledExtensionException;
7 use Drupal\Core\Extension\Exception\UnknownExtensionException; 7 use Drupal\Core\Extension\Exception\UnknownExtensionException;
8 use Drupal\Core\State\StateInterface;
9 8
10 /** 9 /**
11 * Default theme handler using the config system to store installation statuses. 10 * Default theme handler using the config system to store installation statuses.
12 */ 11 */
13 class ThemeHandler implements ThemeHandlerInterface { 12 class ThemeHandler implements ThemeHandlerInterface {
14 13
15 /** 14 /**
16 * Contains the features enabled for themes by default. 15 * A list of all currently available themes.
17 * 16 *
18 * @var array 17 * @var array
19 *
20 * @see _system_default_theme_features()
21 */
22 protected $defaultFeatures = [
23 'favicon',
24 'logo',
25 'node_user_picture',
26 'comment_user_picture',
27 'comment_user_verification',
28 ];
29
30 /**
31 * A list of all currently available themes.
32 *
33 * @var array
34 */ 18 */
35 protected $list; 19 protected $list;
36 20
37 /** 21 /**
38 * The config factory to get the installed themes. 22 * The config factory to get the installed themes.
40 * @var \Drupal\Core\Config\ConfigFactoryInterface 24 * @var \Drupal\Core\Config\ConfigFactoryInterface
41 */ 25 */
42 protected $configFactory; 26 protected $configFactory;
43 27
44 /** 28 /**
45 * The module handler to fire themes_installed/themes_uninstalled hooks.
46 *
47 * @var \Drupal\Core\Extension\ModuleHandlerInterface
48 */
49 protected $moduleHandler;
50
51 /**
52 * The state backend.
53 *
54 * @var \Drupal\Core\State\StateInterface
55 */
56 protected $state;
57
58 /**
59 * The config installer to install configuration.
60 *
61 * @var \Drupal\Core\Config\ConfigInstallerInterface
62 */
63 protected $configInstaller;
64
65 /**
66 * The info parser to parse the theme.info.yml files.
67 *
68 * @var \Drupal\Core\Extension\InfoParserInterface
69 */
70 protected $infoParser;
71
72 /**
73 * A logger instance.
74 *
75 * @var \Psr\Log\LoggerInterface
76 */
77 protected $logger;
78
79 /**
80 * The route builder to rebuild the routes if a theme is installed.
81 *
82 * @var \Drupal\Core\Routing\RouteBuilderInterface
83 */
84 protected $routeBuilder;
85
86 /**
87 * An extension discovery instance. 29 * An extension discovery instance.
88 * 30 *
89 * @var \Drupal\Core\Extension\ExtensionDiscovery 31 * @var \Drupal\Core\Extension\ThemeExtensionList
90 */ 32 */
91 protected $extensionDiscovery; 33 protected $themeList;
92
93 /**
94 * The CSS asset collection optimizer service.
95 *
96 * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface
97 */
98 protected $cssCollectionOptimizer;
99
100 /**
101 * The config manager used to uninstall a theme.
102 *
103 * @var \Drupal\Core\Config\ConfigManagerInterface
104 */
105 protected $configManager;
106 34
107 /** 35 /**
108 * The app root. 36 * The app root.
109 * 37 *
110 * @var string 38 * @var string
116 * 44 *
117 * @param string $root 45 * @param string $root
118 * The app root. 46 * The app root.
119 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory 47 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
120 * The config factory to get the installed themes. 48 * The config factory to get the installed themes.
121 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler 49 * @param \Drupal\Core\Extension\ThemeExtensionList $theme_list
122 * The module handler to fire themes_installed/themes_uninstalled hooks. 50 * A extension discovery instance.
123 * @param \Drupal\Core\State\StateInterface $state 51 */
124 * The state store. 52 public function __construct($root, ConfigFactoryInterface $config_factory, ThemeExtensionList $theme_list) {
125 * @param \Drupal\Core\Extension\InfoParserInterface $info_parser
126 * The info parser to parse the theme.info.yml files.
127 * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery
128 * (optional) A extension discovery instance (for unit tests).
129 */
130 public function __construct($root, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser, ExtensionDiscovery $extension_discovery = NULL) {
131 $this->root = $root; 53 $this->root = $root;
132 $this->configFactory = $config_factory; 54 $this->configFactory = $config_factory;
133 $this->moduleHandler = $module_handler; 55 $this->themeList = $theme_list;
134 $this->state = $state;
135 $this->infoParser = $info_parser;
136 $this->extensionDiscovery = $extension_discovery;
137 } 56 }
138 57
139 /** 58 /**
140 * {@inheritdoc} 59 * {@inheritdoc}
141 */ 60 */
179 * {@inheritdoc} 98 * {@inheritdoc}
180 */ 99 */
181 public function listInfo() { 100 public function listInfo() {
182 if (!isset($this->list)) { 101 if (!isset($this->list)) {
183 $this->list = []; 102 $this->list = [];
184 $themes = $this->systemThemeList(); 103 $installed_themes = $this->configFactory->get('core.extension')->get('theme');
185 // @todo Ensure that systemThemeList() does not contain an empty list 104 if (!empty($installed_themes)) {
186 // during the batch installer, see https://www.drupal.org/node/2322619. 105 $installed_themes = array_intersect_key($this->themeList->getList(), $installed_themes);
187 if (empty($themes)) { 106 array_map([$this, 'addTheme'], $installed_themes);
188 $this->refreshInfo();
189 $this->list = $this->list ?: [];
190 $themes = \Drupal::state()->get('system.theme.data', []);
191 } 107 }
192 foreach ($themes as $theme) {
193 $this->addTheme($theme);
194 }
195 } 108 }
196 return $this->list; 109 return $this->list;
197 } 110 }
198 111
199 /** 112 /**
200 * {@inheritdoc} 113 * {@inheritdoc}
201 */ 114 */
202 public function addTheme(Extension $theme) { 115 public function addTheme(Extension $theme) {
116 // Register the namespaces of installed themes.
117 // @todo Implement proper theme registration
118 // https://www.drupal.org/project/drupal/issues/2941757
119 \Drupal::service('class_loader')->addPsr4('Drupal\\' . $theme->getName() . '\\', $this->root . '/' . $theme->getPath() . '/src');
120
203 if (!empty($theme->info['libraries'])) { 121 if (!empty($theme->info['libraries'])) {
204 foreach ($theme->info['libraries'] as $library => $name) { 122 foreach ($theme->info['libraries'] as $library => $name) {
205 $theme->libraries[$library] = $name; 123 $theme->libraries[$library] = $name;
206 } 124 }
207 } 125 }
216 134
217 /** 135 /**
218 * {@inheritdoc} 136 * {@inheritdoc}
219 */ 137 */
220 public function refreshInfo() { 138 public function refreshInfo() {
221 $extension_config = $this->configFactory->get('core.extension'); 139 $installed = $this->configFactory->get('core.extension')->get('theme');
222 $installed = $extension_config->get('theme');
223 // Only refresh the info if a theme has been installed. Modules are 140 // Only refresh the info if a theme has been installed. Modules are
224 // installed before themes by the installer and this method is called during 141 // installed before themes by the installer and this method is called during
225 // module installation. 142 // module installation.
226 if (empty($installed) && empty($this->list)) { 143 if (empty($installed) && empty($this->list)) {
227 return; 144 return;
228 } 145 }
229
230 $this->reset(); 146 $this->reset();
231 // @todo Avoid re-scanning all themes by retaining the original (unaltered)
232 // theme info somewhere.
233 $list = $this->rebuildThemeData();
234 foreach ($list as $name => $theme) {
235 if (isset($installed[$name])) {
236 $this->addTheme($theme);
237 }
238 }
239 $this->state->set('system.theme.data', $this->list);
240 } 147 }
241 148
242 /** 149 /**
243 * {@inheritdoc} 150 * {@inheritdoc}
244 */ 151 */
245 public function reset() { 152 public function reset() {
246 $this->systemListReset(); 153 $this->themeList->reset();
247 $this->list = NULL; 154 $this->list = NULL;
248 } 155 }
249 156
250 /** 157 /**
251 * {@inheritdoc} 158 * {@inheritdoc}
252 */ 159 */
253 public function rebuildThemeData() { 160 public function rebuildThemeData() {
254 $listing = $this->getExtensionDiscovery(); 161 return $this->themeList->reset()->getList();
255 $themes = $listing->scan('theme');
256 $engines = $listing->scan('theme_engine');
257 $extension_config = $this->configFactory->get('core.extension');
258 $installed = $extension_config->get('theme') ?: [];
259
260 // Set defaults for theme info.
261 $defaults = [
262 'engine' => 'twig',
263 'base theme' => 'stable',
264 'regions' => [
265 'sidebar_first' => 'Left sidebar',
266 'sidebar_second' => 'Right sidebar',
267 'content' => 'Content',
268 'header' => 'Header',
269 'primary_menu' => 'Primary menu',
270 'secondary_menu' => 'Secondary menu',
271 'footer' => 'Footer',
272 'highlighted' => 'Highlighted',
273 'help' => 'Help',
274 'page_top' => 'Page top',
275 'page_bottom' => 'Page bottom',
276 'breadcrumb' => 'Breadcrumb',
277 ],
278 'description' => '',
279 'features' => $this->defaultFeatures,
280 'screenshot' => 'screenshot.png',
281 'php' => DRUPAL_MINIMUM_PHP,
282 'libraries' => [],
283 ];
284
285 $sub_themes = [];
286 $files_theme = [];
287 $files_theme_engine = [];
288 // Read info files for each theme.
289 foreach ($themes as $key => $theme) {
290 // @todo Remove all code that relies on the $status property.
291 $theme->status = (int) isset($installed[$key]);
292
293 $theme->info = $this->infoParser->parse($theme->getPathname()) + $defaults;
294 // Remove the default Stable base theme when 'base theme: false' is set in
295 // a theme .info.yml file.
296 if ($theme->info['base theme'] === FALSE) {
297 unset($theme->info['base theme']);
298 }
299
300 // Add the info file modification time, so it becomes available for
301 // contributed modules to use for ordering theme lists.
302 $theme->info['mtime'] = $theme->getMTime();
303
304 // Invoke hook_system_info_alter() to give installed modules a chance to
305 // modify the data in the .info.yml files if necessary.
306 // @todo Remove $type argument, obsolete with $theme->getType().
307 $type = 'theme';
308 $this->moduleHandler->alter('system_info', $theme->info, $theme, $type);
309
310 if (!empty($theme->info['base theme'])) {
311 $sub_themes[] = $key;
312 // Add the base theme as a proper dependency.
313 $themes[$key]->info['dependencies'][] = $themes[$key]->info['base theme'];
314 }
315
316 // Defaults to 'twig' (see $defaults above).
317 $engine = $theme->info['engine'];
318 if (isset($engines[$engine])) {
319 $theme->owner = $engines[$engine]->getExtensionPathname();
320 $theme->prefix = $engines[$engine]->getName();
321 $files_theme_engine[$engine] = $engines[$engine]->getPathname();
322 }
323
324 // Prefix screenshot with theme path.
325 if (!empty($theme->info['screenshot'])) {
326 $theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot'];
327 }
328
329 $files_theme[$key] = $theme->getPathname();
330 }
331 // Build dependencies.
332 // @todo Move into a generic ExtensionHandler base class.
333 // @see https://www.drupal.org/node/2208429
334 $themes = $this->moduleHandler->buildModuleDependencies($themes);
335
336 // Store filenames to allow system_list() and drupal_get_filename() to
337 // retrieve them for themes and theme engines without having to scan the
338 // filesystem.
339 $this->state->set('system.theme.files', $files_theme);
340 $this->state->set('system.theme_engine.files', $files_theme_engine);
341
342 // After establishing the full list of available themes, fill in data for
343 // sub-themes.
344 foreach ($sub_themes as $key) {
345 $sub_theme = $themes[$key];
346 // The $base_themes property is optional; only set for sub themes.
347 // @see ThemeHandlerInterface::listInfo()
348 $sub_theme->base_themes = $this->getBaseThemes($themes, $key);
349 // empty() cannot be used here, since ThemeHandler::doGetBaseThemes() adds
350 // the key of a base theme with a value of NULL in case it is not found,
351 // in order to prevent needless iterations.
352 if (!current($sub_theme->base_themes)) {
353 continue;
354 }
355 // Determine the root base theme.
356 $root_key = key($sub_theme->base_themes);
357 // Build the list of sub-themes for each of the theme's base themes.
358 foreach (array_keys($sub_theme->base_themes) as $base_theme) {
359 $themes[$base_theme]->sub_themes[$key] = $sub_theme->info['name'];
360 }
361 // Add the theme engine info from the root base theme.
362 if (isset($themes[$root_key]->owner)) {
363 $sub_theme->info['engine'] = $themes[$root_key]->info['engine'];
364 $sub_theme->owner = $themes[$root_key]->owner;
365 $sub_theme->prefix = $themes[$root_key]->prefix;
366 }
367 }
368
369 return $themes;
370 } 162 }
371 163
372 /** 164 /**
373 * {@inheritdoc} 165 * {@inheritdoc}
374 */ 166 */
375 public function getBaseThemes(array $themes, $theme) { 167 public function getBaseThemes(array $themes, $theme) {
376 return $this->doGetBaseThemes($themes, $theme); 168 return $this->themeList->getBaseThemes($themes, $theme);
377 }
378
379 /**
380 * Finds the base themes for the specific theme.
381 *
382 * @param array $themes
383 * An array of available themes.
384 * @param string $theme
385 * The name of the theme whose base we are looking for.
386 * @param array $used_themes
387 * (optional) A recursion parameter preventing endless loops. Defaults to
388 * an empty array.
389 *
390 * @return array
391 * An array of base themes.
392 */
393 protected function doGetBaseThemes(array $themes, $theme, $used_themes = []) {
394 if (!isset($themes[$theme]->info['base theme'])) {
395 return [];
396 }
397
398 $base_key = $themes[$theme]->info['base theme'];
399 // Does the base theme exist?
400 if (!isset($themes[$base_key])) {
401 return [$base_key => NULL];
402 }
403
404 $current_base_theme = [$base_key => $themes[$base_key]->info['name']];
405
406 // Is the base theme itself a child of another theme?
407 if (isset($themes[$base_key]->info['base theme'])) {
408 // Do we already know the base themes of this theme?
409 if (isset($themes[$base_key]->base_themes)) {
410 return $themes[$base_key]->base_themes + $current_base_theme;
411 }
412 // Prevent loops.
413 if (!empty($used_themes[$base_key])) {
414 return [$base_key => NULL];
415 }
416 $used_themes[$base_key] = TRUE;
417 return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
418 }
419 // If we get here, then this is our parent theme.
420 return $current_base_theme;
421 }
422
423 /**
424 * Returns an extension discovery object.
425 *
426 * @return \Drupal\Core\Extension\ExtensionDiscovery
427 * The extension discovery object.
428 */
429 protected function getExtensionDiscovery() {
430 if (!isset($this->extensionDiscovery)) {
431 $this->extensionDiscovery = new ExtensionDiscovery($this->root);
432 }
433 return $this->extensionDiscovery;
434 } 169 }
435 170
436 /** 171 /**
437 * {@inheritdoc} 172 * {@inheritdoc}
438 */ 173 */
439 public function getName($theme) { 174 public function getName($theme) {
440 $themes = $this->listInfo(); 175 return $this->themeList->getName($theme);
441 if (!isset($themes[$theme])) {
442 throw new UnknownExtensionException("Requested the name of a non-existing theme $theme");
443 }
444 return $themes[$theme]->info['name'];
445 }
446
447 /**
448 * Wraps system_list_reset().
449 */
450 protected function systemListReset() {
451 system_list_reset();
452 }
453
454 /**
455 * Wraps system_list().
456 *
457 * @return array
458 * A list of themes keyed by name.
459 */
460 protected function systemThemeList() {
461 return system_list('theme');
462 } 176 }
463 177
464 /** 178 /**
465 * {@inheritdoc} 179 * {@inheritdoc}
466 */ 180 */