3 namespace Drupal\Core\Asset;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Core\Cache\CacheBackendInterface;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Language\LanguageManagerInterface;
10 use Drupal\Core\Theme\ThemeManagerInterface;
13 * The default asset resolver.
15 class AssetResolver implements AssetResolverInterface {
18 * The library discovery service.
20 * @var \Drupal\Core\Asset\LibraryDiscoveryInterface
22 protected $libraryDiscovery;
25 * The library dependency resolver.
27 * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
29 protected $libraryDependencyResolver;
34 * @var \Drupal\Core\Extension\ModuleHandlerInterface
36 protected $moduleHandler;
41 * @var \Drupal\Core\Theme\ThemeManagerInterface
43 protected $themeManager;
46 * The language manager.
48 * @var \Drupal\Core\Language\LanguageManagerInterface
50 protected $languageManager;
55 * @var \Drupal\Core\Cache\CacheBackendInterface
60 * Constructs a new AssetResolver instance.
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
68 * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
70 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
71 * The language manager.
72 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
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;
85 * Returns the libraries that need to be loaded.
87 * For example, with core/a depending on core/c and core/b on core/d:
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']
95 * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
96 * The assets attached to the current response.
99 * A list of libraries and their dependencies, in the order they should be
100 * loaded, excluding any libraries that have already been loaded.
102 protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
104 $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
105 $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
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;
125 'group' => CSS_AGGREGATE_DEFAULT,
128 'preprocess' => TRUE,
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'] += [
143 // Files with a query string cannot be preprocessed.
144 if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
145 $options['preprocess'] = FALSE;
148 // Always add a tiny value to the weight, to conserve the insertion
150 $options['weight'] += count($css) / 1000;
152 // CSS files are being keyed by the full path.
153 $css[$options['data']] = $options;
158 // Allow modules and themes to alter the CSS assets.
159 $this->moduleHandler->alter('css', $css, $assets);
160 $this->themeManager->alter('css', $css, $assets);
162 // Sort CSS items, so that they appear in the correct order.
163 uasort($css, 'static::sort');
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])) {
176 $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
178 $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
184 * Returns the JavaScript settings assets for this response's libraries.
186 * Gathers all drupalSettings from all libraries in the attached assets
187 * collection and merges them.
189 * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
190 * The assets attached to the current response.
192 * A (possibly optimized) collection of JavaScript assets.
194 protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
197 foreach ($this->getLibrariesToLoad($assets) as $library) {
198 list($extension, $name) = explode('/', $library, 2);
199 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
200 if (isset($definition['drupalSettings'])) {
201 $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
211 public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
212 $theme_info = $this->themeManager->getActiveTheme();
213 // Add the theme name to the cache key since themes may implement
214 // hook_library_info_alter(). Additionally add the current language to
215 // support translation of JavaScript files via hook_js_alter().
216 $libraries_to_load = $this->getLibrariesToLoad($assets);
217 $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
219 if ($cached = $this->cache->get($cid)) {
220 list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
226 'group' => JS_DEFAULT,
229 'preprocess' => TRUE,
235 // Collect all libraries that contain JS assets and are in the header.
236 $header_js_libraries = [];
237 foreach ($libraries_to_load as $library) {
238 list($extension, $name) = explode('/', $library, 2);
239 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
240 if (isset($definition['js']) && !empty($definition['header'])) {
241 $header_js_libraries[] = $library;
244 // The current list of header JS libraries are only those libraries that
245 // are in the header, but their dependencies must also be loaded for them
246 // to function correctly, so update the list with those.
247 $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
249 foreach ($libraries_to_load as $library) {
250 list($extension, $name) = explode('/', $library, 2);
251 $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
252 if (isset($definition['js'])) {
253 foreach ($definition['js'] as $options) {
254 $options += $default_options;
256 // 'scope' is a calculated option, based on which libraries are
257 // marked to be loaded from the header (see above).
258 $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
260 // Preprocess can only be set if caching is enabled and no
261 // attributes are set.
262 $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
264 // Always add a tiny value to the weight, to conserve the insertion
266 $options['weight'] += count($javascript) / 1000;
268 // Local and external files must keep their name as the associative
269 // key so the same JavaScript file is not added twice.
270 $javascript[$options['data']] = $options;
275 // Allow modules and themes to alter the JavaScript assets.
276 $this->moduleHandler->alter('js', $javascript, $assets);
277 $this->themeManager->alter('js', $javascript, $assets);
279 // Sort JavaScript assets, so that they appear in the correct order.
280 uasort($javascript, 'static::sort');
282 // Prepare the return value: filter JavaScript assets per scope.
283 $js_assets_header = [];
284 $js_assets_footer = [];
285 foreach ($javascript as $key => $item) {
286 if ($item['scope'] == 'header') {
287 $js_assets_header[$key] = $item;
289 elseif ($item['scope'] == 'footer') {
290 $js_assets_footer[$key] = $item;
295 $collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
296 $js_assets_header = $collection_optimizer->optimize($js_assets_header);
297 $js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
300 // If the core/drupalSettings library is being loaded or is already
301 // loaded, get the JavaScript settings assets, and convert them into a
302 // single "regular" JavaScript asset.
303 $libraries_to_load = $this->getLibrariesToLoad($assets);
304 $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
305 $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
307 // Initialize settings to FALSE since they are not needed by default. This
308 // distinguishes between an empty array which must still allow
309 // hook_js_settings_alter() to be run.
311 if ($settings_required && $settings_have_changed) {
312 $settings = $this->getJsSettingsAssets($assets);
313 // Allow modules to add cached JavaScript settings.
314 foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
315 $function = $module . '_' . 'js_settings_build';
316 $function($settings, $assets);
319 $settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
320 $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
323 if ($settings !== FALSE) {
324 // Attached settings override both library definitions and
325 // hook_js_settings_build().
326 $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
327 // Allow modules and themes to alter the JavaScript settings.
328 $this->moduleHandler->alter('js_settings', $settings, $assets);
329 $this->themeManager->alter('js_settings', $settings, $assets);
330 // Update the $assets object accordingly, so that it reflects the final
332 $assets->setSettings($settings);
333 $settings_as_inline_javascript = [
335 'group' => JS_SETTING,
340 $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
341 // Prepend to the list of JS assets, to render it first. Preferably in
342 // the footer, but in the header if necessary.
343 if ($settings_in_header) {
344 $js_assets_header = $settings_js_asset + $js_assets_header;
347 $js_assets_footer = $settings_js_asset + $js_assets_footer;
357 * Sorts CSS and JavaScript resources.
359 * This sort order helps optimize front-end performance while providing
360 * modules and themes with the necessary control for ordering the CSS and
361 * JavaScript appearing on a page.
364 * First item for comparison. The compared items should be associative
365 * arrays of member items.
367 * Second item for comparison.
371 public static function sort($a, $b) {
372 // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
373 // group appear before items in the CSS_AGGREGATE_THEME group. Modules may
374 // create additional groups by defining their own constants.
375 if ($a['group'] < $b['group']) {
378 elseif ($a['group'] > $b['group']) {
381 // Finally, order by weight.
382 elseif ($a['weight'] < $b['weight']) {
385 elseif ($a['weight'] > $b['weight']) {