annotate core/lib/Drupal/Core/Asset/AssetResolver.php @ 19:fa3358dc1485 tip

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