3 namespace Drupal\Core\Theme;
5 use Drupal\Core\Cache\CacheBackendInterface;
6 use Drupal\Core\Extension\Extension;
7 use Drupal\Core\Extension\ModuleHandlerInterface;
8 use Drupal\Core\Extension\ThemeHandlerInterface;
11 * Provides the theme initialization logic.
13 class ThemeInitialization implements ThemeInitializationInterface {
18 * @var \Drupal\Core\Extension\ThemeHandlerInterface
20 protected $themeHandler;
23 * The cache backend to use for the active theme.
25 * @var \Drupal\Core\Cache\CacheBackendInterface
37 * The extensions that might be attaching assets.
41 protected $extensions;
44 * Constructs a new ThemeInitialization object.
48 * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
50 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
52 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
53 * The module handler to use to load modules.
55 public function __construct($root, ThemeHandlerInterface $theme_handler, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) {
57 $this->themeHandler = $theme_handler;
58 $this->cache = $cache;
59 $this->moduleHandler = $module_handler;
65 public function initTheme($theme_name) {
66 $active_theme = $this->getActiveThemeByName($theme_name);
67 $this->loadActiveTheme($active_theme);
75 public function getActiveThemeByName($theme_name) {
76 if ($cached = $this->cache->get('theme.active_theme.' . $theme_name)) {
79 $themes = $this->themeHandler->listInfo();
81 // If no theme could be negotiated, or if the negotiated theme is not within
82 // the list of installed themes, fall back to the default theme output of
83 // core and modules (like Stark, but without a theme extension at all). This
84 // is possible, because loadActiveTheme() always loads the Twig theme
85 // engine. This is desired, because missing or malformed theme configuration
86 // should not leave the application in a broken state. By falling back to
87 // default output, the user is able to reconfigure the theme through the UI.
88 // Lastly, tests are expected to operate with no theme by default, so as to
89 // only assert the original theme output of modules (unless a test manually
90 // installs a specific theme).
91 if (empty($themes) || !$theme_name || !isset($themes[$theme_name])) {
93 // /core/core.info.yml does not actually exist, but is required because
94 // Extension expects a pathname.
95 $active_theme = $this->getActiveTheme(new Extension($this->root, 'theme', 'core/core.info.yml'));
97 // Early-return and do not set state, because the initialized $theme_name
98 // differs from the original $theme_name.
102 // Find all our ancestor themes and put them in an array.
104 $ancestor = $theme_name;
105 while ($ancestor && isset($themes[$ancestor]->base_theme)) {
106 $ancestor = $themes[$ancestor]->base_theme;
107 if (!$this->themeHandler->themeExists($ancestor)) {
108 if ($ancestor == 'stable') {
109 // Themes that depend on Stable will be fixed by system_update_8014().
110 // There is no harm in not adding it as an ancestor since at worst
111 // some people might experience slight visual regressions on
115 throw new MissingThemeDependencyException(sprintf('Base theme %s has not been installed.', $ancestor), $ancestor);
117 $base_themes[] = $themes[$ancestor];
120 $active_theme = $this->getActiveTheme($themes[$theme_name], $base_themes);
122 $this->cache->set('theme.active_theme.' . $theme_name, $active_theme);
123 return $active_theme;
129 public function loadActiveTheme(ActiveTheme $active_theme) {
130 // Initialize the theme.
131 if ($theme_engine = $active_theme->getEngine()) {
132 // Include the engine.
133 include_once $this->root . '/' . $active_theme->getOwner();
135 if (function_exists($theme_engine . '_init')) {
136 foreach ($active_theme->getBaseThemes() as $base) {
137 call_user_func($theme_engine . '_init', $base->getExtension());
139 call_user_func($theme_engine . '_init', $active_theme->getExtension());
143 // include non-engine theme files
144 foreach ($active_theme->getBaseThemes() as $base) {
145 // Include the theme file or the engine.
146 if ($base->getOwner()) {
147 include_once $this->root . '/' . $base->getOwner();
150 // and our theme gets one too.
151 if ($active_theme->getOwner()) {
152 include_once $this->root . '/' . $active_theme->getOwner();
156 // Always include Twig as the default theme engine.
157 include_once $this->root . '/core/themes/engines/twig/twig.engine';
163 public function getActiveTheme(Extension $theme, array $base_themes = []) {
164 $theme_path = $theme->getPath();
166 $values['path'] = $theme_path;
167 $values['name'] = $theme->getName();
169 // @todo Remove in Drupal 9.0.x.
170 $values['stylesheets_remove'] = $this->prepareStylesheetsRemove($theme, $base_themes);
172 // Prepare libraries overrides from this theme and ancestor themes. This
173 // allows child themes to easily remove CSS files from base themes and
175 $values['libraries_override'] = [];
177 // Get libraries overrides declared by base themes.
178 foreach ($base_themes as $base) {
179 if (!empty($base->info['libraries-override'])) {
180 foreach ($base->info['libraries-override'] as $library => $override) {
181 $values['libraries_override'][$base->getPath()][$library] = $override;
186 // Add libraries overrides declared by this theme.
187 if (!empty($theme->info['libraries-override'])) {
188 foreach ($theme->info['libraries-override'] as $library => $override) {
189 $values['libraries_override'][$theme->getPath()][$library] = $override;
193 // Get libraries extensions declared by base themes.
194 foreach ($base_themes as $base) {
195 if (!empty($base->info['libraries-extend'])) {
196 foreach ($base->info['libraries-extend'] as $library => $extend) {
197 if (isset($values['libraries_extend'][$library])) {
198 // Merge if libraries-extend has already been defined for this
200 $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend);
203 $values['libraries_extend'][$library] = $extend;
208 // Add libraries extensions declared by this theme.
209 if (!empty($theme->info['libraries-extend'])) {
210 foreach ($theme->info['libraries-extend'] as $library => $extend) {
211 if (isset($values['libraries_extend'][$library])) {
212 // Merge if libraries-extend has already been defined for this
214 $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend);
217 $values['libraries_extend'][$library] = $extend;
222 // Do basically the same as the above for libraries
223 $values['libraries'] = [];
225 // Grab libraries from base theme
226 foreach ($base_themes as $base) {
227 if (!empty($base->libraries)) {
228 foreach ($base->libraries as $library) {
229 $values['libraries'][] = $library;
234 // Add libraries used by this theme.
235 if (!empty($theme->libraries)) {
236 foreach ($theme->libraries as $library) {
237 $values['libraries'][] = $library;
241 $values['engine'] = isset($theme->engine) ? $theme->engine : NULL;
242 $values['owner'] = isset($theme->owner) ? $theme->owner : NULL;
243 $values['extension'] = $theme;
245 $base_active_themes = [];
246 foreach ($base_themes as $base_theme) {
247 $base_active_themes[$base_theme->getName()] = $this->getActiveTheme($base_theme, array_slice($base_themes, 1));
250 $values['base_themes'] = $base_active_themes;
251 if (!empty($theme->info['regions'])) {
252 $values['regions'] = $theme->info['regions'];
255 return new ActiveTheme($values);
259 * Gets all extensions.
263 protected function getExtensions() {
264 if (!isset($this->extensions)) {
265 $this->extensions = array_merge($this->moduleHandler->getModuleList(), $this->themeHandler->listInfo());
267 return $this->extensions;
271 * Gets CSS file where tokens have been resolved.
273 * @param string $css_file
274 * CSS file which may contain tokens.
277 * CSS file where placeholders are replaced.
279 * @todo Remove in Drupal 9.0.x.
281 protected function resolveStyleSheetPlaceholders($css_file) {
282 $token_candidate = explode('/', $css_file)[0];
283 if (!preg_match('/@[A-z0-9_-]+/', $token_candidate)) {
287 $token = substr($token_candidate, 1);
290 $extensions = $this->getExtensions();
291 if (isset($extensions[$token])) {
292 return str_replace($token_candidate, $extensions[$token]->getPath(), $css_file);
297 * Prepares stylesheets-remove specified in the *.info.yml file.
299 * @param \Drupal\Core\Extension\Extension $theme
300 * The theme extension object.
301 * @param \Drupal\Core\Extension\Extension[] $base_themes
302 * An array of base themes.
305 * The list of stylesheets-remove specified in the *.info.yml file.
307 * @todo Remove in Drupal 9.0.x.
309 protected function prepareStylesheetsRemove(Extension $theme, $base_themes) {
310 // Prepare stylesheets from this theme as well as all ancestor themes.
311 // We work it this way so that we can have child themes remove CSS files
312 // easily from parent.
313 $stylesheets_remove = [];
314 // Grab stylesheets from base theme.
315 foreach ($base_themes as $base) {
316 if (!empty($base->info['stylesheets-remove'])) {
317 foreach ($base->info['stylesheets-remove'] as $css_file) {
318 $css_file = $this->resolveStyleSheetPlaceholders($css_file);
319 $stylesheets_remove[$css_file] = $css_file;
324 // Add stylesheets used by this theme.
325 if (!empty($theme->info['stylesheets-remove'])) {
326 foreach ($theme->info['stylesheets-remove'] as $css_file) {
327 $css_file = $this->resolveStyleSheetPlaceholders($css_file);
328 $stylesheets_remove[$css_file] = $css_file;
331 return $stylesheets_remove;