1<?php 2 3namespace Drupal\Core\Asset; 4 5use Drupal\Component\Utility\Crypt; 6use Drupal\Component\Utility\NestedArray; 7use Drupal\Core\Cache\CacheBackendInterface; 8use Drupal\Core\Extension\ModuleHandlerInterface; 9use Drupal\Core\Language\LanguageManagerInterface; 10use Drupal\Core\Theme\ThemeManagerInterface; 11 12/** 13 * The default asset resolver. 14 */ 15class AssetResolver implements AssetResolverInterface { 16 17 /** 18 * The library discovery service. 19 * 20 * @var \Drupal\Core\Asset\LibraryDiscoveryInterface 21 */ 22 protected $libraryDiscovery; 23 24 /** 25 * The library dependency resolver. 26 * 27 * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface 28 */ 29 protected $libraryDependencyResolver; 30 31 /** 32 * The module handler. 33 * 34 * @var \Drupal\Core\Extension\ModuleHandlerInterface 35 */ 36 protected $moduleHandler; 37 38 /** 39 * The theme manager. 40 * 41 * @var \Drupal\Core\Theme\ThemeManagerInterface 42 */ 43 protected $themeManager; 44 45 /** 46 * The language manager. 47 * 48 * @var \Drupal\Core\Language\LanguageManagerInterface 49 */ 50 protected $languageManager; 51 52 /** 53 * The cache backend. 54 * 55 * @var \Drupal\Core\Cache\CacheBackendInterface 56 */ 57 protected $cache; 58 59 /** 60 * Constructs a new AssetResolver instance. 61 * 62 * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery 63 * The library discovery service. 64 * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver 65 * The library dependency resolver. 66 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler 67 * The module handler. 68 * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager 69 * The theme manager. 70 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager 71 * The language manager. 72 * @param \Drupal\Core\Cache\CacheBackendInterface $cache 73 * The cache backend. 74 */ 75 public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) { 76 $this->libraryDiscovery = $library_discovery; 77 $this->libraryDependencyResolver = $library_dependency_resolver; 78 $this->moduleHandler = $module_handler; 79 $this->themeManager = $theme_manager; 80 $this->languageManager = $language_manager; 81 $this->cache = $cache; 82 } 83 84 /** 85 * Returns the libraries that need to be loaded. 86 * 87 * For example, with core/a depending on core/c and core/b on core/d: 88 * @code 89 * $assets = new AttachedAssets(); 90 * $assets->setLibraries(['core/a', 'core/b', 'core/c']); 91 * $assets->setAlreadyLoadedLibraries(['core/c']); 92 * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d'] 93 * @endcode 94 * 95 * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets 96 * The assets attached to the current response. 97 * 98 * @return string[] 99 * A list of libraries and their dependencies, in the order they should be 100 * loaded, excluding any libraries that have already been loaded. 101 */ 102 protected function getLibrariesToLoad(AttachedAssetsInterface $assets) { 103 return array_diff( 104 $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()), 105 $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()) 106 ); 107 } 108 109 /** 110 * {@inheritdoc} 111 */ 112 public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { 113 $theme_info = $this->themeManager->getActiveTheme(); 114 // Add the theme name to the cache key since themes may implement 115 // hook_library_info_alter(). 116 $libraries_to_load = $this->getLibrariesToLoad($assets); 117 $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize; 118 if ($cached = $this->cache->get($cid)) { 119 return $cached->data; 120 } 121 122 $css = []; 123 $default_options = [ 124 'type' => 'file', 125 'group' => CSS_AGGREGATE_DEFAULT, 126 'weight' => 0, 127 'media' => 'all', 128 'preprocess' => TRUE, 129 'browsers' => [], 130 ]; 131 132 foreach ($libraries_to_load as $library) { 133 list($extension, $name) = explode('/', $library, 2); 134 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); 135 if (isset($definition['css'])) { 136 foreach ($definition['css'] as $options) { 137 $options += $default_options; 138 $options['browsers'] += [ 139 'IE' => TRUE, 140 '!IE' => TRUE, 141 ]; 142 143 // Files with a query string cannot be preprocessed. 144 if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) { 145 $options['preprocess'] = FALSE; 146 } 147 148 // Always add a tiny value to the weight, to conserve the insertion 149 // order. 150 $options['weight'] += count($css) / 1000; 151 152 // CSS files are being keyed by the full path. 153 $css[$options['data']] = $options; 154 } 155 } 156 } 157 158 // Allow modules and themes to alter the CSS assets. 159 $this->moduleHandler->alter('css', $css, $assets); 160 $this->themeManager->alter('css', $css, $assets); 161 162 // Sort CSS items, so that they appear in the correct order. 163 uasort($css, 'static::sort'); 164 165 // Allow themes to remove CSS files by CSS files full path and file name. 166 // @todo Remove in Drupal 9.0.x. 167 if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) { 168 foreach ($css as $key => $options) { 169 if (isset($stylesheet_remove[$key])) { 170 unset($css[$key]); 171 } 172 } 173 } 174 175 if ($optimize) { 176 $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); 177 } 178 $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); 179 180 return $css; 181 } 182 183 /** 184 * Returns the JavaScript settings assets for this response's libraries. 185 * 186 * Gathers all drupalSettings from all libraries in the attached assets 187 * collection and merges them. 188 * 189 * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets 190 * The assets attached to the current response. 191 * 192 * @return array 193 * A (possibly optimized) collection of JavaScript assets. 194 */ 195 protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { 196 $settings = []; 197 198 foreach ($this->getLibrariesToLoad($assets) as $library) { 199 list($extension, $name) = explode('/', $library, 2); 200 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); 201 if (isset($definition['drupalSettings'])) { 202 $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE); 203 } 204 } 205 206 return $settings; 207 } 208 209 /** 210 * {@inheritdoc} 211 */ 212 public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { 213 $theme_info = $this->themeManager->getActiveTheme(); 214 // Add the theme name to the cache key since themes may implement 215 // hook_library_info_alter(). Additionally add the current language to 216 // support translation of JavaScript files via hook_js_alter(). 217 $libraries_to_load = $this->getLibrariesToLoad($assets); 218 $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; 219 220 if ($cached = $this->cache->get($cid)) { 221 list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data; 222 } 223 else { 224 $javascript = []; 225 $default_options = [ 226 'type' => 'file', 227 'group' => JS_DEFAULT, 228 'weight' => 0, 229 'cache' => TRUE, 230 'preprocess' => TRUE, 231 'attributes' => [], 232 'version' => NULL, 233 'browsers' => [], 234 ]; 235 236 // Collect all libraries that contain JS assets and are in the header. 237 $header_js_libraries = []; 238 foreach ($libraries_to_load as $library) { 239 list($extension, $name) = explode('/', $library, 2); 240 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); 241 if (isset($definition['js']) && !empty($definition['header'])) { 242 $header_js_libraries[] = $library; 243 } 244 } 245 // The current list of header JS libraries are only those libraries that 246 // are in the header, but their dependencies must also be loaded for them 247 // to function correctly, so update the list with those. 248 $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries); 249 250 foreach ($libraries_to_load as $library) { 251 list($extension, $name) = explode('/', $library, 2); 252 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); 253 if (isset($definition['js'])) { 254 foreach ($definition['js'] as $options) { 255 $options += $default_options; 256 257 // 'scope' is a calculated option, based on which libraries are 258 // marked to be loaded from the header (see above). 259 $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer'; 260 261 // Preprocess can only be set if caching is enabled and no 262 // attributes are set. 263 $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE; 264 265 // Always add a tiny value to the weight, to conserve the insertion 266 // order. 267 $options['weight'] += count($javascript) / 1000; 268 269 // Local and external files must keep their name as the associative 270 // key so the same JavaScript file is not added twice. 271 $javascript[$options['data']] = $options; 272 } 273 } 274 } 275 276 // Allow modules and themes to alter the JavaScript assets. 277 $this->moduleHandler->alter('js', $javascript, $assets); 278 $this->themeManager->alter('js', $javascript, $assets); 279 280 // Sort JavaScript assets, so that they appear in the correct order. 281 uasort($javascript, 'static::sort'); 282 283 // Prepare the return value: filter JavaScript assets per scope. 284 $js_assets_header = []; 285 $js_assets_footer = []; 286 foreach ($javascript as $key => $item) { 287 if ($item['scope'] == 'header') { 288 $js_assets_header[$key] = $item; 289 } 290 elseif ($item['scope'] == 'footer') { 291 $js_assets_footer[$key] = $item; 292 } 293 } 294 295 if ($optimize) { 296 $collection_optimizer = \Drupal::service('asset.js.collection_optimizer'); 297 $js_assets_header = $collection_optimizer->optimize($js_assets_header); 298 $js_assets_footer = $collection_optimizer->optimize($js_assets_footer); 299 } 300 301 // If the core/drupalSettings library is being loaded or is already 302 // loaded, get the JavaScript settings assets, and convert them into a 303 // single "regular" JavaScript asset. 304 $libraries_to_load = $this->getLibrariesToLoad($assets); 305 $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); 306 $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0; 307 308 // Initialize settings to FALSE since they are not needed by default. This 309 // distinguishes between an empty array which must still allow 310 // hook_js_settings_alter() to be run. 311 $settings = FALSE; 312 if ($settings_required && $settings_have_changed) { 313 $settings = $this->getJsSettingsAssets($assets); 314 // Allow modules to add cached JavaScript settings. 315 foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) { 316 $function = $module . '_js_settings_build'; 317 $function($settings, $assets); 318 } 319 } 320 $settings_in_header = in_array('core/drupalSettings', $header_js_libraries); 321 $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']); 322 } 323 324 if ($settings !== FALSE) { 325 // Attached settings override both library definitions and 326 // hook_js_settings_build(). 327 $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); 328 // Allow modules and themes to alter the JavaScript settings. 329 $this->moduleHandler->alter('js_settings', $settings, $assets); 330 $this->themeManager->alter('js_settings', $settings, $assets); 331 // Update the $assets object accordingly, so that it reflects the final 332 // settings. 333 $assets->setSettings($settings); 334 $settings_as_inline_javascript = [ 335 'type' => 'setting', 336 'group' => JS_SETTING, 337 'weight' => 0, 338 'browsers' => [], 339 'data' => $settings, 340 ]; 341 $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript]; 342 // Prepend to the list of JS assets, to render it first. Preferably in 343 // the footer, but in the header if necessary. 344 if ($settings_in_header) { 345 $js_assets_header = $settings_js_asset + $js_assets_header; 346 } 347 else { 348 $js_assets_footer = $settings_js_asset + $js_assets_footer; 349 } 350 } 351 return [ 352 $js_assets_header, 353 $js_assets_footer, 354 ]; 355 } 356 357 /** 358 * Sorts CSS and JavaScript resources. 359 * 360 * This sort order helps optimize front-end performance while providing 361 * modules and themes with the necessary control for ordering the CSS and 362 * JavaScript appearing on a page. 363 * 364 * @param $a 365 * First item for comparison. The compared items should be associative 366 * arrays of member items. 367 * @param $b 368 * Second item for comparison. 369 * 370 * @return int 371 */ 372 public static function sort($a, $b) { 373 // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT 374 // group appear before items in the CSS_AGGREGATE_THEME group. Modules may 375 // create additional groups by defining their own constants. 376 if ($a['group'] < $b['group']) { 377 return -1; 378 } 379 elseif ($a['group'] > $b['group']) { 380 return 1; 381 } 382 // Finally, order by weight. 383 elseif ($a['weight'] < $b['weight']) { 384 return -1; 385 } 386 elseif ($a['weight'] > $b['weight']) { 387 return 1; 388 } 389 else { 390 return 0; 391 } 392 } 393 394} 395