Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 6 |
CRAP | |
48.39% |
60 / 124 |
AssetResolver | |
0.00% |
0 / 1 |
|
0.00% |
0 / 6 |
310.18 | |
48.39% |
60 / 124 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 7 |
|||
getLibrariesToLoad | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
getCssAssets | |
0.00% |
0 / 1 |
16.39 | |
68.75% |
22 / 32 |
|||
getJsSettingsAssets | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 7 |
|||
getJsAssets | |
0.00% |
0 / 1 |
58.96 | |
57.58% |
38 / 66 |
|||
sort | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 9 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Asset\AssetResolver. | |
*/ | |
namespace Drupal\Core\Asset; | |
use Drupal\Component\Utility\Crypt; | |
use Drupal\Component\Utility\NestedArray; | |
use Drupal\Core\Cache\CacheBackendInterface; | |
use Drupal\Core\Extension\ModuleHandlerInterface; | |
use Drupal\Core\Language\LanguageManagerInterface; | |
use Drupal\Core\Theme\ThemeManagerInterface; | |
/** | |
* The default asset resolver. | |
*/ | |
class AssetResolver implements AssetResolverInterface { | |
/** | |
* The library discovery service. | |
* | |
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface | |
*/ | |
protected $libraryDiscovery; | |
/** | |
* The library dependency resolver. | |
* | |
* @var \Drupal\Core\Asset\LibraryDependencyResolverInterface | |
*/ | |
protected $libraryDependencyResolver; | |
/** | |
* The module handler. | |
* | |
* @var \Drupal\Core\Extension\ModuleHandlerInterface | |
*/ | |
protected $moduleHandler; | |
/** | |
* The theme manager. | |
* | |
* @var \Drupal\Core\Theme\ThemeManagerInterface | |
*/ | |
protected $themeManager; | |
/** | |
* The language manager. | |
* | |
* @var \Drupal\Core\Language\LanguageManagerInterface $language_manager | |
*/ | |
protected $languageManager; | |
/** | |
* The cache backend. | |
* | |
* @var \Drupal\Core\Cache\CacheBackendInterface | |
*/ | |
protected $cache; | |
/** | |
* Constructs a new AssetResolver instance. | |
* | |
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery | |
* The library discovery service. | |
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver | |
* The library dependency resolver. | |
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler | |
* The module handler. | |
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager | |
* The theme manager. | |
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager | |
* The language manager. | |
* @param \Drupal\Core\Cache\CacheBackendInterface $cache | |
* The cache backend. | |
*/ | |
public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) { | |
$this->libraryDiscovery = $library_discovery; | |
$this->libraryDependencyResolver = $library_dependency_resolver; | |
$this->moduleHandler = $module_handler; | |
$this->themeManager = $theme_manager; | |
$this->languageManager = $language_manager; | |
$this->cache = $cache; | |
} | |
/** | |
* Returns the libraries that need to be loaded. | |
* | |
* For example, with core/a depending on core/c and core/b on core/d: | |
* @code | |
* $assets = new AttachedAssets(); | |
* $assets->setLibraries(['core/a', 'core/b', 'core/c']); | |
* $assets->setAlreadyLoadedLibraries(['core/c']); | |
* $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d'] | |
* @endcode | |
* | |
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets | |
* The assets attached to the current response. | |
* | |
* @return string[] | |
* A list of libraries and their dependencies, in the order they should be | |
* loaded, excluding any libraries that have already been loaded. | |
*/ | |
protected function getLibrariesToLoad(AttachedAssetsInterface $assets) { | |
return array_diff( | |
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()), | |
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()) | |
); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { | |
$theme_info = $this->themeManager->getActiveTheme(); | |
// Add the theme name to the cache key since themes may implement | |
// hook_library_info_alter(). | |
$libraries_to_load = $this->getLibrariesToLoad($assets); | |
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize; | |
if ($cached = $this->cache->get($cid)) { | |
return $cached->data; | |
} | |
$css = []; | |
$default_options = [ | |
'type' => 'file', | |
'group' => CSS_AGGREGATE_DEFAULT, | |
'weight' => 0, | |
'media' => 'all', | |
'preprocess' => TRUE, | |
'browsers' => [], | |
]; | |
foreach ($libraries_to_load as $library) { | |
list($extension, $name) = explode('/', $library, 2); | |
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name); | |
if (isset($definition['css'])) { | |
foreach ($definition['css'] as $options) { | |
$options += $default_options; | |
$options['browsers'] += [ | |
'IE' => TRUE, | |
'!IE' => TRUE, | |
]; | |
// Files with a query string cannot be preprocessed. | |
if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) { | |
$options['preprocess'] = FALSE; | |
} | |
// Always add a tiny value to the weight, to conserve the insertion | |
// order. | |
$options['weight'] += count($css) / 1000; | |
// CSS files are being keyed by the full path. | |
$css[$options['data']] = $options; | |
} | |
} | |
} | |
// Allow modules and themes to alter the CSS assets. | |
$this->moduleHandler->alter('css', $css, $assets); | |
$this->themeManager->alter('css', $css, $assets); | |
// Sort CSS items, so that they appear in the correct order. | |
uasort($css, 'static::sort'); | |
// Allow themes to remove CSS files by CSS files full path and file name. | |
// @todo Remove in Drupal 9.0.x. | |
if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) { | |
foreach ($css as $key => $options) { | |
if (isset($stylesheet_remove[$key])) { | |
unset($css[$key]); | |
} | |
} | |
} | |
if ($optimize) { | |
$css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); | |
} | |
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); | |
return $css; | |
} | |
/** | |
* Returns the JavaScript settings assets for this response's libraries. | |
* | |
* Gathers all drupalSettings from all libraries in the attached assets | |
* collection and merges them. | |
* | |
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets | |
* The assets attached to the current response. | |
* @return array | |
* A (possibly optimized) collection of JavaScript assets. | |
*/ | |
protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { | |
$settings = []; | |
foreach ($this->getLibrariesToLoad($assets) as $library) { | |
list($extension, $name) = explode('/', $library, 2); | |
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name); | |
if (isset($definition['drupalSettings'])) { | |
$settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE); | |
} | |
} | |
return $settings; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { | |
$theme_info = $this->themeManager->getActiveTheme(); | |
// Add the theme name to the cache key since themes may implement | |
// hook_library_info_alter(). Additionally add the current language to | |
// support translation of JavaScript files via hook_js_alter(). | |
$libraries_to_load = $this->getLibrariesToLoad($assets); | |
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; | |
if ($cached = $this->cache->get($cid)) { | |
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data; | |
} | |
else { | |
$javascript = []; | |
$default_options = [ | |
'type' => 'file', | |
'group' => JS_DEFAULT, | |
'weight' => 0, | |
'cache' => TRUE, | |
'preprocess' => TRUE, | |
'attributes' => [], | |
'version' => NULL, | |
'browsers' => [], | |
]; | |
// Collect all libraries that contain JS assets and are in the header. | |
$header_js_libraries = []; | |
foreach ($libraries_to_load as $library) { | |
list($extension, $name) = explode('/', $library, 2); | |
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name); | |
if (isset($definition['js']) && !empty($definition['header'])) { | |
$header_js_libraries[] = $library; | |
} | |
} | |
// The current list of header JS libraries are only those libraries that | |
// are in the header, but their dependencies must also be loaded for them | |
// to function correctly, so update the list with those. | |
$header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries); | |
foreach ($libraries_to_load as $library) { | |
list($extension, $name) = explode('/', $library, 2); | |
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name); | |
if (isset($definition['js'])) { | |
foreach ($definition['js'] as $options) { | |
$options += $default_options; | |
// 'scope' is a calculated option, based on which libraries are | |
// marked to be loaded from the header (see above). | |
$options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer'; | |
// Preprocess can only be set if caching is enabled and no | |
// attributes are set. | |
$options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE; | |
// Always add a tiny value to the weight, to conserve the insertion | |
// order. | |
$options['weight'] += count($javascript) / 1000; | |
// Local and external files must keep their name as the associative | |
// key so the same JavaScript file is not added twice. | |
$javascript[$options['data']] = $options; | |
} | |
} | |
} | |
// Allow modules and themes to alter the JavaScript assets. | |
$this->moduleHandler->alter('js', $javascript, $assets); | |
$this->themeManager->alter('js', $javascript, $assets); | |
// Sort JavaScript assets, so that they appear in the correct order. | |
uasort($javascript, 'static::sort'); | |
// Prepare the return value: filter JavaScript assets per scope. | |
$js_assets_header = []; | |
$js_assets_footer = []; | |
foreach ($javascript as $key => $item) { | |
if ($item['scope'] == 'header') { | |
$js_assets_header[$key] = $item; | |
} | |
elseif ($item['scope'] == 'footer') { | |
$js_assets_footer[$key] = $item; | |
} | |
} | |
if ($optimize) { | |
$collection_optimizer = \Drupal::service('asset.js.collection_optimizer'); | |
$js_assets_header = $collection_optimizer->optimize($js_assets_header); | |
$js_assets_footer = $collection_optimizer->optimize($js_assets_footer); | |
} | |
// If the core/drupalSettings library is being loaded or is already | |
// loaded, get the JavaScript settings assets, and convert them into a | |
// single "regular" JavaScript asset. | |
$libraries_to_load = $this->getLibrariesToLoad($assets); | |
$settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); | |
$settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0; | |
// Initialize settings to FALSE since they are not needed by default. This | |
// distinguishes between an empty array which must still allow | |
// hook_js_settings_alter() to be run. | |
$settings = FALSE; | |
if ($settings_required && $settings_have_changed) { | |
$settings = $this->getJsSettingsAssets($assets); | |
// Allow modules to add cached JavaScript settings. | |
foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) { | |
$function = $module . '_' . 'js_settings_build'; | |
$function($settings, $assets); | |
} | |
} | |
$settings_in_header = in_array('core/drupalSettings', $header_js_libraries); | |
$this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']); | |
} | |
if ($settings !== FALSE) { | |
// Attached settings override both library definitions and | |
// hook_js_settings_build(). | |
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE); | |
// Allow modules and themes to alter the JavaScript settings. | |
$this->moduleHandler->alter('js_settings', $settings, $assets); | |
$this->themeManager->alter('js_settings', $settings, $assets); | |
// Update the $assets object accordingly, so that it reflects the final | |
// settings. | |
$assets->setSettings($settings); | |
$settings_as_inline_javascript = [ | |
'type' => 'setting', | |
'group' => JS_SETTING, | |
'weight' => 0, | |
'browsers' => [], | |
'data' => $settings, | |
]; | |
$settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript]; | |
// Prepend to the list of JS assets, to render it first. Preferably in | |
// the footer, but in the header if necessary. | |
if ($settings_in_header) { | |
$js_assets_header = $settings_js_asset + $js_assets_header; | |
} | |
else { | |
$js_assets_footer = $settings_js_asset + $js_assets_footer; | |
} | |
} | |
return [ | |
$js_assets_header, | |
$js_assets_footer, | |
]; | |
} | |
/** | |
* Sorts CSS and JavaScript resources. | |
* | |
* This sort order helps optimize front-end performance while providing | |
* modules and themes with the necessary control for ordering the CSS and | |
* JavaScript appearing on a page. | |
* | |
* @param $a | |
* First item for comparison. The compared items should be associative | |
* arrays of member items. | |
* @param $b | |
* Second item for comparison. | |
* | |
* @return int | |
*/ | |
public static function sort($a, $b) { | |
// First order by group, so that all items in the CSS_AGGREGATE_DEFAULT | |
// group appear before items in the CSS_AGGREGATE_THEME group. Modules may | |
// create additional groups by defining their own constants. | |
if ($a['group'] < $b['group']) { | |
return -1; | |
} | |
elseif ($a['group'] > $b['group']) { | |
return 1; | |
} | |
// Finally, order by weight. | |
elseif ($a['weight'] < $b['weight']) { | |
return -1; | |
} | |
elseif ($a['weight'] > $b['weight']) { | |
return 1; | |
} | |
else { | |
return 0; | |
} | |
} | |
} |